From ab17a13157977a8f58b56bb6a6540e0234c2b444 Mon Sep 17 00:00:00 2001 From: Keith Massey Date: Fri, 18 Nov 2022 16:48:35 -0600 Subject: [PATCH 001/919] Requiring the presence of the geoip fixture to run GeoIpDownloaderStats.testStats() (#91662) --- .../elasticsearch/ingest/geoip/GeoIpDownloaderStatsIT.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/ingest-geoip/src/internalClusterTest/java/org/elasticsearch/ingest/geoip/GeoIpDownloaderStatsIT.java b/modules/ingest-geoip/src/internalClusterTest/java/org/elasticsearch/ingest/geoip/GeoIpDownloaderStatsIT.java index 288547b6a72d..6076063a38b5 100644 --- a/modules/ingest-geoip/src/internalClusterTest/java/org/elasticsearch/ingest/geoip/GeoIpDownloaderStatsIT.java +++ b/modules/ingest-geoip/src/internalClusterTest/java/org/elasticsearch/ingest/geoip/GeoIpDownloaderStatsIT.java @@ -66,6 +66,11 @@ public void disableDownloader() { } public void testStats() throws Exception { + /* + * Testing without the geoip endpoint fixture falls back to https://storage.googleapis.com/, which can cause this test to run too + * slowly to pass. + */ + assumeTrue("only test with fixture to have stable results", ENDPOINT != null); GeoIpDownloaderStatsAction.Request req = new GeoIpDownloaderStatsAction.Request(); GeoIpDownloaderStatsAction.Response response = client().execute(GeoIpDownloaderStatsAction.INSTANCE, req).actionGet(); XContentTestUtils.JsonMapView jsonMapView = new XContentTestUtils.JsonMapView(convertToMap(response)); From b22719844d5246123a70b9ebb7b57c346cc0898b Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Sat, 19 Nov 2022 13:26:54 +1100 Subject: [PATCH 002/919] Add getRandom method to BuildParams for convenience (#91674) It should help with reducing the ceremonies needed for getting a reproducible random value (mostly boolean) in build.gradle files. Relates: https://github.com/elastic/elasticsearch/pull/91536#discussion_r1026075192 --- .../org/elasticsearch/gradle/internal/info/BuildParams.java | 5 +++++ modules/repository-azure/build.gradle | 3 +-- modules/repository-s3/build.gradle | 2 +- x-pack/plugin/security/qa/jwt-realm/build.gradle | 2 +- x-pack/plugin/security/qa/profile/build.gradle | 2 +- .../legacy-with-basic-license/build.gradle | 2 +- .../legacy-with-full-license/build.gradle | 2 +- 7 files changed, 11 insertions(+), 7 deletions(-) diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/BuildParams.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/BuildParams.java index 33a859747681..784b3fd7325e 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/BuildParams.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/BuildParams.java @@ -18,6 +18,7 @@ import java.time.ZonedDateTime; import java.util.Arrays; import java.util.List; +import java.util.Random; import java.util.function.Consumer; import static java.util.Objects.requireNonNull; @@ -110,6 +111,10 @@ public static String getTestSeed() { return value(testSeed); } + public static Random getRandom() { + return new Random(Long.parseUnsignedLong(testSeed.split(":", 1)[0], 16)); + } + public static Boolean isCi() { return value(isCi); } diff --git a/modules/repository-azure/build.gradle b/modules/repository-azure/build.gradle index 4fa84e80bf4a..03decb2eefaf 100644 --- a/modules/repository-azure/build.gradle +++ b/modules/repository-azure/build.gradle @@ -370,10 +370,9 @@ testClusters.matching { it.name == "yamlRestTest" }.configureEach { } if (useFixture) { setting 'azure.client.integration_test.endpoint_suffix', azureAddress - String firstPartOfSeed = BuildParams.testSeed.tokenize(':').get(0) def ignoreTestSeed = providers.systemProperty('ignore.tests.seed').isPresent() ? PropertyNormalization.IGNORE_VALUE : PropertyNormalization.DEFAULT - setting 'thread_pool.repository_azure.max', (Math.abs(Long.parseUnsignedLong(firstPartOfSeed, 16) % 10) + 1).toString(), ignoreTestSeed + setting 'thread_pool.repository_azure.max', (Math.abs(BuildParams.random.nextLong() % 10) + 1).toString(), ignoreTestSeed } } diff --git a/modules/repository-s3/build.gradle b/modules/repository-s3/build.gradle index a31bdf7ae9b7..c5e8341c0d8d 100644 --- a/modules/repository-s3/build.gradle +++ b/modules/repository-s3/build.gradle @@ -121,7 +121,7 @@ String s3ECSBasePath = System.getenv("amazon_s3_base_path_ecs") String s3STSBucket = System.getenv("amazon_s3_bucket_sts") String s3STSBasePath = System.getenv("amazon_s3_base_path_sts") -boolean s3DisableChunkedEncoding = (new Random(Long.parseUnsignedLong(BuildParams.testSeed.tokenize(':').get(0), 16))).nextBoolean() +boolean s3DisableChunkedEncoding = BuildParams.random.nextBoolean() // If all these variables are missing then we are testing against the internal fixture instead, which has the following // credentials hard-coded in. diff --git a/x-pack/plugin/security/qa/jwt-realm/build.gradle b/x-pack/plugin/security/qa/jwt-realm/build.gradle index 9f4fd09f7da6..4460be5c45f1 100644 --- a/x-pack/plugin/security/qa/jwt-realm/build.gradle +++ b/x-pack/plugin/security/qa/jwt-realm/build.gradle @@ -10,7 +10,7 @@ dependencies { javaRestTestImplementation project(":client:rest") } -boolean explicitIdTokenType = (new Random(Long.parseUnsignedLong(BuildParams.testSeed.tokenize(':').get(0), 16))).nextBoolean() +boolean explicitIdTokenType = BuildParams.random.nextBoolean() testClusters.matching { it.name == 'javaRestTest' }.configureEach { testDistribution = 'DEFAULT' diff --git a/x-pack/plugin/security/qa/profile/build.gradle b/x-pack/plugin/security/qa/profile/build.gradle index 14e13abb207a..ee59c985e12b 100644 --- a/x-pack/plugin/security/qa/profile/build.gradle +++ b/x-pack/plugin/security/qa/profile/build.gradle @@ -9,7 +9,7 @@ dependencies { javaRestTestImplementation project(':x-pack:plugin:security') } -boolean literalUsername = (new Random(Long.parseUnsignedLong(BuildParams.testSeed.tokenize(':').get(0), 16))).nextBoolean() +boolean literalUsername = BuildParams.random.nextBoolean() testClusters.matching { it.name == 'javaRestTest' }.configureEach { testDistribution = 'DEFAULT' diff --git a/x-pack/qa/multi-cluster-search-security/legacy-with-basic-license/build.gradle b/x-pack/qa/multi-cluster-search-security/legacy-with-basic-license/build.gradle index 9bd526c3616a..77c6f91f75cd 100644 --- a/x-pack/qa/multi-cluster-search-security/legacy-with-basic-license/build.gradle +++ b/x-pack/qa/multi-cluster-search-security/legacy-with-basic-license/build.gradle @@ -12,7 +12,7 @@ restResources { } // randomise between sniff and proxy modes -boolean proxyMode = (new Random(Long.parseUnsignedLong(BuildParams.testSeed.tokenize(':').get(0), 16))).nextBoolean() +boolean proxyMode = BuildParams.random.nextBoolean() def fulfillingCluster = testClusters.register('fulfilling-cluster') { setting 'xpack.security.enabled', 'true' diff --git a/x-pack/qa/multi-cluster-search-security/legacy-with-full-license/build.gradle b/x-pack/qa/multi-cluster-search-security/legacy-with-full-license/build.gradle index 07701a46d5c2..35b7a981fca3 100644 --- a/x-pack/qa/multi-cluster-search-security/legacy-with-full-license/build.gradle +++ b/x-pack/qa/multi-cluster-search-security/legacy-with-full-license/build.gradle @@ -12,7 +12,7 @@ restResources { } // randomise between sniff and proxy modes -boolean proxyMode = (new Random(Long.parseUnsignedLong(BuildParams.testSeed.tokenize(':').get(0), 16))).nextBoolean() +boolean proxyMode = BuildParams.random.nextBoolean() def fulfillingCluster = testClusters.register('fulfilling-cluster') { setting 'xpack.security.enabled', 'true' From d1c5ca257e53ffb1e8e68d502c82d3b4f2c22264 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Sun, 20 Nov 2022 19:12:41 +0100 Subject: [PATCH 003/919] Deduplicate Heavy CCR Repository CS Requests (#91398) We run the same request back to back for each put-follower call during the restore. Also, concurrent put-follower calls will all run the same full CS request concurrently. In older versions prior to https://github.com/elastic/elasticsearch/pull/87235 the concurrency was limited by the size of the snapshot pool. With that fix though, they are run at almost arbitry concurrency when many put-follow requests are executed concurrently. -> fixed by using the existing deduplicator to only run a single remote CS request at a time for each CCR repository. Also, this removes the needless forking in the put-follower action that is not necessary any longer now that we have the CCR repository non-blocking (we do the same for normal restores that can safely be started from a transport thread), which should fix some bad-ux situations where the snapshot threads are busy on master, making the put-follower requests not go through in time. --- docs/changelog/91398.yaml | 5 + .../action/SingleResultDeduplicator.java | 98 +++++++++++++++++++ .../SingleResultDeduplicatorTests.java | 77 +++++++++++++++ .../xpack/ccr/repository/CcrRepository.java | 37 ++++--- .../CcrRepositoryRetentionLeaseTests.java | 30 +++--- 5 files changed, 220 insertions(+), 27 deletions(-) create mode 100644 docs/changelog/91398.yaml create mode 100644 server/src/main/java/org/elasticsearch/action/SingleResultDeduplicator.java create mode 100644 server/src/test/java/org/elasticsearch/transport/SingleResultDeduplicatorTests.java diff --git a/docs/changelog/91398.yaml b/docs/changelog/91398.yaml new file mode 100644 index 000000000000..2127e983cbef --- /dev/null +++ b/docs/changelog/91398.yaml @@ -0,0 +1,5 @@ +pr: 91398 +summary: Deduplicate Heavy CCR Repository CS Requests +area: CCR +type: bug +issues: [] diff --git a/server/src/main/java/org/elasticsearch/action/SingleResultDeduplicator.java b/server/src/main/java/org/elasticsearch/action/SingleResultDeduplicator.java new file mode 100644 index 000000000000..20db5fb6efca --- /dev/null +++ b/server/src/main/java/org/elasticsearch/action/SingleResultDeduplicator.java @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.action; + +import org.elasticsearch.action.support.ContextPreservingActionListener; +import org.elasticsearch.common.util.concurrent.ThreadContext; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * + * Wraps an async action that consumes an {@link ActionListener} such that multiple invocations of {@link #execute(ActionListener)} can + * share the result from a single call to the wrapped action. This implementation is similar to {@link ResultDeduplicator} but offers + * stronger guarantees of not seeing a stale result ever. Concretely, every invocation of {@link #execute(ActionListener)} is guaranteed to + * be resolved with a response that has been computed at a time after the call to {@code execute} has been made. This allows this class to + * be used to deduplicate results from actions that produce results that change over time transparently. + * + * @param Result type + */ +public final class SingleResultDeduplicator { + + private final ThreadContext threadContext; + + /** + * List of listeners waiting for the execution after the current in-progress execution. If {@code null} then no execution is in + * progress currently, otherwise an execution is in progress and will trigger another execution that will resolve any listeners queued + * up here once done. + */ + private List> waitingListeners; + + private final Consumer> executeAction; + + public SingleResultDeduplicator(ThreadContext threadContext, Consumer> executeAction) { + this.threadContext = threadContext; + this.executeAction = executeAction; + } + + /** + * Execute the action for the given {@code listener}. + * @param listener listener to resolve with execution result + */ + public void execute(ActionListener listener) { + synchronized (this) { + if (waitingListeners == null) { + // no queued up listeners, just execute this one directly without deduplication and instantiate the list so that + // subsequent executions will wait + waitingListeners = new ArrayList<>(); + } else { + // already running an execution, queue this one up + waitingListeners.add(ContextPreservingActionListener.wrapPreservingContext(listener, threadContext)); + return; + } + } + doExecute(listener); + } + + private void doExecute(ActionListener listener) { + final ActionListener wrappedListener = ActionListener.runBefore(listener, () -> { + final List> listeners; + synchronized (this) { + if (waitingListeners.isEmpty()) { + // no listeners were queued up while this execution ran, so we just reset the state to not having a running execution + waitingListeners = null; + return; + } else { + // we have queued up listeners, so we create a fresh list for the next execution and execute once to handle the + // listeners currently queued up + listeners = waitingListeners; + waitingListeners = new ArrayList<>(); + } + } + doExecute(new ActionListener<>() { + @Override + public void onResponse(T response) { + ActionListener.onResponse(listeners, response); + } + + @Override + public void onFailure(Exception e) { + ActionListener.onFailure(listeners, e); + } + }); + }); + try { + executeAction.accept(wrappedListener); + } catch (Exception e) { + wrappedListener.onFailure(e); + } + } +} diff --git a/server/src/test/java/org/elasticsearch/transport/SingleResultDeduplicatorTests.java b/server/src/test/java/org/elasticsearch/transport/SingleResultDeduplicatorTests.java new file mode 100644 index 000000000000..56bfe72241f2 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/transport/SingleResultDeduplicatorTests.java @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.transport; + +import org.apache.lucene.util.SetOnce; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.SingleResultDeduplicator; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.test.ESTestCase; + +public class SingleResultDeduplicatorTests extends ESTestCase { + + public void testDeduplicatesWithoutShowingStaleData() { + final SetOnce> firstListenerRef = new SetOnce<>(); + final SetOnce> secondListenerRef = new SetOnce<>(); + final var deduplicator = new SingleResultDeduplicator<>(new ThreadContext(Settings.EMPTY), l -> { + if (firstListenerRef.trySet(l) == false) { + secondListenerRef.set(l); + } + }); + final Object result1 = new Object(); + final Object result2 = new Object(); + + final int totalListeners = randomIntBetween(2, 10); + final boolean[] called = new boolean[totalListeners]; + deduplicator.execute(new ActionListener<>() { + @Override + public void onResponse(Object response) { + assertFalse(called[0]); + called[0] = true; + assertEquals(result1, response); + } + + @Override + public void onFailure(Exception e) { + throw new AssertionError(e); + } + }); + + for (int i = 1; i < totalListeners; i++) { + final int index = i; + deduplicator.execute(new ActionListener<>() { + + @Override + public void onResponse(Object response) { + assertFalse(called[index]); + called[index] = true; + assertEquals(result2, response); + } + + @Override + public void onFailure(Exception e) { + throw new AssertionError(e); + } + }); + } + for (int i = 0; i < totalListeners; i++) { + assertFalse(called[i]); + } + firstListenerRef.get().onResponse(result1); + assertTrue(called[0]); + for (int i = 1; i < totalListeners; i++) { + assertFalse(called[i]); + } + secondListenerRef.get().onResponse(result2); + for (int i = 0; i < totalListeners; i++) { + assertTrue(called[i]); + } + } +} diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/repository/CcrRepository.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/repository/CcrRepository.java index 9e95befe0dfc..92270b552ee4 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/repository/CcrRepository.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/repository/CcrRepository.java @@ -16,6 +16,7 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.ActionRunnable; +import org.elasticsearch.action.SingleResultDeduplicator; import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse; import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse; @@ -137,6 +138,8 @@ public class CcrRepository extends AbstractLifecycleComponent implements Reposit private final CounterMetric throttledTime = new CounterMetric(); + private final SingleResultDeduplicator csDeduplicator; + public CcrRepository(RepositoryMetadata metadata, Client client, Settings settings, CcrSettings ccrSettings, ThreadPool threadPool) { this.metadata = metadata; this.ccrSettings = ccrSettings; @@ -145,6 +148,17 @@ public CcrRepository(RepositoryMetadata metadata, Client client, Settings settin this.remoteClusterAlias = Strings.split(metadata.name(), NAME_PREFIX)[1]; this.client = client; this.threadPool = threadPool; + csDeduplicator = new SingleResultDeduplicator<>( + threadPool.getThreadContext(), + l -> getRemoteClusterClient().admin() + .cluster() + .prepareState() + .clear() + .setMetadata(true) + .setNodes(true) + .setMasterNodeTimeout(TimeValue.MAX_VALUE) + .execute(l.map(ClusterStateResponse::getState)) + ); } @Override @@ -177,26 +191,22 @@ public void getSnapshotInfo(GetSnapshotInfoContext context) { assert snapshotIds.size() == 1 && SNAPSHOT_ID.equals(snapshotIds.iterator().next()) : "RemoteClusterRepository only supports " + SNAPSHOT_ID + " as the SnapshotId but saw " + snapshotIds; try { - getRemoteClusterClient().admin() - .cluster() - .prepareState() - .clear() - .setMetadata(true) - .setNodes(true) - // fork to the snapshot meta pool because the context expects to run on it and asserts that it does - .execute(new ThreadedActionListener<>(logger, threadPool, ThreadPool.Names.SNAPSHOT_META, context.map(response -> { - Metadata responseMetadata = response.getState().metadata(); + csDeduplicator.execute( + new ThreadedActionListener<>(logger, threadPool, ThreadPool.Names.SNAPSHOT_META, context.map(response -> { + Metadata responseMetadata = response.metadata(); Map indicesMap = responseMetadata.indices(); return new SnapshotInfo( new Snapshot(this.metadata.name(), SNAPSHOT_ID), List.copyOf(indicesMap.keySet()), List.copyOf(responseMetadata.dataStreams().keySet()), List.of(), - response.getState().getNodes().getMaxNodeVersion(), + response.getNodes().getMaxNodeVersion(), SnapshotState.SUCCESS ); - }), false)); + }), false) + ); } catch (Exception e) { + assert false : e; context.onFailure(e); } } @@ -255,8 +265,8 @@ public IndexMetadata getSnapshotIndexMetaData(RepositoryData repositoryData, Sna @Override public void getRepositoryData(ActionListener listener) { try { - getRemoteClusterClient().admin().cluster().prepareState().clear().setMetadata(true).execute(listener.map(response -> { - final Metadata remoteMetadata = response.getState().getMetadata(); + csDeduplicator.execute(listener.map(response -> { + final Metadata remoteMetadata = response.getMetadata(); final String[] concreteAllIndices = remoteMetadata.getConcreteAllIndices(); final Map copiedSnapshotIds = Maps.newMapWithExpectedSize(concreteAllIndices.length); final Map snapshotsDetails = Maps.newMapWithExpectedSize(concreteAllIndices.length); @@ -285,6 +295,7 @@ public void getRepositoryData(ActionListener listener) { ); })); } catch (Exception e) { + assert false; listener.onFailure(e); } } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/repository/CcrRepositoryRetentionLeaseTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/repository/CcrRepositoryRetentionLeaseTests.java index 841a8c45a8ff..a1c52728e9a7 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/repository/CcrRepositoryRetentionLeaseTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/repository/CcrRepositoryRetentionLeaseTests.java @@ -15,6 +15,7 @@ import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.index.Index; import org.elasticsearch.index.seqno.RetentionLeaseActions; import org.elasticsearch.index.seqno.RetentionLeaseAlreadyExistsException; @@ -54,13 +55,7 @@ public void testWhenRetentionLeaseAlreadyExistsWeTryToRenewIt() { CcrSettings.getSettings().stream().filter(Setting::hasNodeScope) ).collect(Collectors.toSet()); - final CcrRepository repository = new CcrRepository( - repositoryMetadata, - mock(Client.class), - Settings.EMPTY, - new CcrSettings(Settings.EMPTY, new ClusterSettings(Settings.EMPTY, settings)), - mock(ThreadPool.class) - ); + final CcrRepository repository = createCcrRepository(repositoryMetadata, settings); final ShardId followerShardId = new ShardId(new Index("follower-index-name", "follower-index-uuid"), 0); final ShardId leaderShardId = new ShardId(new Index("leader-index-name", "leader-index-uuid"), 0); @@ -110,6 +105,19 @@ public void testWhenRetentionLeaseAlreadyExistsWeTryToRenewIt() { verifyNoMoreInteractions(remoteClient); } + private static CcrRepository createCcrRepository(RepositoryMetadata repositoryMetadata, Set> settings) { + final ThreadPool threadPool = mock(ThreadPool.class); + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + when(threadPool.getThreadContext()).thenReturn(threadContext); + return new CcrRepository( + repositoryMetadata, + mock(Client.class), + Settings.EMPTY, + new CcrSettings(Settings.EMPTY, new ClusterSettings(Settings.EMPTY, settings)), + threadPool + ); + } + public void testWhenRetentionLeaseExpiresBeforeWeCanRenewIt() { final RepositoryMetadata repositoryMetadata = mock(RepositoryMetadata.class); when(repositoryMetadata.name()).thenReturn(CcrRepository.NAME_PREFIX); @@ -118,13 +126,7 @@ public void testWhenRetentionLeaseExpiresBeforeWeCanRenewIt() { CcrSettings.getSettings().stream().filter(Setting::hasNodeScope) ).collect(Collectors.toSet()); - final CcrRepository repository = new CcrRepository( - repositoryMetadata, - mock(Client.class), - Settings.EMPTY, - new CcrSettings(Settings.EMPTY, new ClusterSettings(Settings.EMPTY, settings)), - mock(ThreadPool.class) - ); + final CcrRepository repository = createCcrRepository(repositoryMetadata, settings); final ShardId followerShardId = new ShardId(new Index("follower-index-name", "follower-index-uuid"), 0); final ShardId leaderShardId = new ShardId(new Index("leader-index-name", "leader-index-uuid"), 0); From 3eabef87dd533d1948f5c85008629ed37adfcc07 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 21 Nov 2022 12:30:33 +1100 Subject: [PATCH 004/919] Allowed string claims should support just literal matching (#91737) The allowed string claims are meant to support literal matching for the time being. This PR ensures this is the case. Relates: #91001 --- .../authc/jwt/JwtStringClaimValidator.java | 5 +---- .../jwt/JwtStringClaimValidatorTests.java | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidator.java index add8d0d1336c..7afeff7022f2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidator.java @@ -13,7 +13,6 @@ import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.common.Strings; import org.elasticsearch.rest.RestStatus; -import org.elasticsearch.xpack.core.security.support.StringMatcher; import java.text.ParseException; import java.util.List; @@ -33,13 +32,11 @@ public class JwtStringClaimValidator implements JwtFieldValidator { private final List allowedClaimValues; // Whether the claim should be a single string private final boolean singleValuedClaim; - private final StringMatcher claimValueMatcher; public JwtStringClaimValidator(String claimName, List allowedClaimValues, boolean singleValuedClaim) { this.claimName = claimName; this.allowedClaimValues = allowedClaimValues; this.singleValuedClaim = singleValuedClaim; - this.claimValueMatcher = StringMatcher.of(allowedClaimValues); } @Override @@ -54,7 +51,7 @@ public void validate(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet) { throw new ElasticsearchSecurityException("missing required string claim [" + claimName + "]", RestStatus.BAD_REQUEST); } - if (false == claimValues.stream().anyMatch(claimValueMatcher)) { + if (false == claimValues.stream().anyMatch(allowedClaimValues::contains)) { throw new ElasticsearchSecurityException( "string claim [" + claimName diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidatorTests.java index 22a242c4d7c9..671f0073d14b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidatorTests.java @@ -87,6 +87,28 @@ public void testMatchingClaimValues() throws ParseException { assertThat(e.getMessage(), containsString("does not match allowed claim values")); } + public void testDoesNotSupportWildcardOrRegex() throws ParseException { + final String claimName = randomFrom(randomAlphaOfLengthBetween(3, 8)); + final String claimValue = randomFrom("*", "/.*/"); + final JwtStringClaimValidator validator = new JwtStringClaimValidator(claimName, List.of(claimValue), randomBoolean()); + + // It should not match arbitrary claim value because wildcard or regex is not supported + final JWTClaimsSet invalidJwtClaimsSet = JWTClaimsSet.parse(Map.of(claimName, randomAlphaOfLengthBetween(1, 10))); + final ElasticsearchSecurityException e = expectThrows( + ElasticsearchSecurityException.class, + () -> validator.validate(getJwsHeader(), invalidJwtClaimsSet) + ); + assertThat(e.getMessage(), containsString("does not match allowed claim values")); + + // It should support literal matching + final JWTClaimsSet validJwtClaimsSet = JWTClaimsSet.parse(Map.of(claimName, claimValue)); + try { + validator.validate(getJwsHeader(), validJwtClaimsSet); + } catch (Exception e2) { + throw new AssertionError("validation should have passed without exception", e2); + } + } + private JWSHeader getJwsHeader() throws ParseException { return JWSHeader.parse(Map.of("alg", randomAlphaOfLengthBetween(3, 8))); } From dc8ed67c0c88ae981d85234c6b7b0e21e324a747 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Mon, 21 Nov 2022 12:48:18 +1100 Subject: [PATCH 005/919] Add test for SAML with UTF-8 content (#91672) Adds a test for signed SAML assertions with non-ASCII characters. No code changes were required (java.xml, Shibboleth & Santuario already do the right thing for us) but we didn't have proper test coverage. --- .../authc/saml/SamlAuthenticatorTests.java | 60 +++++++++++++++++-- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticatorTests.java index 3fb4757d9f18..19ae53caca08 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticatorTests.java @@ -345,6 +345,45 @@ public void testSuccessfullyParseContentWithMultipleValidAttributes() throws Exc assertThat(attributes.session(), equalTo(session)); } + public void testSuccessfullyParseContentFromRawXmlWithSignedAssertion() throws Exception { + Instant now = clock.instant(); + final String nameId = randomAlphaOfLengthBetween(12, 24); + final String sessionindex = randomId(); + final String xml = getSimpleResponseFromXmlTemplate(now, nameId, sessionindex); + + SamlToken token = token(signAssertions(xml)); + final SamlAttributes attributes = authenticator.authenticate(token); + assertThat(attributes, notNullValue()); + assertThat(attributes.attributes(), iterableWithSize(2)); + final List uid = attributes.getAttributeValues(UID_OID); + assertThat(uid, contains("daredevil")); + assertThat(uid, iterableWithSize(1)); + assertThat(attributes.name(), notNullValue()); + assertThat(attributes.name().format, equalTo(TRANSIENT)); + assertThat(attributes.name().value, equalTo(nameId)); + } + + public void testSuccessfullyParseContentFromRawXmlWithSignedUnicodeAssertion() throws Exception { + Instant now = clock.instant(); + // Picking a completely random, but valid XML, unicode char is hard. We just insert some well known chars. + final String nameId = randomAlphaOfLengthBetween(2, 4) + "セキュリティ" + randomAlphaOfLengthBetween(2, 4); + final String nameIdFormat = "urn:fake:nameid-format:soluções"; + final String sessionindex = randomId(); + // Randomly skip the header because the XML parser is supposed to infer UTF-8 if there is no header + final String xml = getSimpleResponseFromXmlTemplate(randomBoolean(), now, nameIdFormat, nameId, sessionindex); + + SamlToken token = token(signAssertions(xml)); + final SamlAttributes attributes = authenticator.authenticate(token); + assertThat(attributes, notNullValue()); + assertThat(attributes.attributes(), iterableWithSize(2)); + final List uid = attributes.getAttributeValues(UID_OID); + assertThat(uid, contains("daredevil")); + assertThat(uid, iterableWithSize(1)); + assertThat(attributes.name(), notNullValue()); + assertThat(attributes.name().format, equalTo(nameIdFormat)); + assertThat(attributes.name().value, equalTo(nameId)); + } + public void testSuccessfullyParseContentFromEncryptedAssertion() throws Exception { final Instant now = clock.instant(); final String xml = getSimpleResponseAsString(now); @@ -1540,9 +1579,18 @@ private Response getSimpleResponse( } private String getSimpleResponseFromXmlTemplate(Instant now, String nameId, String sessionindex) { + return getSimpleResponseFromXmlTemplate(true, now, TRANSIENT, nameId, sessionindex); + } + + private String getSimpleResponseFromXmlTemplate( + boolean includeXmlHeader, + Instant now, + String nameIdFormat, + String nameId, + String sessionindex + ) { Instant validUntil = now.plusSeconds(30); - String xml = "\n" - + "" + " %(IDP_ENTITY_ID)" + " " - + " %(nameId)" + " " @@ -1586,6 +1634,10 @@ private String getSimpleResponseFromXmlTemplate(Instant now, String nameId, Stri + " " + ""; + if (includeXmlHeader) { + xml = "\n" + xml; + } + final Map replacements = new HashMap<>(); replacements.put("IDP_ENTITY_ID", IDP_ENTITY_ID); replacements.put("METHOD_BEARER", METHOD_BEARER); @@ -1597,7 +1649,7 @@ private String getSimpleResponseFromXmlTemplate(Instant now, String nameId, Stri replacements.put("sessionindex", sessionindex); replacements.put("SP_ACS_URL", SP_ACS_URL); replacements.put("SP_ENTITY_ID", SP_ENTITY_ID); - replacements.put("TRANSIENT", TRANSIENT); + replacements.put("nameIdFormat", nameIdFormat); replacements.put("validUntil", validUntil); return NamedFormatter.format(xml, replacements); From c21bd63cbfdc02eaa3f918546ea43c71765063ea Mon Sep 17 00:00:00 2001 From: David Roberts Date: Mon, 21 Nov 2022 08:17:08 +0000 Subject: [PATCH 006/919] [ML] Impose a minimum on the automatically calculated JVM size (#91732) This change fixes a discrepancy that has existed for a long time but was revealed by #91694. The ML automatic node/JVM sizing code contained a minimum node size but did not restrict the minimum JVM size to the size that would be chosen on that minimum node size. This could throw off calculations at small scale. Fixes #91728 --- .../xpack/ml/utils/NativeMemoryCalculator.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/NativeMemoryCalculator.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/NativeMemoryCalculator.java index 6964639f0535..4920e94ee1f0 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/NativeMemoryCalculator.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/NativeMemoryCalculator.java @@ -40,6 +40,8 @@ public final class NativeMemoryCalculator { // Must match the value used in MachineDependentHeap.MachineNodeRole.ML_ONLY. public static final long JVM_SIZE_KNOT_POINT = ByteSizeValue.ofGb(16).getBytes(); private static final long BYTES_IN_4MB = ByteSizeValue.ofMb(4).getBytes(); + // The minimum automatic node size implicitly defines a minimum JVM size + private static final long MINIMUM_AUTOMATIC_JVM_SIZE = dynamicallyCalculateJvmSizeFromNodeSize(MINIMUM_AUTOMATIC_NODE_SIZE); private NativeMemoryCalculator() {} @@ -165,7 +167,7 @@ public static long dynamicallyCalculateJvmSizeFromNodeSize(long nodeSize) { ); } - public static long dynamicallyCalculateJvmSizeFromMlNativeMemorySize(long nativeMachineMemory) { + public static long dynamicallyCalculateJvmSizeFromMlNativeMemorySize(long mlNativeMemorySize) { // For <= 16GB node, the JVM is 0.4 * total_node_size. This means the rest is 0.6 the node size. // So, nativeAndOverhead = 0.6 * total_node_size => total_node_size = (nativeAndOverhead / 0.6) // Consequently jvmSize = (nativeAndOverhead / 0.6) * 0.4 = nativeAndOverhead * 2 / 3 @@ -177,7 +179,7 @@ public static long dynamicallyCalculateJvmSizeFromMlNativeMemorySize(long native // // In both cases JVM size is rounded down to the next lower multiple of 4 megabytes to match // MachineDependentHeap.MachineNodeRole.ML_ONLY. - long nativeAndOverhead = nativeMachineMemory + OS_OVERHEAD; + long nativeAndOverhead = mlNativeMemorySize + OS_OVERHEAD; long higherAnswer; if (nativeAndOverhead <= (JVM_SIZE_KNOT_POINT - dynamicallyCalculateJvmSizeFromNodeSize(JVM_SIZE_KNOT_POINT))) { higherAnswer = (nativeAndOverhead * 2 / 3 / BYTES_IN_4MB) * BYTES_IN_4MB; @@ -196,10 +198,10 @@ public static long dynamicallyCalculateJvmSizeFromMlNativeMemorySize(long native long lowerAnswer = higherAnswer - BYTES_IN_4MB; long nodeSizeImpliedByLowerAnswer = nativeAndOverhead + lowerAnswer; if (dynamicallyCalculateJvmSizeFromNodeSize(nodeSizeImpliedByLowerAnswer) == lowerAnswer) { - return Math.min(lowerAnswer, STATIC_JVM_UPPER_THRESHOLD); + return Math.max(MINIMUM_AUTOMATIC_JVM_SIZE, Math.min(lowerAnswer, STATIC_JVM_UPPER_THRESHOLD)); } } - return Math.min(higherAnswer, STATIC_JVM_UPPER_THRESHOLD); + return Math.max(MINIMUM_AUTOMATIC_JVM_SIZE, Math.min(higherAnswer, STATIC_JVM_UPPER_THRESHOLD)); } /** From d7fdaaebcd7cf30bb378e56fd78cef722a0c55b8 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 21 Nov 2022 09:41:49 +0100 Subject: [PATCH 007/919] Remove unused user-metadata hooks from shard snapshots (#91736) All the functionality around passing user metadata to shard level snapshots as well as for filtering it was only put in place for the encrypted repository. Now that that's gone in #91617 there's no need to keep this code around that actually prevents some planned optimizations. --- .../RepositoryFilterUserMetadataIT.java | 104 ------------------ .../repositories/Repository.java | 10 -- .../repositories/SnapshotShardContext.java | 11 -- .../snapshots/SnapshotShardsService.java | 82 ++++++-------- .../snapshots/SnapshotsService.java | 3 +- .../ShardSnapshotTaskRunnerTests.java | 2 - .../repositories/fs/FsRepositoryTests.java | 3 - .../index/shard/IndexShardTestCase.java | 1 - .../SourceOnlySnapshotRepository.java | 1 - .../SourceOnlySnapshotShardTests.java | 5 - .../SearchableSnapshotDirectoryTests.java | 2 - 11 files changed, 32 insertions(+), 192 deletions(-) delete mode 100644 server/src/internalClusterTest/java/org/elasticsearch/snapshots/RepositoryFilterUserMetadataIT.java diff --git a/server/src/internalClusterTest/java/org/elasticsearch/snapshots/RepositoryFilterUserMetadataIT.java b/server/src/internalClusterTest/java/org/elasticsearch/snapshots/RepositoryFilterUserMetadataIT.java deleted file mode 100644 index d89c38f323a8..000000000000 --- a/server/src/internalClusterTest/java/org/elasticsearch/snapshots/RepositoryFilterUserMetadataIT.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -package org.elasticsearch.snapshots; - -import org.elasticsearch.cluster.service.ClusterService; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.util.BigArrays; -import org.elasticsearch.env.Environment; -import org.elasticsearch.indices.recovery.RecoverySettings; -import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.plugins.RepositoryPlugin; -import org.elasticsearch.repositories.FinalizeSnapshotContext; -import org.elasticsearch.repositories.Repository; -import org.elasticsearch.repositories.SnapshotShardContext; -import org.elasticsearch.repositories.fs.FsRepository; -import org.elasticsearch.test.ESIntegTestCase; -import org.elasticsearch.xcontent.NamedXContentRegistry; - -import java.util.Collection; -import java.util.Collections; -import java.util.Map; - -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; -import static org.hamcrest.Matchers.is; - -public class RepositoryFilterUserMetadataIT extends ESIntegTestCase { - - @Override - protected Collection> nodePlugins() { - return Collections.singleton(MetadataFilteringPlugin.class); - } - - public void testFilteredRepoMetadataIsUsed() { - final String masterName = internalCluster().getMasterName(); - final String repoName = "test-repo"; - assertAcked( - client().admin() - .cluster() - .preparePutRepository(repoName) - .setType(MetadataFilteringPlugin.TYPE) - .setSettings( - Settings.builder().put("location", randomRepoPath()).put(MetadataFilteringPlugin.MASTER_SETTING_VALUE, masterName) - ) - ); - createIndex("test-idx"); - final SnapshotInfo snapshotInfo = client().admin() - .cluster() - .prepareCreateSnapshot(repoName, "test-snap") - .setWaitForCompletion(true) - .get() - .getSnapshotInfo(); - assertThat(snapshotInfo.userMetadata(), is(Collections.singletonMap(MetadataFilteringPlugin.MOCK_FILTERED_META, masterName))); - } - - // Mock plugin that stores the name of the master node that started a snapshot in each snapshot's metadata - public static final class MetadataFilteringPlugin extends org.elasticsearch.plugins.Plugin implements RepositoryPlugin { - - private static final String MOCK_FILTERED_META = "mock_filtered_meta"; - - private static final String MASTER_SETTING_VALUE = "initial_master"; - - private static final String TYPE = "mock_meta_filtering"; - - @Override - public Map getRepositories( - Environment env, - NamedXContentRegistry namedXContentRegistry, - ClusterService clusterService, - BigArrays bigArrays, - RecoverySettings recoverySettings - ) { - return Collections.singletonMap( - "mock_meta_filtering", - metadata -> new FsRepository(metadata, env, namedXContentRegistry, clusterService, bigArrays, recoverySettings) { - - // Storing the initially expected metadata value here to verify that #filterUserMetadata is only called once on the - // initial master node starting the snapshot - private final String initialMetaValue = metadata.settings().get(MASTER_SETTING_VALUE); - - @Override - public void finalizeSnapshot(FinalizeSnapshotContext finalizeSnapshotContext) { - super.finalizeSnapshot(finalizeSnapshotContext); - } - - @Override - public void snapshotShard(SnapshotShardContext context) { - assertThat(context.userMetadata(), is(Collections.singletonMap(MOCK_FILTERED_META, initialMetaValue))); - super.snapshotShard(context); - } - - @Override - public Map adaptUserMetadata(Map userMetadata) { - return Collections.singletonMap(MOCK_FILTERED_META, clusterService.getNodeName()); - } - } - ); - } - } -} diff --git a/server/src/main/java/org/elasticsearch/repositories/Repository.java b/server/src/main/java/org/elasticsearch/repositories/Repository.java index cc4cd2fc4499..d818476c68c2 100644 --- a/server/src/main/java/org/elasticsearch/repositories/Repository.java +++ b/server/src/main/java/org/elasticsearch/repositories/Repository.java @@ -11,7 +11,6 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateUpdateTask; -import org.elasticsearch.cluster.SnapshotsInProgress; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.metadata.RepositoryMetadata; @@ -31,7 +30,6 @@ import java.io.IOException; import java.util.Collection; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; @@ -299,14 +297,6 @@ void cloneShardSnapshot( ActionListener listener ); - /** - * Hook that allows a repository to filter the user supplied snapshot metadata in {@link SnapshotsInProgress.Entry#userMetadata()} - * during snapshot initialization. - */ - default Map adaptUserMetadata(Map userMetadata) { - return userMetadata; - } - /** * Block until all in-flight operations for this repository have completed. Must only be called after this instance has been closed * by a call to stop {@link #close()}. diff --git a/server/src/main/java/org/elasticsearch/repositories/SnapshotShardContext.java b/server/src/main/java/org/elasticsearch/repositories/SnapshotShardContext.java index 68cef0607151..52fea9deffb3 100644 --- a/server/src/main/java/org/elasticsearch/repositories/SnapshotShardContext.java +++ b/server/src/main/java/org/elasticsearch/repositories/SnapshotShardContext.java @@ -18,8 +18,6 @@ import org.elasticsearch.index.store.Store; import org.elasticsearch.snapshots.SnapshotId; -import java.util.Map; - /** * Context holding the state for creating a shard snapshot via {@link Repository#snapshotShard(SnapshotShardContext)}. * Wraps a {@link org.elasticsearch.index.engine.Engine.IndexCommitRef} that is released once this instances is completed by invoking @@ -36,7 +34,6 @@ public final class SnapshotShardContext extends ActionListener.Delegating userMetadata; private final long snapshotStartTime; /** @@ -50,8 +47,6 @@ public final class SnapshotShardContext extends ActionListener.Delegating userMetadata, final long snapshotStartTime, ActionListener listener ) { @@ -78,7 +72,6 @@ public SnapshotShardContext( this.shardStateIdentifier = shardStateIdentifier; this.snapshotStatus = snapshotStatus; this.repositoryMetaVersion = repositoryMetaVersion; - this.userMetadata = userMetadata; this.snapshotStartTime = snapshotStartTime; } @@ -115,10 +108,6 @@ public Version getRepositoryMetaVersion() { return repositoryMetaVersion; } - public Map userMetadata() { - return userMetadata; - } - public long snapshotStartTime() { return snapshotStartTime; } diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotShardsService.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotShardsService.java index 8357e3467336..16b5a78bac4d 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotShardsService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotShardsService.java @@ -237,15 +237,7 @@ private void startNewSnapshots(List snapshotsInProgre + snapshotStatus.generation() + "] for snapshot with old-format compatibility"; shardSnapshotTasks.add( - newShardSnapshotTask( - shardId, - snapshot, - indexId, - entry.userMetadata(), - snapshotStatus, - entry.version(), - entry.startTime() - ) + newShardSnapshotTask(shardId, snapshot, indexId, snapshotStatus, entry.version(), entry.startTime()) ); } @@ -276,55 +268,45 @@ private Runnable newShardSnapshotTask( final ShardId shardId, final Snapshot snapshot, final IndexId indexId, - final Map userMetadata, final IndexShardSnapshotStatus snapshotStatus, final Version entryVersion, final long entryStartTime ) { // separate method to make sure this lambda doesn't capture any heavy local objects like a SnapshotsInProgress.Entry - return () -> snapshot( - shardId, - snapshot, - indexId, - userMetadata, - snapshotStatus, - entryVersion, - entryStartTime, - new ActionListener<>() { - @Override - public void onResponse(ShardSnapshotResult shardSnapshotResult) { - final ShardGeneration newGeneration = shardSnapshotResult.getGeneration(); - assert newGeneration != null; - assert newGeneration.equals(snapshotStatus.generation()); - if (logger.isDebugEnabled()) { - final IndexShardSnapshotStatus.Copy lastSnapshotStatus = snapshotStatus.asCopy(); - logger.debug( - "[{}][{}] completed snapshot to [{}] with status [{}] at generation [{}]", - shardId, - snapshot, - snapshot.getRepository(), - lastSnapshotStatus, - snapshotStatus.generation() - ); - } - notifySuccessfulSnapshotShard(snapshot, shardId, shardSnapshotResult); + return () -> snapshot(shardId, snapshot, indexId, snapshotStatus, entryVersion, entryStartTime, new ActionListener<>() { + @Override + public void onResponse(ShardSnapshotResult shardSnapshotResult) { + final ShardGeneration newGeneration = shardSnapshotResult.getGeneration(); + assert newGeneration != null; + assert newGeneration.equals(snapshotStatus.generation()); + if (logger.isDebugEnabled()) { + final IndexShardSnapshotStatus.Copy lastSnapshotStatus = snapshotStatus.asCopy(); + logger.debug( + "[{}][{}] completed snapshot to [{}] with status [{}] at generation [{}]", + shardId, + snapshot, + snapshot.getRepository(), + lastSnapshotStatus, + snapshotStatus.generation() + ); } + notifySuccessfulSnapshotShard(snapshot, shardId, shardSnapshotResult); + } - @Override - public void onFailure(Exception e) { - final String failure; - if (e instanceof AbortedSnapshotException) { - failure = "aborted"; - logger.debug(() -> format("[%s][%s] aborted shard snapshot", shardId, snapshot), e); - } else { - failure = summarizeFailure(e); - logger.warn(() -> format("[%s][%s] failed to snapshot shard", shardId, snapshot), e); - } - snapshotStatus.moveToFailed(threadPool.absoluteTimeInMillis(), failure); - notifyFailedSnapshotShard(snapshot, shardId, failure, snapshotStatus.generation()); + @Override + public void onFailure(Exception e) { + final String failure; + if (e instanceof AbortedSnapshotException) { + failure = "aborted"; + logger.debug(() -> format("[%s][%s] aborted shard snapshot", shardId, snapshot), e); + } else { + failure = summarizeFailure(e); + logger.warn(() -> format("[%s][%s] failed to snapshot shard", shardId, snapshot), e); } + snapshotStatus.moveToFailed(threadPool.absoluteTimeInMillis(), failure); + notifyFailedSnapshotShard(snapshot, shardId, failure, snapshotStatus.generation()); } - ); + }); } // package private for testing @@ -359,7 +341,6 @@ private void snapshot( final ShardId shardId, final Snapshot snapshot, final IndexId indexId, - final Map userMetadata, final IndexShardSnapshotStatus snapshotStatus, Version version, final long entryStartTime, @@ -398,7 +379,6 @@ private void snapshot( getShardStateId(indexShard, snapshotRef.getIndexCommit()), snapshotStatus, version, - userMetadata, entryStartTime, listener ) diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java index ddc7d8004595..9d8ddd451290 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java @@ -292,7 +292,6 @@ public void createSnapshot(final CreateSnapshotRequest request, final ActionList featureStatesSet = Collections.emptySet(); } - final Map userMeta = repository.adaptUserMetadata(request.userMetadata()); repository.executeConsistentStateUpdate(repositoryData -> new ClusterStateUpdateTask(request.masterNodeTimeout()) { private SnapshotsInProgress.Entry newEntry; @@ -412,7 +411,7 @@ public ClusterState execute(ClusterState currentState) { threadPool.absoluteTimeInMillis(), repositoryData.getGenId(), shards, - userMeta, + request.userMetadata(), version, List.copyOf(featureStates) ); diff --git a/server/src/test/java/org/elasticsearch/repositories/blobstore/ShardSnapshotTaskRunnerTests.java b/server/src/test/java/org/elasticsearch/repositories/blobstore/ShardSnapshotTaskRunnerTests.java index 5cfd872a7b1b..806a3bf73ab8 100644 --- a/server/src/test/java/org/elasticsearch/repositories/blobstore/ShardSnapshotTaskRunnerTests.java +++ b/server/src/test/java/org/elasticsearch/repositories/blobstore/ShardSnapshotTaskRunnerTests.java @@ -30,7 +30,6 @@ import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; -import java.util.Collections; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -137,7 +136,6 @@ public static SnapshotShardContext dummyContext(final SnapshotId snapshotId, fin null, IndexShardSnapshotStatus.newInitializing(null), Version.CURRENT, - Collections.emptyMap(), startTime, ActionListener.noop() ); diff --git a/server/src/test/java/org/elasticsearch/repositories/fs/FsRepositoryTests.java b/server/src/test/java/org/elasticsearch/repositories/fs/FsRepositoryTests.java index c76b131a4cb1..f6f99035bc34 100644 --- a/server/src/test/java/org/elasticsearch/repositories/fs/FsRepositoryTests.java +++ b/server/src/test/java/org/elasticsearch/repositories/fs/FsRepositoryTests.java @@ -60,7 +60,6 @@ import java.io.IOException; import java.nio.file.Path; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -112,7 +111,6 @@ public void testSnapshotAndRestore() throws IOException { null, snapshotStatus, Version.CURRENT, - Collections.emptyMap(), randomMillisUpToYear9999(), snapshot1Future ) @@ -155,7 +153,6 @@ public void testSnapshotAndRestore() throws IOException { null, snapshotStatus2, Version.CURRENT, - Collections.emptyMap(), randomMillisUpToYear9999(), snapshot2future ) diff --git a/test/framework/src/main/java/org/elasticsearch/index/shard/IndexShardTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/shard/IndexShardTestCase.java index c7ffa47896e8..4a03170b170c 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/shard/IndexShardTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/shard/IndexShardTestCase.java @@ -1051,7 +1051,6 @@ protected ShardGeneration snapshotShard(final IndexShard shard, final Snapshot s null, snapshotStatus, Version.CURRENT, - Collections.emptyMap(), randomMillisUpToYear9999(), future ) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/snapshots/sourceonly/SourceOnlySnapshotRepository.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/snapshots/sourceonly/SourceOnlySnapshotRepository.java index 94e01e79d2d5..c0272fe91237 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/snapshots/sourceonly/SourceOnlySnapshotRepository.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/snapshots/sourceonly/SourceOnlySnapshotRepository.java @@ -209,7 +209,6 @@ protected void closeInternal() { context.stateIdentifier(), context.status(), context.getRepositoryMetaVersion(), - context.userMetadata(), context.snapshotStartTime(), context ) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/sourceonly/SourceOnlySnapshotShardTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/sourceonly/SourceOnlySnapshotShardTests.java index d9c25639d811..450ded1eefd8 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/sourceonly/SourceOnlySnapshotShardTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/sourceonly/SourceOnlySnapshotShardTests.java @@ -133,7 +133,6 @@ public void testSourceIncomplete() throws IOException { null, indexShardSnapshotStatus, Version.CURRENT, - Collections.emptyMap(), randomMillisUpToYear9999(), future ) @@ -176,7 +175,6 @@ public void testIncrementalSnapshot() throws IOException { null, indexShardSnapshotStatus, Version.CURRENT, - Collections.emptyMap(), randomMillisUpToYear9999(), future ) @@ -208,7 +206,6 @@ public void testIncrementalSnapshot() throws IOException { null, indexShardSnapshotStatus, Version.CURRENT, - Collections.emptyMap(), randomMillisUpToYear9999(), future ) @@ -240,7 +237,6 @@ public void testIncrementalSnapshot() throws IOException { null, indexShardSnapshotStatus, Version.CURRENT, - Collections.emptyMap(), randomMillisUpToYear9999(), future ) @@ -302,7 +298,6 @@ public void testRestoreMinmal() throws IOException { null, indexShardSnapshotStatus, Version.CURRENT, - Collections.emptyMap(), randomMillisUpToYear9999(), future ) diff --git a/x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/xpack/searchablesnapshots/store/SearchableSnapshotDirectoryTests.java b/x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/xpack/searchablesnapshots/store/SearchableSnapshotDirectoryTests.java index 612af98a488b..575a922eb370 100644 --- a/x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/xpack/searchablesnapshots/store/SearchableSnapshotDirectoryTests.java +++ b/x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/xpack/searchablesnapshots/store/SearchableSnapshotDirectoryTests.java @@ -122,7 +122,6 @@ import java.util.function.Predicate; import java.util.stream.Collectors; -import static java.util.Collections.emptyMap; import static org.elasticsearch.xpack.searchablesnapshots.SearchableSnapshots.SNAPSHOT_CACHE_ENABLED_SETTING; import static org.elasticsearch.xpack.searchablesnapshots.SearchableSnapshots.SNAPSHOT_CACHE_EXCLUDED_FILE_TYPES_SETTING; import static org.elasticsearch.xpack.searchablesnapshots.SearchableSnapshots.SNAPSHOT_CACHE_PREWARM_ENABLED_SETTING; @@ -631,7 +630,6 @@ protected void assertSnapshotOrGenericThread() { null, snapshotStatus, Version.CURRENT, - emptyMap(), randomMillisUpToYear9999(), future ) From 08803b01d80f9f6742e60fdeb9e27ef367c43c57 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Mon, 21 Nov 2022 10:02:47 +0000 Subject: [PATCH 008/919] Fix NPE when method was called on an array type (#91713) This fixes #87562 --- docs/changelog/91713.yaml | 6 ++++++ .../painless/phase/DefaultSemanticAnalysisPhase.java | 5 ++++- .../elasticsearch/painless/WhenThingsGoWrongTests.java | 8 ++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/91713.yaml diff --git a/docs/changelog/91713.yaml b/docs/changelog/91713.yaml new file mode 100644 index 000000000000..c5fe0b73719b --- /dev/null +++ b/docs/changelog/91713.yaml @@ -0,0 +1,6 @@ +pr: 91713 +summary: Fix NPE when method was called on an array type +area: Infra/Scripting +type: bug +issues: + - 87562 diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultSemanticAnalysisPhase.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultSemanticAnalysisPhase.java index 8c6ac55e8ffe..51302ebe302c 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultSemanticAnalysisPhase.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultSemanticAnalysisPhase.java @@ -14,6 +14,7 @@ import org.elasticsearch.painless.Location; import org.elasticsearch.painless.Operation; import org.elasticsearch.painless.lookup.PainlessCast; +import org.elasticsearch.painless.lookup.PainlessClass; import org.elasticsearch.painless.lookup.PainlessClassBinding; import org.elasticsearch.painless.lookup.PainlessConstructor; import org.elasticsearch.painless.lookup.PainlessField; @@ -3283,7 +3284,9 @@ public void visitCall(ECall userCallNode, SemanticScope semanticScope) { method = lookup.lookupPainlessMethod(type, false, methodName, userArgumentsSize); if (method == null) { - dynamic = lookup.lookupPainlessClass(type).annotations.containsKey(DynamicTypeAnnotation.class) + PainlessClass pc = lookup.lookupPainlessClass(type); + dynamic = pc != null + && pc.annotations.containsKey(DynamicTypeAnnotation.class) && lookup.lookupPainlessSubClassesMethod(type, methodName, userArgumentsSize) != null; if (dynamic == false) { diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/WhenThingsGoWrongTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/WhenThingsGoWrongTests.java index 77c2bb630c3d..7050bccffa06 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/WhenThingsGoWrongTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/WhenThingsGoWrongTests.java @@ -840,4 +840,12 @@ public void testInstanceMethodNotFound() { iae = expectScriptThrows(IllegalArgumentException.class, () -> exec("doesNotExist(1, 'string', false)")); assertEquals(iae.getMessage(), "Unknown call [doesNotExist] with [3] arguments."); } + + public void testArrayToArrayException() { + IllegalArgumentException iae = expectScriptThrows( + IllegalArgumentException.class, + () -> exec("return new String[] {'a'}.noMethod()") + ); + assertTrue(iae.getMessage().contains("member method") && iae.getMessage().contains("not found")); + } } From 5859a38896eb0bdc6d516e3872ab84f9de71f340 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Mon, 21 Nov 2022 11:54:54 +0100 Subject: [PATCH 009/919] Do not merge remote indices by index name (#91703) IndicesPrivileges only support up to one FLS/DLS definition. However, by collating remote index privileges by index expression, a role may end up with multiple such definitions. This complicates the translation steps necessary to retrieve remote access privileges for an authenticated subject. This complexity is unnecessary, since we currently do not take advantage of it. This PR removes logic that collates remote indices privileges by index expression during role building, thus simplifying role building and enabling the implementation for retrieving remote access privileges for an authenticated subject #91734. --- .../permission/RemoteIndicesPermission.java | 5 + .../user/GetUserPrivilegesResponseTests.java | 10 +- ...RoleWithRemoteIndicesPrivilegesRestIT.java | 108 ++++++++++++-- .../authz/store/CompositeRolesStore.java | 136 +++++++----------- .../authz/store/CompositeRolesStoreTests.java | 101 ++++++++++++- 5 files changed, 257 insertions(+), 103 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/RemoteIndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/RemoteIndicesPermission.java index 0a9344f3cc41..2abc93997fa9 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/RemoteIndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/RemoteIndicesPermission.java @@ -51,6 +51,11 @@ public Builder addGroup( final boolean allowRestrictedIndices, final String... indices ) { + assert query == null || query.size() <= 1 : "remote indices groups only support up to one DLS query"; + assert fieldPermissions.getFieldPermissionsDefinitions() + .stream() + .noneMatch(groups -> groups.getFieldGrantExcludeGroups().size() > 1) + : "remote indices groups only support up to one FLS field-grant-exclude group"; remoteIndicesGroups.computeIfAbsent(remoteClusterAliases, k -> new ArrayList<>()) .add( new IndicesPermission.Group( diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesResponseTests.java index 622dc2655255..bbdf8603e7a6 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/GetUserPrivilegesResponseTests.java @@ -198,7 +198,7 @@ private GetUserPrivilegesResponse randomResponse(boolean allowRemoteIndices) { randomArray(3, ConfigurableClusterPrivilege[]::new, () -> new ManageApplicationPrivileges(randomStringSet(3))) ); final Set index = Sets.newHashSet( - randomArray(5, GetUserPrivilegesResponse.Indices[]::new, this::randomIndices) + randomArray(5, GetUserPrivilegesResponse.Indices[]::new, () -> randomIndices(true)) ); final Set application = Sets.newHashSet( randomArray( @@ -217,7 +217,7 @@ private GetUserPrivilegesResponse randomResponse(boolean allowRemoteIndices) { randomArray( 5, GetUserPrivilegesResponse.RemoteIndices[]::new, - () -> new GetUserPrivilegesResponse.RemoteIndices(randomIndices(), randomStringSet(6)) + () -> new GetUserPrivilegesResponse.RemoteIndices(randomIndices(false), randomStringSet(6)) ) ) : Set.of(); @@ -225,13 +225,13 @@ private GetUserPrivilegesResponse randomResponse(boolean allowRemoteIndices) { return new GetUserPrivilegesResponse(cluster, conditionalCluster, index, application, runAs, remoteIndex); } - private GetUserPrivilegesResponse.Indices randomIndices() { + private GetUserPrivilegesResponse.Indices randomIndices(boolean allowMultipleFlsDlsDefinitions) { return new GetUserPrivilegesResponse.Indices( randomStringSet(6), randomStringSet(8), Sets.newHashSet( randomArray( - 3, + allowMultipleFlsDlsDefinitions ? 3 : 1, FieldGrantExcludeGroup[]::new, () -> new FieldGrantExcludeGroup( generateRandomStringArray(3, 5, false, false), @@ -239,7 +239,7 @@ private GetUserPrivilegesResponse.Indices randomIndices() { ) ) ), - randomStringSet(3).stream().map(BytesArray::new).collect(Collectors.toSet()), + randomStringSet(allowMultipleFlsDlsDefinitions ? 3 : 1).stream().map(BytesArray::new).collect(Collectors.toSet()), randomBoolean() ); } diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/role/RoleWithRemoteIndicesPrivilegesRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/role/RoleWithRemoteIndicesPrivilegesRestIT.java index 20a2cf00bc87..d0530929e095 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/role/RoleWithRemoteIndicesPrivilegesRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/role/RoleWithRemoteIndicesPrivilegesRestIT.java @@ -28,8 +28,10 @@ import java.util.List; import java.util.Map; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; public class RoleWithRemoteIndicesPrivilegesRestIT extends SecurityOnTrialLicenseRestTestCase { @@ -55,21 +57,25 @@ public void cleanup() throws IOException { } public void testRemoteIndexPrivileges() throws IOException { - var putRoleRequest = new Request("PUT", "_security/role/" + REMOTE_SEARCH_ROLE); + var putRoleRequest = new Request("PUT", "/_security/role/" + REMOTE_SEARCH_ROLE); putRoleRequest.setJsonEntity(""" { "remote_indices": [ { "names": ["index-a", "*"], "privileges": ["read"], - "clusters": ["remote-a", "*"] + "clusters": ["remote-a", "*"], + "query": "{\\"match\\":{\\"field\\":\\"a\\"}}", + "field_security" : { + "grant": ["field"] + } } ] }"""); final Response putRoleResponse1 = adminClient().performRequest(putRoleRequest); assertOK(putRoleResponse1); - final Response getRoleResponse = adminClient().performRequest(new Request("GET", "_security/role/" + REMOTE_SEARCH_ROLE)); + final Response getRoleResponse = adminClient().performRequest(new Request("GET", "/_security/role/" + REMOTE_SEARCH_ROLE)); assertOK(getRoleResponse); expectRoleDescriptorInResponse( getRoleResponse, @@ -83,7 +89,12 @@ public void testRemoteIndexPrivileges() throws IOException { null, null, new RoleDescriptor.RemoteIndicesPrivileges[] { - RoleDescriptor.RemoteIndicesPrivileges.builder("remote-a", "*").indices("index-a", "*").privileges("read").build() } + RoleDescriptor.RemoteIndicesPrivileges.builder("remote-a", "*") + .indices("index-a", "*") + .query("{\"match\":{\"field\":\"a\"}}") + .privileges("read") + .grantedFields("field") + .build() } ) ); @@ -113,7 +124,11 @@ public void testRemoteIndexPrivileges() throws IOException { { "names": ["index-a", "*"], "privileges": ["read"], - "clusters": ["remote-a", "*"] + "clusters": ["remote-a", "*"], + "query": "{\\"match\\":{\\"field\\":\\"a\\"}}", + "field_security" : { + "grant": ["field"] + } } ] }"""); @@ -146,7 +161,12 @@ public void testRemoteIndexPrivileges() throws IOException { null, null, new RoleDescriptor.RemoteIndicesPrivileges[] { - RoleDescriptor.RemoteIndicesPrivileges.builder("remote-a", "*").indices("index-a", "*").privileges("read").build() } + RoleDescriptor.RemoteIndicesPrivileges.builder("remote-a", "*") + .indices("index-a", "*") + .privileges("read") + .query("{\"match\":{\"field\":\"a\"}}") + .grantedFields("field") + .build() } ) ); } @@ -159,7 +179,11 @@ public void testGetUserPrivileges() throws IOException { { "names": ["index-a", "*"], "privileges": ["read"], - "clusters": ["remote-a", "*"] + "clusters": ["remote-a", "*"], + "query": "{\\"match\\":{\\"field\\":\\"a\\"}}", + "field_security": { + "grant": ["field"] + } } ] }"""); @@ -168,7 +192,7 @@ public void testGetUserPrivileges() throws IOException { final Response getUserPrivilegesResponse1 = executeAsRemoteSearchUser(new Request("GET", "/_security/user/_privileges")); assertOK(getUserPrivilegesResponse1); - assertThat(responseAsMap(getUserPrivilegesResponse1), equalTo(XContentHelper.convertToMap(JsonXContent.jsonXContent, """ + assertThat(responseAsMap(getUserPrivilegesResponse1), equalTo(mapFromJson(""" { "cluster": [], "global": [], @@ -180,10 +204,12 @@ public void testGetUserPrivileges() throws IOException { "names": ["*", "index-a"], "privileges": ["read"], "allow_restricted_indices": false, - "clusters": ["remote-a", "*"] + "clusters": ["remote-a", "*"], + "query": ["{\\"match\\":{\\"field\\":\\"a\\"}}"], + "field_security": [{"grant": ["field"]}] } ] - }""", false))); + }"""))); final var putRoleRequest2 = new Request("PUT", "/_security/role/" + REMOTE_SEARCH_ROLE); putRoleRequest2.setJsonEntity(""" @@ -208,7 +234,7 @@ public void testGetUserPrivileges() throws IOException { final Response getUserPrivilegesResponse2 = executeAsRemoteSearchUser(new Request("GET", "/_security/user/_privileges")); assertOK(getUserPrivilegesResponse2); - assertThat(responseAsMap(getUserPrivilegesResponse2), equalTo(XContentHelper.convertToMap(JsonXContent.jsonXContent, """ + assertThat(responseAsMap(getUserPrivilegesResponse2), equalTo(mapFromJson(""" { "cluster": ["all"], "global": [], @@ -229,7 +255,65 @@ public void testGetUserPrivileges() throws IOException { "clusters": ["remote-a", "*"] } ] - }""", false))); + }"""))); + } + + public void testGetUserPrivilegesWithMultipleFlsDlsDefinitionsPreservesGroupPerIndexPrivilege() throws IOException { + final var putRoleRequest = new Request("PUT", "/_security/role/" + REMOTE_SEARCH_ROLE); + putRoleRequest.setJsonEntity(""" + { + "remote_indices": [ + { + "names": ["index-a", "*"], + "privileges": ["read"], + "clusters": ["remote-a", "*"], + "query": "{\\"match\\":{\\"field-a\\":\\"a\\"}}", + "field_security": { + "grant": ["field-a"] + } + }, + { + "names": ["index-a", "*"], + "privileges": ["read"], + "clusters": ["remote-a", "*"], + "query": "{\\"match\\":{\\"field-b\\":\\"b\\"}}", + "field_security": { + "grant": ["field-b"] + } + } + ] + }"""); + final Response putRoleResponse1 = adminClient().performRequest(putRoleRequest); + assertOK(putRoleResponse1); + + final Response getUserPrivilegesResponse1 = executeAsRemoteSearchUser(new Request("GET", "/_security/user/_privileges")); + assertOK(getUserPrivilegesResponse1); + final Map actual = responseAsMap(getUserPrivilegesResponse1); + // The order of remote indices is not deterministic, so manually extract remote indices from map response to compare ignoring order + @SuppressWarnings("unchecked") + final List rawRemoteIndices = (List) actual.get("remote_indices"); + assertThat(rawRemoteIndices, notNullValue()); + assertThat(rawRemoteIndices, containsInAnyOrder(mapFromJson(""" + { + "names": ["*", "index-a"], + "privileges": ["read"], + "allow_restricted_indices": false, + "clusters": ["remote-a", "*"], + "query": ["{\\"match\\":{\\"field-a\\":\\"a\\"}}"], + "field_security": [{"grant": ["field-a"]}] + }"""), mapFromJson(""" + { + "names": ["*", "index-a"], + "privileges": ["read"], + "allow_restricted_indices": false, + "clusters": ["remote-a", "*"], + "query": ["{\\"match\\":{\\"field-b\\":\\"b\\"}}"], + "field_security": [{"grant": ["field-b"]}] + }"""))); + } + + private static Map mapFromJson(String json) { + return XContentHelper.convertToMap(JsonXContent.jsonXContent, json, false); } private Response executeAsRemoteSearchUser(final Request request) throws IOException { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index 10b8df8410fa..9f4b80fc26ee 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -435,9 +435,7 @@ public static void buildRoleFromDescriptors( final Map, MergeableIndicesPrivilege> indicesPrivilegesMap = new HashMap<>(); final Map, MergeableIndicesPrivilege> restrictedIndicesPrivilegesMap = new HashMap<>(); - // Outer map keyed by remote cluster alias expressions, inner map by index name expressions - final Map, Map, MergeableIndicesPrivilege>> remoteIndicesPrivilegesMap = new HashMap<>(); - final Map, Map, MergeableIndicesPrivilege>> restrictedRemoteIndicesPrivilegesMap = new HashMap<>(); + final Map, Set> remoteIndicesPrivilegesByCluster = new HashMap<>(); // Keyed by application + resource final Map>, Set> applicationPrivilegesMap = new HashMap<>(); @@ -459,13 +457,7 @@ public static void buildRoleFromDescriptors( MergeableIndicesPrivilege.collatePrivilegesByIndices(descriptor.getIndicesPrivileges(), false, indicesPrivilegesMap); if (descriptor.hasRemoteIndicesPrivileges()) { - final RemoteIndicesPrivileges[] remoteIndicesPrivileges = descriptor.getRemoteIndicesPrivileges(); - MergeableIndicesPrivilege.collatePrivilegesByRemoteIndices( - remoteIndicesPrivileges, - true, - restrictedRemoteIndicesPrivilegesMap - ); - MergeableIndicesPrivilege.collatePrivilegesByRemoteIndices(remoteIndicesPrivileges, false, remoteIndicesPrivilegesMap); + groupIndexPrivilegesByCluster(descriptor.getRemoteIndicesPrivileges(), remoteIndicesPrivilegesByCluster); } for (RoleDescriptor.ApplicationResourcePrivileges appPrivilege : descriptor.getApplicationPrivileges()) { @@ -504,27 +496,17 @@ public static void buildRoleFromDescriptors( ) ); - remoteIndicesPrivilegesMap.forEach((clusterAliasKey, remoteIndicesPrivilegesMapForCluster) -> { - remoteIndicesPrivilegesMapForCluster.forEach( - (key, privilege) -> builder.addRemoteGroup( - clusterAliasKey, - fieldPermissionsCache.getFieldPermissions(privilege.fieldPermissionsDefinition), - privilege.query, - IndexPrivilege.get(privilege.privileges), - false, - privilege.indices.toArray(Strings.EMPTY_ARRAY) - ) - ); - }); - restrictedRemoteIndicesPrivilegesMap.forEach((clusterAliasKey, remoteIndicesPrivilegesMapForCluster) -> { - remoteIndicesPrivilegesMapForCluster.forEach( - (key, privilege) -> builder.addRemoteGroup( + remoteIndicesPrivilegesByCluster.forEach((clusterAliasKey, remoteIndicesPrivilegesForCluster) -> { + remoteIndicesPrivilegesForCluster.forEach( + (privilege) -> builder.addRemoteGroup( clusterAliasKey, - fieldPermissionsCache.getFieldPermissions(privilege.fieldPermissionsDefinition), - privilege.query, - IndexPrivilege.get(privilege.privileges), - true, - privilege.indices.toArray(Strings.EMPTY_ARRAY) + fieldPermissionsCache.getFieldPermissions( + new FieldPermissionsDefinition(privilege.getGrantedFields(), privilege.getDeniedFields()) + ), + privilege.getQuery() == null ? null : newHashSet(privilege.getQuery()), + IndexPrivilege.get(newHashSet(Objects.requireNonNull(privilege.getPrivileges()))), + privilege.allowRestrictedIndices(), + newHashSet(Objects.requireNonNull(privilege.getIndices())).toArray(new String[0]) ) ); }); @@ -591,6 +573,25 @@ boolean isValueInNegativeLookupCache(String key) { return negativeLookupCache.get(key) != null; } + private static void groupIndexPrivilegesByCluster( + final RemoteIndicesPrivileges[] remoteIndicesPrivileges, + final Map, Set> remoteIndexPrivilegesByCluster + ) { + assert remoteIndicesPrivileges != null; + // if a remote index privilege is an explicit denial, then we treat it as non-existent to stay consistent with local index + // privileges + final boolean isExplicitDenial = remoteIndicesPrivileges.length == 1 + && "none".equalsIgnoreCase(remoteIndicesPrivileges[0].indicesPrivileges().getPrivileges()[0]); + if (isExplicitDenial) { + return; + } + for (final RemoteIndicesPrivileges remoteIndicesPrivilege : remoteIndicesPrivileges) { + final IndicesPrivileges indicesPrivilege = remoteIndicesPrivilege.indicesPrivileges(); + final Set clusterAliasKey = newHashSet(remoteIndicesPrivilege.remoteClusters()); + remoteIndexPrivilegesByCluster.computeIfAbsent(clusterAliasKey, k -> new HashSet<>()).add(indicesPrivilege); + } + } + /** * A mutable class that can be used to represent the combination of one or more {@link IndicesPrivileges} */ @@ -630,29 +631,6 @@ void merge(MergeableIndicesPrivilege other) { } } - private static void collatePrivilegesByRemoteIndices( - final RemoteIndicesPrivileges[] remoteIndicesPrivileges, - final boolean allowsRestrictedIndices, - final Map, Map, MergeableIndicesPrivilege>> remoteIndicesPrivilegesMap - ) { - assert remoteIndicesPrivileges != null; - // if a remote index privilege is an explicit denial, then we treat it as non-existent to stay consistent with local index - // privileges - final boolean isExplicitDenial = remoteIndicesPrivileges.length == 1 - && "none".equalsIgnoreCase(remoteIndicesPrivileges[0].indicesPrivileges().getPrivileges()[0]); - if (isExplicitDenial) { - return; - } - for (final RemoteIndicesPrivileges remoteIndicesPrivilege : remoteIndicesPrivileges) { - final Set clusterAlias = newHashSet(remoteIndicesPrivilege.remoteClusters()); - collatePrivilegesByIndices( - remoteIndicesPrivilege.indicesPrivileges(), - allowsRestrictedIndices, - remoteIndicesPrivilegesMap.computeIfAbsent(clusterAlias, k -> new HashMap<>()) - ); - } - } - private static void collatePrivilegesByIndices( final IndicesPrivileges[] indicesPrivileges, final boolean allowsRestrictedIndices, @@ -666,41 +644,33 @@ private static void collatePrivilegesByIndices( return; } for (final IndicesPrivileges indicesPrivilege : indicesPrivileges) { - collatePrivilegesByIndices(indicesPrivilege, allowsRestrictedIndices, indicesPrivilegesMap); - } - } - - private static void collatePrivilegesByIndices( - final IndicesPrivileges indicesPrivilege, - final boolean allowsRestrictedIndices, - final Map, MergeableIndicesPrivilege> indicesPrivilegesMap - ) { - if (indicesPrivilege.allowRestrictedIndices() != allowsRestrictedIndices) { - return; - } - final Set key = newHashSet(indicesPrivilege.getIndices()); - indicesPrivilegesMap.compute(key, (k, value) -> { - if (value == null) { - return new MergeableIndicesPrivilege( - indicesPrivilege.getIndices(), - indicesPrivilege.getPrivileges(), - indicesPrivilege.getGrantedFields(), - indicesPrivilege.getDeniedFields(), - indicesPrivilege.getQuery() - ); - } else { - value.merge( - new MergeableIndicesPrivilege( + if (indicesPrivilege.allowRestrictedIndices() != allowsRestrictedIndices) { + continue; + } + final Set key = newHashSet(indicesPrivilege.getIndices()); + indicesPrivilegesMap.compute(key, (k, value) -> { + if (value == null) { + return new MergeableIndicesPrivilege( indicesPrivilege.getIndices(), indicesPrivilege.getPrivileges(), indicesPrivilege.getGrantedFields(), indicesPrivilege.getDeniedFields(), indicesPrivilege.getQuery() - ) - ); - return value; - } - }); + ); + } else { + value.merge( + new MergeableIndicesPrivilege( + indicesPrivilege.getIndices(), + indicesPrivilege.getPrivileges(), + indicesPrivilege.getGrantedFields(), + indicesPrivilege.getDeniedFields(), + indicesPrivilege.getQuery() + ) + ); + return value; + } + }); + } } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index 75e11cf0c6fc..0df955e901ca 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -66,7 +66,9 @@ import org.elasticsearch.xpack.core.security.authz.accesscontrol.DocumentSubsetBitsetCache; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; import org.elasticsearch.xpack.core.security.authz.permission.ClusterPermission; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; import org.elasticsearch.xpack.core.security.authz.permission.IndicesPermission; import org.elasticsearch.xpack.core.security.authz.permission.RemoteIndicesPermission; import org.elasticsearch.xpack.core.security.authz.permission.Role; @@ -1080,6 +1082,76 @@ public void testBuildRoleWithSingleRemoteIndicesDefinition() { assertHasIndexGroupsForClusters(role.remoteIndices(), Set.of(clusterAlias), indexGroup("index-1")); } + public void testBuildRoleWithFlsAndDlsInRemoteIndicesDefinition() { + String clusterAlias = randomFrom("remote-1", "*"); + Role role = buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new RoleDescriptor.RemoteIndicesPrivileges[] { + RoleDescriptor.RemoteIndicesPrivileges.builder(clusterAlias) + .indices("index-1") + .privileges("read") + .query("{\"match\":{\"field\":\"a\"}}") + .grantedFields("field") + .build() } + ) + ); + assertHasRemoteGroupsForClusters(role.remoteIndices(), Set.of(clusterAlias)); + assertHasIndexGroupsForClusters( + role.remoteIndices(), + Set.of(clusterAlias), + indexGroup( + IndexPrivilege.READ, + false, + "{\"match\":{\"field\":\"a\"}}", + new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[] { "field" }, null), + "index-1" + ) + ); + + role = buildRole( + roleDescriptorWithIndicesPrivileges( + "r1", + new RoleDescriptor.RemoteIndicesPrivileges[] { + RoleDescriptor.RemoteIndicesPrivileges.builder(clusterAlias) + .indices("index-1") + .privileges("read") + .query("{\"match\":{\"field\":\"a\"}}") + .grantedFields("field") + .build() } + ), + roleDescriptorWithIndicesPrivileges( + "r1", + new RoleDescriptor.RemoteIndicesPrivileges[] { + RoleDescriptor.RemoteIndicesPrivileges.builder(clusterAlias) + .indices("index-1") + .privileges("read") + .query("{\"match\":{\"field\":\"b\"}}") + .grantedFields("other") + .build() } + ) + ); + assertHasRemoteGroupsForClusters(role.remoteIndices(), Set.of(clusterAlias)); + assertHasIndexGroupsForClusters( + role.remoteIndices(), + Set.of(clusterAlias), + indexGroup( + IndexPrivilege.READ, + false, + "{\"match\":{\"field\":\"a\"}}", + new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[] { "field" }, null), + "index-1" + ), + indexGroup( + IndexPrivilege.READ, + false, + "{\"match\":{\"field\":\"b\"}}", + new FieldPermissionsDefinition.FieldGrantExcludeGroup(new String[] { "other" }, null), + "index-1" + ) + ); + } + public void testBuildRoleWithEmptyOrNoneRemoteIndices() { Role role = buildRole( roleDescriptorWithIndicesPrivileges( @@ -1129,7 +1201,7 @@ public void testBuildRoleWithRemoteIndicesDoesNotMergeWhenNothingToMerge() { assertThat(allowedRead.test(mockIndexAbstraction("foo")), equalTo(false)); } - public void testBuildRoleWithRemoteIndicesMergesRemotesAndLocalsSeparately() { + public void testBuildRoleWithRemoteIndicesDoesNotCombineRemotesAndLocals() { Role role = buildRole( roleDescriptorWithIndicesPrivileges( "r1", @@ -1246,7 +1318,8 @@ public void testBuildRoleWithMultipleRemoteMergedAcrossPrivilegesAndDescriptors( assertHasIndexGroupsForClusters( role.remoteIndices(), Set.of("remote-1"), - indexGroup(IndexPrivilege.get(Set.of("read", "none")), false, "index-1") + indexGroup(IndexPrivilege.get(Set.of("read")), false, "index-1"), + indexGroup(IndexPrivilege.get(Set.of("none")), false, "index-1") ); } @@ -2616,6 +2689,22 @@ private static Matcher indexGroup( final IndexPrivilege privilege, final boolean allowRestrictedIndices, final String... indices + ) { + return indexGroup( + privilege, + allowRestrictedIndices, + null, + new FieldPermissionsDefinition.FieldGrantExcludeGroup(null, null), + indices + ); + } + + private static Matcher indexGroup( + final IndexPrivilege privilege, + final boolean allowRestrictedIndices, + @Nullable final String query, + final FieldPermissionsDefinition.FieldGrantExcludeGroup flsGroup, + final String... indices ) { return new BaseMatcher<>() { @Override @@ -2624,8 +2713,10 @@ public boolean matches(Object o) { return false; } final IndicesPermission.Group group = (IndicesPermission.Group) o; - return equalTo(privilege).matches(group.privilege()) + return equalTo(query == null ? null : Set.of(new BytesArray(query))).matches(group.getQuery()) + && equalTo(privilege).matches(group.privilege()) && equalTo(allowRestrictedIndices).matches(group.allowRestrictedIndices()) + && equalTo(new FieldPermissions(new FieldPermissionsDefinition(Set.of(flsGroup)))).matches(group.getFieldPermissions()) && arrayContaining(indices).matches(group.indices()); } @@ -2639,6 +2730,10 @@ public void describeTo(Description description) { + allowRestrictedIndices + ", indices=" + Strings.arrayToCommaDelimitedString(indices) + + ", query=" + + query + + ", fieldGrantExcludeGroup=" + + flsGroup + '}' ); } From b990e0a04f562296eeab77f5e9df9610b2907107 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 21 Nov 2022 11:40:12 +0000 Subject: [PATCH 010/919] Tidier handling of disabled DB allocator (#91705) Minor followup to #91038 --- .../TransportGetDesiredBalanceAction.java | 14 ++++---------- ...TransportGetDesiredBalanceActionTests.java | 19 ++++++++++++++----- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/allocation/TransportGetDesiredBalanceAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/allocation/TransportGetDesiredBalanceAction.java index e2b40605798e..2b62ddfa8239 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/allocation/TransportGetDesiredBalanceAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/allocation/TransportGetDesiredBalanceAction.java @@ -11,7 +11,6 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.master.TransportMasterNodeReadAction; -import org.elasticsearch.cluster.ClusterModule; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.block.ClusterBlockException; import org.elasticsearch.cluster.block.ClusterBlockLevel; @@ -60,8 +59,8 @@ public TransportGetDesiredBalanceAction( DesiredBalanceResponse::from, ThreadPool.Names.MANAGEMENT ); - this.desiredBalanceShardsAllocator = shardsAllocator instanceof DesiredBalanceShardsAllocator - ? (DesiredBalanceShardsAllocator) shardsAllocator + this.desiredBalanceShardsAllocator = shardsAllocator instanceof DesiredBalanceShardsAllocator desiredBalanceShardsAllocator + ? desiredBalanceShardsAllocator : null; } @@ -72,13 +71,8 @@ protected void masterOperation( ClusterState state, ActionListener listener ) throws Exception { - String allocatorName = ClusterModule.SHARDS_ALLOCATOR_TYPE_SETTING.get(state.metadata().settings()); - if (allocatorName.equals(ClusterModule.DESIRED_BALANCE_ALLOCATOR) == false || desiredBalanceShardsAllocator == null) { - listener.onFailure( - new ResourceNotFoundException( - "Expected the shard balance allocator to be `desired_balance`, but got `" + allocatorName + "`" - ) - ); + if (desiredBalanceShardsAllocator == null) { + listener.onFailure(new ResourceNotFoundException("Desired balance allocator is not in use, no desired balance found")); return; } diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/allocation/TransportGetDesiredBalanceActionTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/allocation/TransportGetDesiredBalanceActionTests.java index d29535aaf1f8..e0d343e5ee79 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/allocation/TransportGetDesiredBalanceActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/allocation/TransportGetDesiredBalanceActionTests.java @@ -23,11 +23,13 @@ import org.elasticsearch.cluster.routing.allocation.allocator.DesiredBalanceShardsAllocator; import org.elasticsearch.cluster.routing.allocation.allocator.DesiredBalanceStats; import org.elasticsearch.cluster.routing.allocation.allocator.ShardAssignment; +import org.elasticsearch.cluster.routing.allocation.allocator.ShardsAllocator; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.Index; import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.rest.RestStatus; import org.elasticsearch.tasks.Task; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; @@ -42,6 +44,7 @@ import java.util.Set; import java.util.stream.Collectors; +import static org.hamcrest.Matchers.equalTo; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -70,15 +73,21 @@ public void setUpMocks() throws Exception { public void testReturnsErrorIfAllocatorIsNotDesiredBalanced() throws Exception { when(metadata.settings()).thenReturn(Settings.builder().put("cluster.routing.allocation.type", "balanced").build()); - transportGetDesiredBalanceAction.masterOperation(mock(Task.class), mock(DesiredBalanceRequest.class), clusterState, listener); + new TransportGetDesiredBalanceAction( + mock(TransportService.class), + mock(ClusterService.class), + mock(ThreadPool.class), + mock(ActionFilters.class), + mock(IndexNameExpressionResolver.class), + mock(ShardsAllocator.class) + ).masterOperation(mock(Task.class), mock(DesiredBalanceRequest.class), clusterState, listener); ArgumentCaptor exceptionArgumentCaptor = ArgumentCaptor.forClass(ResourceNotFoundException.class); verify(listener).onFailure(exceptionArgumentCaptor.capture()); - assertEquals( - "Expected the shard balance allocator to be `desired_balance`, but got `balanced`", - exceptionArgumentCaptor.getValue().getMessage() - ); + final var exception = exceptionArgumentCaptor.getValue(); + assertEquals("Desired balance allocator is not in use, no desired balance found", exception.getMessage()); + assertThat(exception.status(), equalTo(RestStatus.NOT_FOUND)); } public void testReturnsErrorIfDesiredBalanceIsNotAvailable() throws Exception { From 34c30ad7becbdc7abec42a053a2b99008ce9f44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Krze=C5=9Bniak?= Date: Mon, 21 Nov 2022 13:13:44 +0100 Subject: [PATCH 011/919] [DOCS] typo in date_histogram aggregation example (#91715) * [DOCS] typo in date_histogram aggregation example The field name fixed * Update docs/reference/aggregations/bucket/datehistogram-aggregation.asciidoc Co-authored-by: Abdon Pijpelink --- .../aggregations/bucket/datehistogram-aggregation.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/aggregations/bucket/datehistogram-aggregation.asciidoc b/docs/reference/aggregations/bucket/datehistogram-aggregation.asciidoc index 405aa6863111..59cf166ea693 100644 --- a/docs/reference/aggregations/bucket/datehistogram-aggregation.asciidoc +++ b/docs/reference/aggregations/bucket/datehistogram-aggregation.asciidoc @@ -705,7 +705,7 @@ POST /sales/_search?size=0 -------------------------------------------------- // TEST[setup:sales] -<1> Documents without a value in the `publish_date` field will fall into the +<1> Documents without a value in the `date` field will fall into the same bucket as documents that have the value `2000-01-01`. [[date-histogram-order]] From 12037505202b3cdd59ce7d6233cbcef074838dda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Fern=C3=A1ndez=20Casta=C3=B1o?= Date: Mon, 21 Nov 2022 13:21:01 +0100 Subject: [PATCH 012/919] Add more debug info to SnapshotBasedRecoveryIT (#91745) Relates #91383 --- .../upgrades/SnapshotBasedRecoveryIT.java | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/SnapshotBasedRecoveryIT.java b/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/SnapshotBasedRecoveryIT.java index ba61b703f12a..5997220f10b8 100644 --- a/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/SnapshotBasedRecoveryIT.java +++ b/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/SnapshotBasedRecoveryIT.java @@ -102,7 +102,12 @@ public void testSnapshotBasedRecovery() throws Exception { // Drop replicas updateIndexSettings(indexName, Settings.builder().put(IndexMetadata.INDEX_NUMBER_OF_REPLICAS_SETTING.getKey(), 0)); updateIndexSettings(indexName, Settings.builder().put(IndexMetadata.INDEX_NUMBER_OF_REPLICAS_SETTING.getKey(), 1)); - ensureGreen(indexName); + try { + ensureGreen(indexName); + } catch (AssertionError e) { + logAllocationExplain(); + throw e; + } assertMatchAllReturnsAllDocuments(indexName, numDocs); assertMatchQueryReturnsAllDocuments(indexName, numDocs); } @@ -110,6 +115,20 @@ public void testSnapshotBasedRecovery() throws Exception { } } + private void logAllocationExplain() throws Exception { + // Used to debug #91383 + var request = new Request(HttpGet.METHOD_NAME, "_cluster/allocation/explain?include_disk_info=true&include_yes_decisions=true"); + request.setJsonEntity(""" + { + "index": "snapshot_based_recovery", + "shard": 0, + "primary": false + } + """); + var response = client().performRequest(request); + logger.info("--> allocation explain {}", EntityUtils.toString(response.getEntity())); + } + private List getUpgradedNodeIds() throws IOException { Request request = new Request(HttpGet.METHOD_NAME, "_nodes/_all"); Response response = client().performRequest(request); From ed9ded042ecfa0c6c9428f8e97c110d21bc11a57 Mon Sep 17 00:00:00 2001 From: Rene Groeschke Date: Mon, 21 Nov 2022 13:21:34 +0100 Subject: [PATCH 013/919] Update spotless gradle plugin (#90945) --- gradle/build.versions.toml | 2 +- gradle/verification-metadata.xml | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/gradle/build.versions.toml b/gradle/build.versions.toml index ad0509058706..d76e81d2c2fd 100644 --- a/gradle/build.versions.toml +++ b/gradle/build.versions.toml @@ -39,6 +39,6 @@ shadow-plugin = "gradle.plugin.com.github.johnrengelman:shadow:7.1.2" spock-core = { group = "org.spockframework", name="spock-core", version.ref="spock" } spock-junit4 = { group = "org.spockframework", name="spock-junit4", version.ref="spock" } spock-platform = { group = "org.spockframework", name="spock-bom", version.ref="spock" } -spotless-plugin = "com.diffplug.spotless:spotless-plugin-gradle:6.7.2" +spotless-plugin = "com.diffplug.spotless:spotless-plugin-gradle:6.11.0" wiremock = "com.github.tomakehurst:wiremock-jre8-standalone:2.23.2" xmlunit-core = "org.xmlunit:xmlunit-core:2.8.2" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 2eb1bfda3018..8316f1a98e08 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -184,19 +184,19 @@ - - - + + + - - + + - - - + + + @@ -664,9 +664,9 @@ - - - + + + @@ -3049,9 +3049,9 @@ - - - + + + From bb3698cfee7c0a3e74d9bca09214df9aeb1e4a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Campinas?= Date: Mon, 21 Nov 2022 13:31:43 +0100 Subject: [PATCH 014/919] Fix NPE in IndexService#getNodeMappingStats (#91334) The field mapperService in IndexService can be null, which needs to be handled in getNodeMappingStats. It returns a null in that case, which is already handled by the caller CommonStats. --- docs/changelog/91334.yaml | 6 ++++++ .../java/org/elasticsearch/index/IndexService.java | 4 +++- .../org/elasticsearch/index/IndexServiceTests.java | 13 +++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/91334.yaml diff --git a/docs/changelog/91334.yaml b/docs/changelog/91334.yaml new file mode 100644 index 000000000000..5669524db61a --- /dev/null +++ b/docs/changelog/91334.yaml @@ -0,0 +1,6 @@ +pr: 91334 +summary: "Fix NPE in IndexService getNodeMappingStats" +area: "Stats" +type: bug +issues: + - 91259 diff --git a/server/src/main/java/org/elasticsearch/index/IndexService.java b/server/src/main/java/org/elasticsearch/index/IndexService.java index a107efaa9764..53624459be47 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexService.java +++ b/server/src/main/java/org/elasticsearch/index/IndexService.java @@ -300,8 +300,10 @@ public IndexShard getShard(int shardId) { } public NodeMappingStats getNodeMappingStats() { + if (mapperService == null) { + return null; + } long totalCount = mapperService().mappingLookup().getTotalFieldsCount(); - Index index = index(); long totalEstimatedOverhead = totalCount * 1024L; // 1KiB estimated per mapping NodeMappingStats indexNodeMappingStats = new NodeMappingStats(totalCount, totalEstimatedOverhead); return indexNodeMappingStats; diff --git a/server/src/test/java/org/elasticsearch/index/IndexServiceTests.java b/server/src/test/java/org/elasticsearch/index/IndexServiceTests.java index b6604c0c9302..30891c76b369 100644 --- a/server/src/test/java/org/elasticsearch/index/IndexServiceTests.java +++ b/server/src/test/java/org/elasticsearch/index/IndexServiceTests.java @@ -57,6 +57,19 @@ public static CompressedXContent filter(QueryBuilder filterBuilder) throws IOExc return new CompressedXContent(Strings.toString(builder)); } + public void testClosedIndexHasNullNodeMappingStats() throws Exception { + final IndexService indexService = createIndex("test", Settings.EMPTY); + ensureGreen("test"); + + final Index index = indexService.index(); + assertAcked(client().admin().indices().prepareClose(index.getName())); + assertBusy(() -> assertTrue("Index not found: " + index.getName(), getInstanceFromNode(IndicesService.class).hasIndex(index))); + + final IndexService closedIndexService = getInstanceFromNode(IndicesService.class).indexServiceSafe(index); + assertNotSame(indexService, closedIndexService); + assertNull(closedIndexService.getNodeMappingStats()); + } + public void testBaseAsyncTask() throws Exception { IndexService indexService = createIndex("test", Settings.EMPTY); AtomicReference latch = new AtomicReference<>(new CountDownLatch(1)); From 7e9ce33e8483825c56c62c4f095416c3e4a32d28 Mon Sep 17 00:00:00 2001 From: Olivier Cavadenti Date: Mon, 21 Nov 2022 13:32:48 +0100 Subject: [PATCH 015/919] Instrumenting Weight#count in ProfileWeight (#85656) Start instrumenting Weight#count function in ProfileWeight because we start to use it to compute total hit counts and aggregation counts. Resolve #85203 --- docs/changelog/85656.yaml | 6 +++++ docs/reference/search/profile.asciidoc | 24 +++++++++++++++---- .../search/profile/query/ProfileWeight.java | 8 ++++++- .../search/profile/query/QueryTimingType.java | 1 + .../profile/query/QueryProfilerTests.java | 6 +++++ 5 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 docs/changelog/85656.yaml diff --git a/docs/changelog/85656.yaml b/docs/changelog/85656.yaml new file mode 100644 index 000000000000..3ca31b32c6f6 --- /dev/null +++ b/docs/changelog/85656.yaml @@ -0,0 +1,6 @@ +pr: 85656 +summary: Instrument Weight#count in ProfileWeight +area: Search +type: enhancement +issues: + - 85203 diff --git a/docs/reference/search/profile.asciidoc b/docs/reference/search/profile.asciidoc index 30a554ec0b60..f56c45c57aab 100644 --- a/docs/reference/search/profile.asciidoc +++ b/docs/reference/search/profile.asciidoc @@ -97,7 +97,9 @@ The API returns the following result: "create_weight": 4694895, "shallow_advance": 0, "create_weight_count": 1, - "build_scorer": 7112295 + "build_scorer": 7112295, + "count_weight": 0, + "count_weight_count": 0 }, "children": [ { @@ -122,7 +124,9 @@ The API returns the following result: "create_weight": 2382719, "shallow_advance": 9754, "create_weight_count": 1, - "build_scorer": 1355007 + "build_scorer": 1355007, + "count_weight": 0, + "count_weight_count": 0 } }, { @@ -147,7 +151,9 @@ The API returns the following result: "create_weight": 130951, "shallow_advance": 2512, "create_weight_count": 1, - "build_scorer": 46153 + "build_scorer": 46153, + "count_weight": 0, + "count_weight_count": 0 } } ] @@ -386,7 +392,9 @@ Lucene execution: "create_weight": 4694895, "shallow_advance": 0, "create_weight_count": 1, - "build_scorer": 7112295 + "build_scorer": 7112295, + "count_weight": 0, + "count_weight_count": 0 } -------------------------------------------------- // TESTRESPONSE[s/^/{\n"took": $body.took,\n"timed_out": $body.timed_out,\n"_shards": $body._shards,\n"hits": $body.hits,\n"profile": {\n"shards": [ {\n"id": "$body.$_path",\n"searches": [{\n"query": [{\n"type": "BooleanQuery",\n"description": "message:get message:search",\n"time_in_nanos": $body.$_path,/] @@ -653,10 +661,12 @@ The API returns the following result: "compute_max_score": 0, "advance": 3942, "advance_count": 4, + "count_weight_count": 0, "score": 0, "build_scorer_count": 2, "create_weight": 38380, "shallow_advance": 0, + "count_weight": 0, "create_weight_count": 1, "build_scorer": 99296 } @@ -679,9 +689,11 @@ The API returns the following result: "advance": 3552, "advance_count": 1, "score": 5027, + "count_weight_count": 0, "build_scorer_count": 2, "create_weight": 107840, "shallow_advance": 0, + "count_weight": 0, "create_weight_count": 1, "build_scorer": 44215 } @@ -1260,7 +1272,9 @@ One of the `dfs.knn` sections for a shard looks like the following: "create_weight" : 128879, "shallow_advance" : 0, "create_weight_count" : 1, - "build_scorer" : 307595 + "build_scorer" : 307595, + "count_weight": 0, + "count_weight_count": 0 } } ], diff --git a/server/src/main/java/org/elasticsearch/search/profile/query/ProfileWeight.java b/server/src/main/java/org/elasticsearch/search/profile/query/ProfileWeight.java index 2114f7321ad7..21f983e0c756 100644 --- a/server/src/main/java/org/elasticsearch/search/profile/query/ProfileWeight.java +++ b/server/src/main/java/org/elasticsearch/search/profile/query/ProfileWeight.java @@ -102,7 +102,13 @@ public Explanation explain(LeafReaderContext context, int doc) throws IOExceptio @Override public int count(LeafReaderContext context) throws IOException { - return subQueryWeight.count(context); + Timer timer = profile.getTimer(QueryTimingType.COUNT_WEIGHT); + timer.start(); + try { + return subQueryWeight.count(context); + } finally { + timer.stop(); + } } @Override diff --git a/server/src/main/java/org/elasticsearch/search/profile/query/QueryTimingType.java b/server/src/main/java/org/elasticsearch/search/profile/query/QueryTimingType.java index fc8ae88e27ff..03444a8b52c6 100644 --- a/server/src/main/java/org/elasticsearch/search/profile/query/QueryTimingType.java +++ b/server/src/main/java/org/elasticsearch/search/profile/query/QueryTimingType.java @@ -12,6 +12,7 @@ public enum QueryTimingType { CREATE_WEIGHT, + COUNT_WEIGHT, BUILD_SCORER, NEXT_DOC, ADVANCE, diff --git a/server/src/test/java/org/elasticsearch/search/profile/query/QueryProfilerTests.java b/server/src/test/java/org/elasticsearch/search/profile/query/QueryProfilerTests.java index c52a67ed29bf..65e3195ec44f 100644 --- a/server/src/test/java/org/elasticsearch/search/profile/query/QueryProfilerTests.java +++ b/server/src/test/java/org/elasticsearch/search/profile/query/QueryProfilerTests.java @@ -106,6 +106,7 @@ public void testBasic() throws IOException { assertEquals(1, results.size()); Map breakdown = results.get(0).getTimeBreakdown(); assertThat(breakdown.get(QueryTimingType.CREATE_WEIGHT.toString()), greaterThan(0L)); + assertThat(breakdown.get(QueryTimingType.COUNT_WEIGHT.toString()), equalTo(0L)); assertThat(breakdown.get(QueryTimingType.BUILD_SCORER.toString()), greaterThan(0L)); assertThat(breakdown.get(QueryTimingType.NEXT_DOC.toString()), greaterThan(0L)); assertThat(breakdown.get(QueryTimingType.ADVANCE.toString()), equalTo(0L)); @@ -113,6 +114,7 @@ public void testBasic() throws IOException { assertThat(breakdown.get(QueryTimingType.MATCH.toString()), equalTo(0L)); assertThat(breakdown.get(QueryTimingType.CREATE_WEIGHT.toString() + "_count"), greaterThan(0L)); + assertThat(breakdown.get(QueryTimingType.COUNT_WEIGHT.toString() + "_count"), equalTo(0L)); assertThat(breakdown.get(QueryTimingType.BUILD_SCORER.toString() + "_count"), greaterThan(0L)); assertThat(breakdown.get(QueryTimingType.NEXT_DOC.toString() + "_count"), greaterThan(0L)); assertThat(breakdown.get(QueryTimingType.ADVANCE.toString() + "_count"), equalTo(0L)); @@ -149,6 +151,7 @@ public void testNoScoring() throws IOException { assertEquals(1, results.size()); Map breakdown = results.get(0).getTimeBreakdown(); assertThat(breakdown.get(QueryTimingType.CREATE_WEIGHT.toString()), greaterThan(0L)); + assertThat(breakdown.get(QueryTimingType.COUNT_WEIGHT.toString()), equalTo(0L)); assertThat(breakdown.get(QueryTimingType.BUILD_SCORER.toString()), greaterThan(0L)); assertThat(breakdown.get(QueryTimingType.NEXT_DOC.toString()), greaterThan(0L)); assertThat(breakdown.get(QueryTimingType.ADVANCE.toString()), equalTo(0L)); @@ -156,6 +159,7 @@ public void testNoScoring() throws IOException { assertThat(breakdown.get(QueryTimingType.MATCH.toString()), equalTo(0L)); assertThat(breakdown.get(QueryTimingType.CREATE_WEIGHT.toString() + "_count"), greaterThan(0L)); + assertThat(breakdown.get(QueryTimingType.COUNT_WEIGHT.toString() + "_count"), equalTo(0L)); assertThat(breakdown.get(QueryTimingType.BUILD_SCORER.toString() + "_count"), greaterThan(0L)); assertThat(breakdown.get(QueryTimingType.NEXT_DOC.toString() + "_count"), greaterThan(0L)); assertThat(breakdown.get(QueryTimingType.ADVANCE.toString() + "_count"), equalTo(0L)); @@ -189,6 +193,7 @@ public void testApproximations() throws IOException { assertEquals(1, results.size()); Map breakdown = results.get(0).getTimeBreakdown(); assertThat(breakdown.get(QueryTimingType.CREATE_WEIGHT.toString()), greaterThan(0L)); + assertThat(breakdown.get(QueryTimingType.COUNT_WEIGHT.toString()), greaterThan(0L)); assertThat(breakdown.get(QueryTimingType.BUILD_SCORER.toString()), greaterThan(0L)); assertThat(breakdown.get(QueryTimingType.NEXT_DOC.toString()), greaterThan(0L)); assertThat(breakdown.get(QueryTimingType.ADVANCE.toString()), equalTo(0L)); @@ -196,6 +201,7 @@ public void testApproximations() throws IOException { assertThat(breakdown.get(QueryTimingType.MATCH.toString()), greaterThan(0L)); assertThat(breakdown.get(QueryTimingType.CREATE_WEIGHT.toString() + "_count"), greaterThan(0L)); + assertThat(breakdown.get(QueryTimingType.COUNT_WEIGHT.toString() + "_count"), greaterThan(0L)); assertThat(breakdown.get(QueryTimingType.BUILD_SCORER.toString() + "_count"), greaterThan(0L)); assertThat(breakdown.get(QueryTimingType.NEXT_DOC.toString() + "_count"), greaterThan(0L)); assertThat(breakdown.get(QueryTimingType.ADVANCE.toString() + "_count"), equalTo(0L)); From cf0b1af418cad79dfa9c8193c3be51e3c644d8d9 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 21 Nov 2022 12:47:57 +0000 Subject: [PATCH 016/919] Respect gateway allocations in DB computer (#91690) If one of the gateway allocators initializes a shard somewhere then the spawned desired balance computation should start from a state with that shard initializing. Today it doesn't, but this commit fixes that. Closes #91451 Closes #91613 --- .../gateway/ReplicaShardAllocatorIT.java | 1 - .../ReplicaShardAllocatorSyncIdIT.java | 1 - .../routing/allocation/RoutingAllocation.java | 12 ++++- .../DesiredBalanceShardsAllocator.java | 5 ++ .../allocation/FailedShardsRoutingTests.java | 6 +++ .../DesiredBalanceComputerTests.java | 46 ++++++++++++++++++ .../DesiredBalanceShardsAllocatorTests.java | 48 ++++++++++++------- 7 files changed, 99 insertions(+), 20 deletions(-) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/gateway/ReplicaShardAllocatorIT.java b/server/src/internalClusterTest/java/org/elasticsearch/gateway/ReplicaShardAllocatorIT.java index b04425f75df0..459b5cfeb491 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/gateway/ReplicaShardAllocatorIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/gateway/ReplicaShardAllocatorIT.java @@ -244,7 +244,6 @@ public void testRecentPrimaryInformation() throws Exception { transportServiceOnPrimary.clearAllRules(); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/91613") public void testFullClusterRestartPerformNoopRecovery() throws Exception { int numOfReplicas = randomIntBetween(1, 2); internalCluster().ensureAtLeastNumDataNodes(numOfReplicas + 2); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/gateway/ReplicaShardAllocatorSyncIdIT.java b/server/src/internalClusterTest/java/org/elasticsearch/gateway/ReplicaShardAllocatorSyncIdIT.java index cdb09dc31054..1008eabd6cc9 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/gateway/ReplicaShardAllocatorSyncIdIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/gateway/ReplicaShardAllocatorSyncIdIT.java @@ -225,7 +225,6 @@ public void testPreferCopyCanPerformNoopRecovery() throws Exception { transportServiceOnPrimary.clearAllRules(); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/91451") public void testFullClusterRestartPerformNoopRecovery() throws Exception { int numOfReplicas = randomIntBetween(1, 2); internalCluster().ensureAtLeastNumDataNodes(numOfReplicas + 2); diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/RoutingAllocation.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/RoutingAllocation.java index ecc6fb295fea..7846cbacb90d 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/RoutingAllocation.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/RoutingAllocation.java @@ -407,7 +407,17 @@ public void setSimulatedClusterInfo(ClusterInfo clusterInfo) { } public RoutingAllocation immutableClone() { - return new RoutingAllocation(deciders, clusterState, clusterInfo, shardSizeInfo, currentNanoTime); + return new RoutingAllocation( + deciders, + routingNodesChanged() + ? ClusterState.builder(clusterState) + .routingTable(RoutingTable.of(clusterState.routingTable().version(), routingNodes)) + .build() + : clusterState, + clusterInfo, + shardSizeInfo, + currentNanoTime + ); } public RoutingAllocation mutableCloneForSimulation() { diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceShardsAllocator.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceShardsAllocator.java index 652fe0c468ec..1e1495e0aada 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceShardsAllocator.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceShardsAllocator.java @@ -251,6 +251,11 @@ public void onFailure(Exception e) { assert MasterService.isPublishFailureException(e) : e; onNoLongerMaster(); } + + @Override + public String toString() { + return "ReconcileDesiredBalanceTask[lastConvergedIndex=" + desiredBalance.lastConvergedIndex() + "]"; + } } private final class ReconcileDesiredBalanceExecutor implements ClusterStateTaskExecutor { diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/FailedShardsRoutingTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/FailedShardsRoutingTests.java index 7f814c4dd866..86ed2badab0e 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/FailedShardsRoutingTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/FailedShardsRoutingTests.java @@ -151,6 +151,12 @@ public void testFailedShardPrimaryRelocatingToAndFrom() { // check promotion of replica to primary assertThat(clusterState.getRoutingNodes().node(origReplicaNodeId).iterator().next().state(), equalTo(STARTED)); assertThat(clusterState.routingTable().index("test").shard(0).primaryShard().currentNodeId(), equalTo(origReplicaNodeId)); + + if (clusterState.routingTable().index("test").shard(0).replicaShards().get(0).assignedToNode() == false) { + // desired node may be ignored due to previous failure - try again if so + clusterState = startShardsAndReroute(allocation, clusterState); + } + assertThat( clusterState.routingTable().index("test").shard(0).replicaShards().get(0).currentNodeId(), anyOf(equalTo(origPrimaryNodeId), equalTo("node3")) diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceComputerTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceComputerTests.java index 123ca20b1567..c66fdf2ad91b 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceComputerTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceComputerTests.java @@ -327,6 +327,52 @@ public void testRespectsAssignmentOfUnknownReplicas() { ); } + public void testRespectsAssignmentByGatewayAllocators() { + var desiredBalanceComputer = createDesiredBalanceComputer(); + var clusterState = createInitialClusterState(3); + var index = clusterState.metadata().index(TEST_INDEX).getIndex(); + + final var routingAllocation = new RoutingAllocation( + new AllocationDeciders(List.of()), + clusterState.mutableRoutingNodes(), + clusterState, + ClusterInfo.EMPTY, + SnapshotShardSizeInfo.EMPTY, + 0L + ); + for (var iterator = routingAllocation.routingNodes().unassigned().iterator(); iterator.hasNext();) { + var shardRouting = iterator.next(); + if (shardRouting.shardId().id() == 0 && shardRouting.primary()) { + routingAllocation.routingNodes() + .startShard( + logger, + iterator.initialize("node-2", null, 0L, routingAllocation.changes()), + routingAllocation.changes(), + 0L + ); + break; + } + } + + var desiredBalance = desiredBalanceComputer.compute( + DesiredBalance.INITIAL, + DesiredBalanceInput.create(randomNonNegativeLong(), routingAllocation), + queue(), + input -> true + ); + + assertDesiredAssignments( + desiredBalance, + Map.of( + new ShardId(index, 0), + new ShardAssignment(Set.of("node-2", "node-1"), 2, 0, 0), + new ShardId(index, 1), + new ShardAssignment(Set.of("node-0", "node-1"), 2, 0, 0) + ) + ); + + } + public void testSimulatesAchievingDesiredBalanceBeforeDelegating() { var allocateCalled = new AtomicBoolean(); diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceShardsAllocatorTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceShardsAllocatorTests.java index e41878f7107a..d854a3cf6def 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceShardsAllocatorTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceShardsAllocatorTests.java @@ -59,18 +59,18 @@ import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_NUMBER_OF_REPLICAS; import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_NUMBER_OF_SHARDS; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; public class DesiredBalanceShardsAllocatorTests extends ESTestCase { + private static final String LOCAL_NODE_ID = "node-1"; + private static final String OTHER_NODE_ID = "node-2"; + public void testGatewayAllocatorPreemptsAllocation() { + final var nodeId = randomFrom(LOCAL_NODE_ID, OTHER_NODE_ID); testAllocate( - (allocation, unassignedAllocationHandler) -> unassignedAllocationHandler.initialize( - allocation.nodes().getLocalNodeId(), - null, - 0L, - allocation.changes() - ), - routingTable -> assertTrue(routingTable.index("test-index").shard(0).primaryShard().assignedToNode()) + (allocation, unassignedAllocationHandler) -> unassignedAllocationHandler.initialize(nodeId, null, 0L, allocation.changes()), + routingTable -> assertEquals(nodeId, routingTable.index("test-index").shard(0).primaryShard().currentNodeId()) ); } @@ -106,9 +106,10 @@ public void testAllocate( var deterministicTaskQueue = new DeterministicTaskQueue(); var threadPool = deterministicTaskQueue.getThreadPool(); - var localNode = createDiscoveryNode("node-1"); + var localNode = createDiscoveryNode(LOCAL_NODE_ID); + var otherNode = createDiscoveryNode(OTHER_NODE_ID); var initialState = ClusterState.builder(new ClusterName(ClusterServiceUtils.class.getSimpleName())) - .nodes(DiscoveryNodes.builder().add(localNode).localNodeId(localNode.getId()).masterNodeId(localNode.getId())) + .nodes(DiscoveryNodes.builder().add(localNode).add(otherNode).localNodeId(localNode.getId()).masterNodeId(localNode.getId())) .blocks(ClusterBlocks.EMPTY_CLUSTER_BLOCK) .build(); @@ -116,8 +117,8 @@ public void testAllocate( var clusterService = new ClusterService( Settings.EMPTY, clusterSettings, - new FakeThreadPoolMasterService("node-1", "test", threadPool, deterministicTaskQueue::scheduleNow), - new ClusterApplierService("node-1", Settings.EMPTY, clusterSettings, threadPool) { + new FakeThreadPoolMasterService(LOCAL_NODE_ID, "test", threadPool, deterministicTaskQueue::scheduleNow), + new ClusterApplierService(LOCAL_NODE_ID, Settings.EMPTY, clusterSettings, threadPool) { @Override protected PrioritizedEsThreadPoolExecutor createThreadPoolExecutor() { return deterministicTaskQueue.getPrioritizedEsThreadPoolExecutor(); @@ -139,10 +140,13 @@ public ClusterState apply(ClusterState clusterState, Consumer } }; - var allocationService = createAllocationService( - new DesiredBalanceShardsAllocator(createShardsAllocator(), threadPool, clusterService, reconcileAction), - createGatewayAllocator(allocateUnassigned) + final var desiredBalanceShardsAllocator = new DesiredBalanceShardsAllocator( + createShardsAllocator(), + threadPool, + clusterService, + reconcileAction ); + var allocationService = createAllocationService(desiredBalanceShardsAllocator, createGatewayAllocator(allocateUnassigned)); allocationServiceRef.set(allocationService); var listenerCalled = new AtomicBoolean(false); @@ -173,7 +177,17 @@ public void onFailure(Exception e) { try { assertTrue(listenerCalled.get()); - verifier.accept(clusterService.state().routingTable()); + final var routingTable = clusterService.state().routingTable(); + verifier.accept(routingTable); + final var desiredBalance = desiredBalanceShardsAllocator.getDesiredBalance(); + for (final var indexRoutingTable : routingTable) { + for (int shardId = 0; shardId < indexRoutingTable.size(); shardId++) { + final var shardRoutingTable = indexRoutingTable.shard(shardId); + for (final var assignedShard : shardRoutingTable.assignedShards()) { + assertThat(desiredBalance.getAssignment(assignedShard.shardId()).nodeIds(), hasItem(assignedShard.currentNodeId())); + } + } + } } finally { clusterService.close(); } @@ -283,8 +297,8 @@ public void testFailListenersOnNoLongerMasterException() throws InterruptedExcep var newMasterElected = new CountDownLatch(1); var clusterStateUpdatesExecuted = new CountDownLatch(1); - var node1 = createDiscoveryNode("node-1"); - var node2 = createDiscoveryNode("node-2"); + var node1 = createDiscoveryNode(LOCAL_NODE_ID); + var node2 = createDiscoveryNode(OTHER_NODE_ID); var initial = ClusterState.builder(ClusterName.DEFAULT) .nodes(DiscoveryNodes.builder().add(node1).add(node2).localNodeId(node1.getId()).masterNodeId(node1.getId())) .build(); From 7249e4bd9336be8525469a0a4eb92eb6848b19d8 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Mon, 21 Nov 2022 09:54:34 -0500 Subject: [PATCH 017/919] Revert "Revert "Update tech preview notice for synthetic source (#91474)" (#91589)" (#91669) This reverts commit ddef28bd2f4bf8e896c87eb6897174c9c4b03ca6. --- .../reference/mapping/fields/synthetic-source.asciidoc | 9 ++++++++- .../mapping/types/aggregate-metric-double.asciidoc | 10 +++++++++- docs/reference/mapping/types/boolean.asciidoc | 10 +++++++++- docs/reference/mapping/types/date.asciidoc | 10 +++++++++- docs/reference/mapping/types/date_nanos.asciidoc | 10 +++++++++- docs/reference/mapping/types/dense-vector.asciidoc | 10 +++++++++- docs/reference/mapping/types/geo-point.asciidoc | 10 +++++++++- docs/reference/mapping/types/histogram.asciidoc | 10 +++++++++- docs/reference/mapping/types/ip.asciidoc | 10 +++++++++- docs/reference/mapping/types/keyword.asciidoc | 10 +++++++++- docs/reference/mapping/types/numeric.asciidoc | 10 +++++++++- docs/reference/mapping/types/text.asciidoc | 10 +++++++++- docs/reference/mapping/types/version.asciidoc | 10 +++++++++- 13 files changed, 116 insertions(+), 13 deletions(-) diff --git a/docs/reference/mapping/fields/synthetic-source.asciidoc b/docs/reference/mapping/fields/synthetic-source.asciidoc index 50a9ad315129..04f5e22e6e93 100644 --- a/docs/reference/mapping/fields/synthetic-source.asciidoc +++ b/docs/reference/mapping/fields/synthetic-source.asciidoc @@ -1,5 +1,12 @@ [[synthetic-source]] -==== Synthetic `_source` preview:[] +==== Synthetic `_source` + +IMPORTANT: Synthetic `_source` is Generally Available only for TSDB indices +(indices that have `index.mode` set to `time_series`). For other indices +synthetic `_source` is in technical preview. Features in technical preview may +be changed or removed in a future release. Elastic will apply best effort to fix +any issues, but features in technical preview are not subject to the support SLA +of official GA features. Though very handy to have around, the source field takes up a significant amount of space on disk. Instead of storing source documents on disk exactly as you diff --git a/docs/reference/mapping/types/aggregate-metric-double.asciidoc b/docs/reference/mapping/types/aggregate-metric-double.asciidoc index 9df58119e8a3..b80d2ad5057a 100644 --- a/docs/reference/mapping/types/aggregate-metric-double.asciidoc +++ b/docs/reference/mapping/types/aggregate-metric-double.asciidoc @@ -248,7 +248,15 @@ The search returns the following hit. The value of the `default_metric` field, // TESTRESPONSE[s/\.\.\./"took": $body.took,"timed_out": false,"_shards": $body._shards,/] [[aggregate-metric-double-synthetic-source]] -==== Synthetic `_source` preview:[] +==== Synthetic `_source` + +IMPORTANT: Synthetic `_source` is Generally Available only for TSDB indices +(indices that have `index.mode` set to `time_series`). For other indices +synthetic `_source` is in technical preview. Features in technical preview may +be changed or removed in a future release. Elastic will apply best effort to fix +any issues, but features in technical preview are not subject to the support SLA +of official GA features. + `aggregate_metric-double` fields support <> in their default configuration. Synthetic `_source` cannot be used together with <>. diff --git a/docs/reference/mapping/types/boolean.asciidoc b/docs/reference/mapping/types/boolean.asciidoc index c1347866e406..8b62cecd9d52 100644 --- a/docs/reference/mapping/types/boolean.asciidoc +++ b/docs/reference/mapping/types/boolean.asciidoc @@ -216,7 +216,15 @@ The following parameters are accepted by `boolean` fields: Metadata about the field. [[boolean-synthetic-source]] -==== Synthetic `_source` preview:[] +==== Synthetic `_source` + +IMPORTANT: Synthetic `_source` is Generally Available only for TSDB indices +(indices that have `index.mode` set to `time_series`). For other indices +synthetic `_source` is in technical preview. Features in technical preview may +be changed or removed in a future release. Elastic will apply best effort to fix +any issues, but features in technical preview are not subject to the support SLA +of official GA features. + `boolean` fields support <> in their default configuration. Synthetic `_source` cannot be used together with <> or with <> disabled. diff --git a/docs/reference/mapping/types/date.asciidoc b/docs/reference/mapping/types/date.asciidoc index eb5aeee9e8a1..e8c21e8f3dc4 100644 --- a/docs/reference/mapping/types/date.asciidoc +++ b/docs/reference/mapping/types/date.asciidoc @@ -232,7 +232,15 @@ Which will reply with a date like: ---- [[date-synthetic-source]] -==== Synthetic `_source` preview:[] +==== Synthetic `_source` + +IMPORTANT: Synthetic `_source` is Generally Available only for TSDB indices +(indices that have `index.mode` set to `time_series`). For other indices +synthetic `_source` is in technical preview. Features in technical preview may +be changed or removed in a future release. Elastic will apply best effort to fix +any issues, but features in technical preview are not subject to the support SLA +of official GA features. + `date` fields support <> in their default configuration. Synthetic `_source` cannot be used together with <>, <> set to true diff --git a/docs/reference/mapping/types/date_nanos.asciidoc b/docs/reference/mapping/types/date_nanos.asciidoc index 493a08dda74f..8f3af831e2b7 100644 --- a/docs/reference/mapping/types/date_nanos.asciidoc +++ b/docs/reference/mapping/types/date_nanos.asciidoc @@ -141,7 +141,15 @@ field. This limitation also affects <>. --- [[date-nanos-synthetic-source]] -==== Synthetic `_source` preview:[] +==== Synthetic `_source` + +IMPORTANT: Synthetic `_source` is Generally Available only for TSDB indices +(indices that have `index.mode` set to `time_series`). For other indices +synthetic `_source` is in technical preview. Features in technical preview may +be changed or removed in a future release. Elastic will apply best effort to fix +any issues, but features in technical preview are not subject to the support SLA +of official GA features. + `date_nanos` fields support <> in their default configuration. Synthetic `_source` cannot be used together with <>, <> set to true diff --git a/docs/reference/mapping/types/dense-vector.asciidoc b/docs/reference/mapping/types/dense-vector.asciidoc index 198fbf1f928e..6f135e31698c 100644 --- a/docs/reference/mapping/types/dense-vector.asciidoc +++ b/docs/reference/mapping/types/dense-vector.asciidoc @@ -195,5 +195,13 @@ neighbors for each new node. Defaults to `100`. ==== [[dense-vector-synthetic-source]] -==== Synthetic `_source` preview:[] +==== Synthetic `_source` + +IMPORTANT: Synthetic `_source` is Generally Available only for TSDB indices +(indices that have `index.mode` set to `time_series`). For other indices +synthetic `_source` is in technical preview. Features in technical preview may +be changed or removed in a future release. Elastic will apply best effort to fix +any issues, but features in technical preview are not subject to the support SLA +of official GA features. + `dense_vector` fields support <> . diff --git a/docs/reference/mapping/types/geo-point.asciidoc b/docs/reference/mapping/types/geo-point.asciidoc index 3f08234a98b0..47996b8e4822 100644 --- a/docs/reference/mapping/types/geo-point.asciidoc +++ b/docs/reference/mapping/types/geo-point.asciidoc @@ -208,7 +208,15 @@ def lon = doc['location'].lon; -------------------------------------------------- [[geo-point-synthetic-source]] -==== Synthetic source preview:[] +==== Synthetic source + +IMPORTANT: Synthetic `_source` is Generally Available only for TSDB indices +(indices that have `index.mode` set to `time_series`). For other indices +synthetic `_source` is in technical preview. Features in technical preview may +be changed or removed in a future release. Elastic will apply best effort to fix +any issues, but features in technical preview are not subject to the support SLA +of official GA features. + `geo_point` fields support <> in their default configuration. Synthetic `_source` cannot be used together with <>, <>, or with diff --git a/docs/reference/mapping/types/histogram.asciidoc b/docs/reference/mapping/types/histogram.asciidoc index 703cce6c83d0..fa95c4e6328d 100644 --- a/docs/reference/mapping/types/histogram.asciidoc +++ b/docs/reference/mapping/types/histogram.asciidoc @@ -69,7 +69,15 @@ means the field can technically be aggregated with either algorithm, in practice index data in that manner (e.g. centroids for T-Digest or intervals for HDRHistogram) to ensure best accuracy. [[histogram-synthetic-source]] -==== Synthetic `_source` preview:[] +==== Synthetic `_source` + +IMPORTANT: Synthetic `_source` is Generally Available only for TSDB indices +(indices that have `index.mode` set to `time_series`). For other indices +synthetic `_source` is in technical preview. Features in technical preview may +be changed or removed in a future release. Elastic will apply best effort to fix +any issues, but features in technical preview are not subject to the support SLA +of official GA features. + `histogram` fields support <> in their default configuration. Synthetic `_source` cannot be used together with <> or <>. diff --git a/docs/reference/mapping/types/ip.asciidoc b/docs/reference/mapping/types/ip.asciidoc index 1a7f90954a96..25e18a0c60a6 100644 --- a/docs/reference/mapping/types/ip.asciidoc +++ b/docs/reference/mapping/types/ip.asciidoc @@ -152,7 +152,15 @@ GET my-index-000001/_search -------------------------------------------------- [[ip-synthetic-source]] -==== Synthetic `_source` preview:[] +==== Synthetic `_source` + +IMPORTANT: Synthetic `_source` is Generally Available only for TSDB indices +(indices that have `index.mode` set to `time_series`). For other indices +synthetic `_source` is in technical preview. Features in technical preview may +be changed or removed in a future release. Elastic will apply best effort to fix +any issues, but features in technical preview are not subject to the support SLA +of official GA features. + `ip` fields support <> in their default configuration. Synthetic `_source` cannot be used together with <> or with <> disabled. diff --git a/docs/reference/mapping/types/keyword.asciidoc b/docs/reference/mapping/types/keyword.asciidoc index c1bd5b49dfc9..a47e1d4549eb 100644 --- a/docs/reference/mapping/types/keyword.asciidoc +++ b/docs/reference/mapping/types/keyword.asciidoc @@ -170,7 +170,15 @@ Dimension fields have the following constraints: -- [[keyword-synthetic-source]] -==== Synthetic `_source` preview:[] +==== Synthetic `_source` + +IMPORTANT: Synthetic `_source` is Generally Available only for TSDB indices +(indices that have `index.mode` set to `time_series`). For other indices +synthetic `_source` is in technical preview. Features in technical preview may +be changed or removed in a future release. Elastic will apply best effort to fix +any issues, but features in technical preview are not subject to the support SLA +of official GA features. + `keyword` fields support <> in their default configuration. Synthetic `_source` cannot be used together with a <> or <>. diff --git a/docs/reference/mapping/types/numeric.asciidoc b/docs/reference/mapping/types/numeric.asciidoc index 2e9805d2f178..bee79746806b 100644 --- a/docs/reference/mapping/types/numeric.asciidoc +++ b/docs/reference/mapping/types/numeric.asciidoc @@ -228,7 +228,15 @@ numeric field can't be both a time series dimension and a time series metric. This parameter is required. [[numeric-synthetic-source]] -==== Synthetic `_source` preview:[] +==== Synthetic `_source` + +IMPORTANT: Synthetic `_source` is Generally Available only for TSDB indices +(indices that have `index.mode` set to `time_series`). For other indices +synthetic `_source` is in technical preview. Features in technical preview may +be changed or removed in a future release. Elastic will apply best effort to fix +any issues, but features in technical preview are not subject to the support SLA +of official GA features. + All numeric fields except `unsigned_long` support <> in their default configuration. Synthetic `_source` cannot be used together with <>, <>, or diff --git a/docs/reference/mapping/types/text.asciidoc b/docs/reference/mapping/types/text.asciidoc index 730ca12c1a3e..aec330701a76 100644 --- a/docs/reference/mapping/types/text.asciidoc +++ b/docs/reference/mapping/types/text.asciidoc @@ -160,7 +160,15 @@ The following parameters are accepted by `text` fields: Metadata about the field. [[text-synthetic-source]] -==== Synthetic `_source` preview:[] +==== Synthetic `_source` + +IMPORTANT: Synthetic `_source` is Generally Available only for TSDB indices +(indices that have `index.mode` set to `time_series`). For other indices +synthetic `_source` is in technical preview. Features in technical preview may +be changed or removed in a future release. Elastic will apply best effort to fix +any issues, but features in technical preview are not subject to the support SLA +of official GA features. + `text` fields support <> if they have a <> sub-field that supports synthetic `_source` or if the `text` field sets `store` to `true`. Either way, it may diff --git a/docs/reference/mapping/types/version.asciidoc b/docs/reference/mapping/types/version.asciidoc index b042ea02da7c..b208127ae270 100644 --- a/docs/reference/mapping/types/version.asciidoc +++ b/docs/reference/mapping/types/version.asciidoc @@ -69,7 +69,15 @@ you strongly rely on these kind of queries. [[version-synthetic-source]] -==== Synthetic `_source` preview:[] +==== Synthetic `_source` + +IMPORTANT: Synthetic `_source` is Generally Available only for TSDB indices +(indices that have `index.mode` set to `time_series`). For other indices +synthetic `_source` is in technical preview. Features in technical preview may +be changed or removed in a future release. Elastic will apply best effort to fix +any issues, but features in technical preview are not subject to the support SLA +of official GA features. + `version` fields support <> so long as they don't declare <>. From 69bf51312c10840a37ac940b7ccf2682514b7c4a Mon Sep 17 00:00:00 2001 From: Mark Tozzi Date: Mon, 21 Nov 2022 09:59:27 -0500 Subject: [PATCH 018/919] clean up a few things in AggregatorTestCase (#91727) Small refactoring follow up on aggregations tests. This plumbs the AggTestConfig object into the private searchAndReduce method that actually does the work (as opposed to the public driver), and also pulls the flag for partial reduces up into the test config, which both makes the flag clearer and gives us an override point. The partial reduce flag still defaults to being random. --- .../aggregations/AggregatorTestCase.java | 95 ++++++++++++------- 1 file changed, 61 insertions(+), 34 deletions(-) diff --git a/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java b/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java index b0befd0911cc..26f417cfa889 100644 --- a/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java @@ -463,17 +463,7 @@ protected A searchAndReduc runWithCrankyCircuitBreaker(indexSettings, searcher, aggTestConfig); // Second run it to the end CircuitBreakerService breakerService = new NoneCircuitBreakerService(); - return searchAndReduce( - indexSettings, - searcher, - aggTestConfig.query(), - aggTestConfig.builder(), - aggTestConfig.maxBuckets(), - aggTestConfig.splitLeavesIntoSeparateAggregators(), - aggTestConfig.shouldBeCached(), - breakerService, - aggTestConfig.fieldTypes() - ); + return searchAndReduce(indexSettings, searcher, breakerService, aggTestConfig); } /** @@ -485,17 +475,7 @@ private void runWithCrankyCircuitBreaker(IndexSettings indexSettings, IndexSearc CircuitBreakerService crankyService = new CrankyCircuitBreakerService(); for (int i = 0; i < 5; i++) { try { - searchAndReduce( - indexSettings, - searcher, - aggTestConfig.query(), - aggTestConfig.builder(), - aggTestConfig.maxBuckets(), - aggTestConfig.splitLeavesIntoSeparateAggregators(), - aggTestConfig.shouldBeCached(), - crankyService, - aggTestConfig.fieldTypes() - ); + searchAndReduce(indexSettings, searcher, crankyService, aggTestConfig); } catch (CircuitBreakingException e) { // expected } catch (IOException e) { @@ -508,14 +488,16 @@ private void runWithCrankyCircuitBreaker(IndexSettings indexSettings, IndexSearc private A searchAndReduce( IndexSettings indexSettings, IndexSearcher searcher, - Query query, - AggregationBuilder builder, - int maxBucket, - boolean splitLeavesIntoSeparateAggregators, - boolean shouldBeCached, CircuitBreakerService breakerService, - MappedFieldType... fieldTypes + AggTestConfig aggTestConfig ) throws IOException { + Query query = aggTestConfig.query(); + AggregationBuilder builder = aggTestConfig.builder(); + int maxBucket = aggTestConfig.maxBuckets(); + boolean splitLeavesIntoSeparateAggregators = aggTestConfig.splitLeavesIntoSeparateAggregators(); + boolean shouldBeCached = aggTestConfig.shouldBeCached(); + MappedFieldType[] fieldTypes = aggTestConfig.fieldTypes(); + final IndexReaderContext ctx = searcher.getTopReaderContext(); final PipelineTree pipelines = builder.buildPipelineTree(); List aggs = new ArrayList<>(); @@ -588,7 +570,7 @@ private A searchAndReduce( BigArrays bigArraysForReduction = new MockBigArrays(new MockPageCacheRecycler(Settings.EMPTY), breakerService); try { - if (randomBoolean() && aggs.size() > 1) { + if (aggTestConfig.incrementalReduce() && aggs.size() > 1) { // sometimes do an incremental reduce int toReduceSize = aggs.size(); Collections.shuffle(aggs, random()); @@ -1528,27 +1510,72 @@ public record AggTestConfig( int maxBuckets, boolean splitLeavesIntoSeparateAggregators, boolean shouldBeCached, + boolean incrementalReduce, MappedFieldType... fieldTypes ) { public AggTestConfig(AggregationBuilder builder, MappedFieldType... fieldTypes) { - this(new MatchAllDocsQuery(), builder, DEFAULT_MAX_BUCKETS, randomBoolean(), true, fieldTypes); + this(new MatchAllDocsQuery(), builder, DEFAULT_MAX_BUCKETS, randomBoolean(), true, randomBoolean(), fieldTypes); } public AggTestConfig withQuery(Query query) { - return new AggTestConfig(query, builder, maxBuckets, splitLeavesIntoSeparateAggregators, shouldBeCached, fieldTypes); + return new AggTestConfig( + query, + builder, + maxBuckets, + splitLeavesIntoSeparateAggregators, + shouldBeCached, + incrementalReduce, + fieldTypes + ); } public AggTestConfig withSplitLeavesIntoSeperateAggregators(boolean splitLeavesIntoSeparateAggregators) { - return new AggTestConfig(query, builder, maxBuckets, splitLeavesIntoSeparateAggregators, shouldBeCached, fieldTypes); + return new AggTestConfig( + query, + builder, + maxBuckets, + splitLeavesIntoSeparateAggregators, + shouldBeCached, + incrementalReduce, + fieldTypes + ); } public AggTestConfig withShouldBeCached(boolean shouldBeCached) { - return new AggTestConfig(query, builder, maxBuckets, splitLeavesIntoSeparateAggregators, shouldBeCached, fieldTypes); + return new AggTestConfig( + query, + builder, + maxBuckets, + splitLeavesIntoSeparateAggregators, + shouldBeCached, + incrementalReduce, + fieldTypes + ); } public AggTestConfig withMaxBuckets(int maxBuckets) { - return new AggTestConfig(query, builder, maxBuckets, splitLeavesIntoSeparateAggregators, shouldBeCached, fieldTypes); + return new AggTestConfig( + query, + builder, + maxBuckets, + splitLeavesIntoSeparateAggregators, + shouldBeCached, + incrementalReduce, + fieldTypes + ); + } + + public AggTestConfig withIncrementalReduce(boolean incrementalReduce) { + return new AggTestConfig( + query, + builder, + maxBuckets, + splitLeavesIntoSeparateAggregators, + shouldBeCached, + incrementalReduce, + fieldTypes + ); } } } From 642a77f66e90c056d6baf37e680cf43044707dd9 Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Mon, 21 Nov 2022 16:11:16 +0100 Subject: [PATCH 019/919] Followup on comments from #91612 (#91700) --- .../BalancedShardsAllocatorTests.java | 204 +++++++++--------- .../ClusterAllocationSimulationTests.java | 7 +- .../DesiredBalanceReconcilerTests.java | 1 - .../cluster/ESAllocationTestCase.java | 27 ++- 4 files changed, 112 insertions(+), 127 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocatorTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocatorTests.java index f7fa7cfc88f1..6b23e5126655 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocatorTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocatorTests.java @@ -15,6 +15,7 @@ import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ESAllocationTestCase; +import org.elasticsearch.cluster.EmptyClusterInfoService; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.node.DiscoveryNode; @@ -31,33 +32,36 @@ import org.elasticsearch.cluster.routing.TestShardRouting; import org.elasticsearch.cluster.routing.allocation.AllocateUnassignedDecision; import org.elasticsearch.cluster.routing.allocation.RoutingAllocation; -import org.elasticsearch.cluster.routing.allocation.WriteLoadForecaster; import org.elasticsearch.cluster.routing.allocation.decider.AllocationDeciders; import org.elasticsearch.cluster.routing.allocation.decider.ThrottlingAllocationDecider; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeValue; -import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.Tuple; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.snapshots.SnapshotShardSizeInfo; +import org.elasticsearch.test.gateway.TestGatewayAllocator; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.OptionalDouble; import java.util.Set; -import java.util.stream.IntStream; -import java.util.stream.Stream; - +import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static java.util.stream.Collectors.mapping; +import static java.util.stream.Collectors.summingDouble; +import static java.util.stream.Collectors.summingLong; +import static java.util.stream.Collectors.toSet; import static org.elasticsearch.cluster.routing.ShardRoutingState.RELOCATING; import static org.elasticsearch.cluster.routing.allocation.allocator.BalancedShardsAllocator.Balancer.getIndexDiskUsageInBytes; -import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.everyItem; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.lessThanOrEqualTo; @@ -93,96 +97,97 @@ public void testDecideShardAllocation() { assertNotNull(allocateDecision.getTargetNode().getId(), assignedShards.get(0).currentNodeId()); } - public void testBalanceByShardLoad() { - - var smallIndices = IntStream.range(1, 5) - .mapToObj(i -> IndexMetadata.builder("small-index-" + i)) - .map(builder -> builder.settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(0)) - .map(builder -> builder.indexWriteLoadForecast(randomIngestLoad(1.5))); - - var heavyIndex = IndexMetadata.builder("heavy-index") - .settings(settings(Version.CURRENT)) - .numberOfShards(1) - .numberOfReplicas(0) - .indexWriteLoadForecast(8.0); - - var clusterState = stateWithStartedIndices(Stream.concat(smallIndices, Stream.of(heavyIndex)).toList()); - - var testWriteLoadForecaster = new WriteLoadForecaster() { - @Override - public Metadata.Builder withWriteLoadForecastForWriteIndex(String dataStreamName, Metadata.Builder metadata) { - throw new UnsupportedOperationException("Not required for test"); - } - - @Override - @SuppressForbidden(reason = "This is required to test balancing by ingest load") - public OptionalDouble getForecastedWriteLoad(IndexMetadata indexMetadata) { - return indexMetadata.getForecastedWriteLoad(); - } - }; + public void testBalanceByWriteLoad() { + + var allocationService = new MockAllocationService( + yesAllocationDeciders(), + new TestGatewayAllocator(), + new BalancedShardsAllocator( + Settings.EMPTY, + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), + TEST_WRITE_LOAD_FORECASTER + ), + EmptyClusterInfoService.INSTANCE, + SNAPSHOT_INFO_SERVICE_WITH_NO_SHARD_SIZES + ); - var settings = Settings.EMPTY; - var allocator = new BalancedShardsAllocator( - settings, - new ClusterSettings(settings, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), - testWriteLoadForecaster + var clusterState = applyStartedShardsUntilNoChange( + stateWithStartedIndices( + IndexMetadata.builder("heavy-index").indexWriteLoadForecast(8.0), + IndexMetadata.builder("light-index-1").indexWriteLoadForecast(1.0), + IndexMetadata.builder("light-index-2").indexWriteLoadForecast(2.0), + IndexMetadata.builder("light-index-3").indexWriteLoadForecast(3.0), + IndexMetadata.builder("zero-write-load-index").indexWriteLoadForecast(0.0), + IndexMetadata.builder("no-write-load-index") + ), + allocationService ); - var allocation = createRoutingAllocation(clusterState); - allocator.allocate(allocation); - assertThat(allocation.metadata().getTotalNumberOfShards(), allOf(greaterThanOrEqualTo(3), lessThanOrEqualTo(5))); - for (RoutingNode routingNode : allocation.routingNodes()) { - var nodeIngestLoad = 0.0; - for (ShardRouting shardRouting : routingNode) { - if (shardRouting.started() || shardRouting.initializing()) { // count load from the target node when relocating - var indexMetadata = clusterState.metadata().index(shardRouting.index()); - nodeIngestLoad += testWriteLoadForecaster.getForecastedWriteLoad(indexMetadata).orElse(0.0); - } - } - assertThat(nodeIngestLoad, lessThanOrEqualTo(8.0 + 1.5)); - } - } + assertThat( + getShardsPerNode(clusterState).values(), + contains( + Set.of("heavy-index"), + Set.of("light-index-1", "light-index-2", "light-index-3", "zero-write-load-index", "no-write-load-index") + ) + ); - private Double randomIngestLoad(double max) { - return switch (randomInt(3)) { - case 0 -> null; - case 1 -> 0.0; - default -> randomDoubleBetween(1.0, max, true); - }; + assertThat( + getPerNode( + clusterState, + summingDouble( + it -> TEST_WRITE_LOAD_FORECASTER.getForecastedWriteLoad(clusterState.metadata().index(it.index())).orElse(0.0) + ) + ).values(), + everyItem(lessThanOrEqualTo(8.0)) + ); } public void testBalanceByDiskUsage() { - var smallIndices = IntStream.range(1, 5) - .mapToObj(i -> IndexMetadata.builder("small-index-" + i)) - .map(builder -> builder.settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(0)) - .map(builder -> builder.shardSizeInBytesForecast(ByteSizeValue.ofGb(1).getBytes())); + var allocationService = createAllocationService( + Settings.builder() + // enable disk based balancing + .put(BalancedShardsAllocator.DISK_USAGE_BALANCE_FACTOR_SETTING.getKey(), "1e-9") + .build() + ); - var heavyIndex = IndexMetadata.builder("heavy-index") - .settings(settings(Version.CURRENT)) - .numberOfShards(1) - .numberOfReplicas(0) - .shardSizeInBytesForecast(ByteSizeValue.ofGb(8).getBytes()); + var clusterState = applyStartedShardsUntilNoChange( + stateWithStartedIndices( + IndexMetadata.builder("heavy-index").shardSizeInBytesForecast(ByteSizeValue.ofGb(8).getBytes()), + IndexMetadata.builder("light-index-1").shardSizeInBytesForecast(ByteSizeValue.ofGb(1).getBytes()), + IndexMetadata.builder("light-index-2").shardSizeInBytesForecast(ByteSizeValue.ofGb(2).getBytes()), + IndexMetadata.builder("light-index-3").shardSizeInBytesForecast(ByteSizeValue.ofGb(3).getBytes()), + IndexMetadata.builder("zero-disk-usage-index").shardSizeInBytesForecast(0L), + IndexMetadata.builder("no-disk-usage-index") + ), + allocationService + ); - var clusterState = stateWithStartedIndices(Stream.concat(smallIndices, Stream.of(heavyIndex)).toList()); + assertThat( + getShardsPerNode(clusterState).values(), + contains( + Set.of("heavy-index"), + Set.of("light-index-1", "light-index-2", "light-index-3", "zero-disk-usage-index", "no-disk-usage-index") + ) + ); - var allocator = new BalancedShardsAllocator( - Settings.builder().put("cluster.routing.allocation.balance.disk_usage", "1e-9").build() + assertThat( + getPerNode( + clusterState, + summingLong(it -> clusterState.metadata().index(it.index()).getForecastedShardSizeInBytes().orElse(0L)) + ).values(), + everyItem(lessThanOrEqualTo(ByteSizeValue.ofGb(8).getBytes())) ); - var allocation = createRoutingAllocation(clusterState); - allocator.allocate(allocation); + } - assertThat(allocation.metadata().getTotalNumberOfShards(), allOf(greaterThanOrEqualTo(3), lessThanOrEqualTo(5))); - for (RoutingNode routingNode : allocation.routingNodes()) { - var nodeDiskUsage = 0L; - for (ShardRouting shardRouting : routingNode) { - if (shardRouting.started() || shardRouting.initializing()) { // count load from the target node when relocating - var indexMetadata = clusterState.metadata().index(shardRouting.index()); - nodeDiskUsage += indexMetadata.getForecastedShardSizeInBytes().orElse(0L); - } - } - assertThat(nodeDiskUsage, lessThanOrEqualTo(ByteSizeValue.ofGb(8 + 1).getBytes())); - } + private static Map> getShardsPerNode(ClusterState clusterState) { + return getPerNode(clusterState, mapping(ShardRouting::getIndexName, toSet())); + } + + private static Map getPerNode(ClusterState clusterState, Collector collector) { + return clusterState.getRoutingNodes() + .stream() + .collect(Collectors.toMap(RoutingNode::nodeId, it -> StreamSupport.stream(it.spliterator(), false).collect(collector))); } /** @@ -333,26 +338,13 @@ private RoutingAllocation createRoutingAllocation(ClusterState clusterState) { ); } - private static ClusterState stateWithStartedIndices(List indices) { + private static ClusterState stateWithStartedIndices(IndexMetadata.Builder... indices) { var metadataBuilder = Metadata.builder(); var routingTableBuilder = RoutingTable.builder(); for (var index : indices) { - var inSyncId = UUIDs.randomBase64UUID(random()); - var build = index.putInSyncAllocationIds(0, Set.of(inSyncId)).build(); + var build = index.settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(0).build(); metadataBuilder.put(build, false); - routingTableBuilder.add( - IndexRoutingTable.builder(build.getIndex()) - .addShard( - TestShardRouting.newShardRouting( - new ShardId(build.getIndex(), 0), - randomFrom("node-1", "node-2"), - null, - true, - ShardRoutingState.STARTED, - AllocationId.newInitializing(inSyncId) - ) - ) - ); + routingTableBuilder.addAsNew(build); } return ClusterState.builder(ClusterName.DEFAULT) @@ -383,13 +375,9 @@ private void addIndex( var numberOfShards = assignments.entrySet().stream().mapToInt(Map.Entry::getValue).sum(); var inSyncIds = randomList(numberOfShards, numberOfShards, () -> UUIDs.randomBase64UUID(random())); var indexMetadataBuilder = IndexMetadata.builder(name) - .settings( - Settings.builder() - .put("index.number_of_shards", numberOfShards) - .put("index.number_of_replicas", 0) - .put("index.version.created", Version.CURRENT) - .build() - ); + .settings(settings(Version.CURRENT)) + .numberOfShards(numberOfShards) + .numberOfReplicas(0); for (int shardId = 0; shardId < numberOfShards; shardId++) { indexMetadataBuilder.putInSyncAllocationIds(shardId, Set.of(inSyncIds.get(shardId))); diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/ClusterAllocationSimulationTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/ClusterAllocationSimulationTests.java index 6495de7b7441..98c068d874d9 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/ClusterAllocationSimulationTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/ClusterAllocationSimulationTests.java @@ -380,9 +380,8 @@ void addNode(long sizeBytes, int shardCount, double writeLoad) { for (ShardRouting shardRouting : routingNode) { shards += 1; totalBytes += shardSizesByIndex.get(shardRouting.index().getName()); - totalWriteLoad += SIMULATION_WRITE_LOAD_FORECASTER.getForecastedWriteLoad( - clusterState.metadata().index(shardRouting.index()) - ).orElseThrow(() -> new AssertionError("missing write load")); + totalWriteLoad += TEST_WRITE_LOAD_FORECASTER.getForecastedWriteLoad(clusterState.metadata().index(shardRouting.index())) + .orElseThrow(() -> new AssertionError("missing write load")); } results.startObject(); @@ -476,7 +475,7 @@ private Map.Entry createNewAllocationSer new BalancedShardsAllocator( Settings.EMPTY, new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), - SIMULATION_WRITE_LOAD_FORECASTER + TEST_WRITE_LOAD_FORECASTER ), threadPool, clusterService, diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceReconcilerTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceReconcilerTests.java index 13df8b5e6af6..e80be30262e7 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceReconcilerTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceReconcilerTests.java @@ -1024,7 +1024,6 @@ public void testDoNotRebalanceToTheNodeThatNoLongerExists() { .put(SETTING_NUMBER_OF_REPLICAS, 0) .put(SETTING_INDEX_VERSION_CREATED.getKey(), Version.CURRENT) ) - .system(randomBoolean()) .build(); final var index = indexMetadata.getIndex(); final var shardId = new ShardId(index, 0); diff --git a/test/framework/src/main/java/org/elasticsearch/cluster/ESAllocationTestCase.java b/test/framework/src/main/java/org/elasticsearch/cluster/ESAllocationTestCase.java index c984f3a31556..38ac3dfb6422 100644 --- a/test/framework/src/main/java/org/elasticsearch/cluster/ESAllocationTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/cluster/ESAllocationTestCase.java @@ -71,6 +71,19 @@ public Long getShardSize(ShardRouting shardRouting) { } }; + public static final WriteLoadForecaster TEST_WRITE_LOAD_FORECASTER = new WriteLoadForecaster() { + @Override + public Metadata.Builder withWriteLoadForecastForWriteIndex(String dataStreamName, Metadata.Builder metadata) { + throw new AssertionError("Not required for testing"); + } + + @Override + @SuppressForbidden(reason = "tests do not need a license to access the write load") + public OptionalDouble getForecastedWriteLoad(IndexMetadata indexMetadata) { + return indexMetadata.getForecastedWriteLoad(); + } + }; + public static MockAllocationService createAllocationService() { return createAllocationService(Settings.EMPTY); } @@ -397,18 +410,4 @@ public void allocateUnassigned( } } } - - protected static final WriteLoadForecaster SIMULATION_WRITE_LOAD_FORECASTER = new WriteLoadForecaster() { - @Override - public Metadata.Builder withWriteLoadForecastForWriteIndex(String dataStreamName, Metadata.Builder metadata) { - throw new AssertionError("only called during rollover"); - } - - @Override - @SuppressForbidden(reason = "tests do not need a license to access the write load") - public OptionalDouble getForecastedWriteLoad(IndexMetadata indexMetadata) { - return indexMetadata.getForecastedWriteLoad(); - } - }; - } From 1c9a39c612015f6e97218fe67bd12322c14a7c53 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 21 Nov 2022 15:37:35 +0000 Subject: [PATCH 020/919] Skip ancient closed indices in desired balance (#91765) This assertion fails in the presence of pre-7.2.0 closed indices because such indices don't even have routing table entries. Relates #33888 Closes #91470 --- .../allocator/DesiredBalanceReconciler.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceReconciler.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceReconciler.java index 6bf0ed8eeba0..19a388964fea 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceReconciler.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceReconciler.java @@ -11,6 +11,9 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.lucene.util.ArrayUtil; +import org.elasticsearch.Version; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.MetadataIndexStateService; import org.elasticsearch.cluster.metadata.SingleNodeShutdownMetadata; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.routing.RoutingNode; @@ -95,8 +98,11 @@ private boolean allocateUnassignedInvariant() { assert routingNodes.unassigned().isEmpty(); - final var shardCounts = allocation.metadata() - .stream() + final var shardCounts = allocation.metadata().stream().filter(indexMetadata -> + // skip any pre-7.2 closed indices which have no routing table entries at all + indexMetadata.getCreationVersion().onOrAfter(Version.V_7_2_0) + || indexMetadata.getState() == IndexMetadata.State.OPEN + || MetadataIndexStateService.isIndexVerifiedBeforeClosed(indexMetadata)) .flatMap( indexMetadata -> IntStream.range(0, indexMetadata.getNumberOfShards()) .mapToObj( @@ -151,7 +157,7 @@ private void failAllocationOfNewPrimaries(RoutingAllocation allocation) { private void allocateUnassigned() { RoutingNodes.UnassignedShards unassigned = routingNodes.unassigned(); if (logger.isTraceEnabled()) { - logger.trace("Start allocating unassigned shards"); + logger.trace("Start allocating unassigned shards: {}", routingNodes.toString()); } if (unassigned.isEmpty()) { return; From 6b26477b77be75e9a5bcd12d90a8afd4a120f93b Mon Sep 17 00:00:00 2001 From: Joe Gallo Date: Mon, 21 Nov 2022 17:13:03 -0500 Subject: [PATCH 021/919] More accurate total ingest stats (#91730) --- docs/changelog/91730.yaml | 6 ++++++ .../org/elasticsearch/ingest/IngestService.java | 13 +++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) create mode 100644 docs/changelog/91730.yaml diff --git a/docs/changelog/91730.yaml b/docs/changelog/91730.yaml new file mode 100644 index 000000000000..3db9189da486 --- /dev/null +++ b/docs/changelog/91730.yaml @@ -0,0 +1,6 @@ +pr: 91730 +summary: More accurate total ingest stats +area: Ingest Node +type: bug +issues: + - 91358 diff --git a/server/src/main/java/org/elasticsearch/ingest/IngestService.java b/server/src/main/java/org/elasticsearch/ingest/IngestService.java index 11ce150cb923..95016b688db9 100644 --- a/server/src/main/java/org/elasticsearch/ingest/IngestService.java +++ b/server/src/main/java/org/elasticsearch/ingest/IngestService.java @@ -761,8 +761,13 @@ private void executePipelines( } Pipeline pipeline = holder.pipeline; String originalIndex = indexRequest.indices()[0]; + long startTimeInNanos = System.nanoTime(); + totalMetrics.preIngest(); innerExecute(slot, indexRequest, pipeline, onDropped, e -> { + long ingestTimeInNanos = System.nanoTime() - startTimeInNanos; + totalMetrics.postIngest(ingestTimeInNanos); if (e != null) { + totalMetrics.ingestFailed(); logger.debug( () -> format( "failed to execute pipeline [%s] for document [%s/%s]", @@ -893,10 +898,6 @@ private void innerExecute( return; } - long startTimeInNanos = System.nanoTime(); - // the pipeline specific stat holder may not exist and that is fine: - // (e.g. the pipeline may have been removed while we're ingesting a document - totalMetrics.preIngest(); String index = indexRequest.index(); String id = indexRequest.id(); String routing = indexRequest.routing(); @@ -916,10 +917,7 @@ private void innerExecute( logger.warn("A listener was unexpectedly called more than once", new RuntimeException(e)); assert false : "A listener was unexpectedly called more than once"; } else { - long ingestTimeInNanos = System.nanoTime() - startTimeInNanos; - totalMetrics.postIngest(ingestTimeInNanos); if (e != null) { - totalMetrics.ingestFailed(); handler.accept(e); } else if (result == null) { itemDroppedHandler.accept(slot); @@ -951,7 +949,6 @@ private void innerExecute( // processor creates a source map that is self-referencing. // In that case, we catch and wrap the exception so we can // include which pipeline failed. - totalMetrics.ingestFailed(); handler.accept( new IllegalArgumentException( "Failed to generate the source document for ingest pipeline [" + pipeline.getId() + "]", From fbb300bef5a27f7e36fd349e749e7dc9b11dd1fd Mon Sep 17 00:00:00 2001 From: David Roberts Date: Tue, 22 Nov 2022 08:30:45 +0000 Subject: [PATCH 022/919] Extra kibana_system privileges for Fleet transform upgrades (#91499) These changes go with those of elastic/kibana#142920. As we formalize the process by which the Fleet package installer will upgrade transforms more operations are required for managing the transforms and the related destination index: 1. Need to be able to add an alias on the transform destination index and adjust which indices it points to when upgrading the transform. 2. Need to be able to remove a default ingest pipeline from the settings of an old transform destination index during an upgrade that deletes the ingest pipeline. --- docs/changelog/91499.yaml | 5 +++++ .../authz/store/ReservedRolesStore.java | 14 +++++++++++--- .../authz/store/ReservedRolesStoreTests.java | 18 +++++++++++++----- 3 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 docs/changelog/91499.yaml diff --git a/docs/changelog/91499.yaml b/docs/changelog/91499.yaml new file mode 100644 index 000000000000..600c0f27cedd --- /dev/null +++ b/docs/changelog/91499.yaml @@ -0,0 +1,5 @@ +pr: 91499 +summary: Extra `kibana_system` privileges for Fleet transform upgrades +area: Authorization +type: enhancement +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java index bbeab238dc0c..399639f70afb 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java @@ -811,12 +811,20 @@ public static RoleDescriptor kibanaSystemRoleDescriptor(String name) { ".metrics-endpoint.metadata_current_default", ".metrics-endpoint.metadata_united_default" ) - .privileges("create_index", "delete_index", "read", "index") + .privileges("create_index", "delete_index", "read", "index", IndicesAliasesAction.NAME, UpdateSettingsAction.NAME) .build(), // For src/dest indices of the example transform package RoleDescriptor.IndicesPrivileges.builder() .indices("kibana_sample_data_*") - .privileges("create_index", "delete_index", "read", "index", "view_index_metadata") + .privileges( + "create_index", + "delete_index", + "read", + "index", + "view_index_metadata", + IndicesAliasesAction.NAME, + UpdateSettingsAction.NAME + ) .build(), // For src/dest indices of the Cloud Security Posture packages that ships a transform RoleDescriptor.IndicesPrivileges.builder() @@ -825,7 +833,7 @@ public static RoleDescriptor kibanaSystemRoleDescriptor(String name) { .build(), RoleDescriptor.IndicesPrivileges.builder() .indices("logs-cloud_security_posture.findings_latest-default", "logs-cloud_security_posture.scores-default") - .privileges("create_index", "read", "index", "delete") + .privileges("create_index", "read", "index", "delete", IndicesAliasesAction.NAME, UpdateSettingsAction.NAME) .build() }, null, new ConfigurableClusterPrivilege[] { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java index 8c562e267a62..728ddbadf138 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java @@ -993,12 +993,15 @@ public void testKibanaSystemRole() { assertThat(kibanaRole.indices().allowedIndicesMatcher(IndexAction.NAME).test(indexAbstraction), is(true)); assertThat(kibanaRole.indices().allowedIndicesMatcher(UpdateAction.NAME).test(indexAbstraction), is(true)); assertThat(kibanaRole.indices().allowedIndicesMatcher(BulkAction.NAME).test(indexAbstraction), is(true)); - // Allow create and delete index + // Allow create and delete index, modifying aliases, and updating index settings assertThat(kibanaRole.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(indexAbstraction), is(true)); assertThat(kibanaRole.indices().allowedIndicesMatcher(AutoCreateAction.NAME).test(indexAbstraction), is(true)); assertThat(kibanaRole.indices().allowedIndicesMatcher(CreateDataStreamAction.NAME).test(indexAbstraction), is(true)); assertThat(kibanaRole.indices().allowedIndicesMatcher(DeleteIndexAction.NAME).test(indexAbstraction), is(true)); assertThat(kibanaRole.indices().allowedIndicesMatcher(DeleteDataStreamAction.NAME).test(indexAbstraction), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(GetAliasesAction.NAME).test(indexAbstraction), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(IndicesAliasesAction.NAME).test(indexAbstraction), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(indexAbstraction), is(true)); // Implied by the overall view_index_metadata and monitor privilege assertViewIndexMetadata(kibanaRole, indexName); @@ -1013,9 +1016,8 @@ public void testKibanaSystemRole() { is(indexAbstraction.getType() != IndexAbstraction.Type.DATA_STREAM) ); - // Deny deleting documents and modifying the index settings + // Deny deleting documents and rollover assertThat(kibanaRole.indices().allowedIndicesMatcher(DeleteAction.NAME).test(indexAbstraction), is(false)); - assertThat(kibanaRole.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(indexAbstraction), is(false)); assertThat(kibanaRole.indices().allowedIndicesMatcher(RolloverAction.NAME).test(indexAbstraction), is(false)); }); @@ -1073,10 +1075,13 @@ public void testKibanaSystemRole() { assertThat(kibanaRole.indices().allowedIndicesMatcher(IndexAction.NAME).test(indexAbstraction), is(true)); assertThat(kibanaRole.indices().allowedIndicesMatcher(UpdateAction.NAME).test(indexAbstraction), is(true)); assertThat(kibanaRole.indices().allowedIndicesMatcher(BulkAction.NAME).test(indexAbstraction), is(true)); - // Allow create and delete index + // Allow create and delete index, modifying aliases, and updating index settings assertThat(kibanaRole.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(indexAbstraction), is(true)); assertThat(kibanaRole.indices().allowedIndicesMatcher(AutoCreateAction.NAME).test(indexAbstraction), is(true)); assertThat(kibanaRole.indices().allowedIndicesMatcher(CreateDataStreamAction.NAME).test(indexAbstraction), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(GetAliasesAction.NAME).test(indexAbstraction), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(IndicesAliasesAction.NAME).test(indexAbstraction), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(indexAbstraction), is(true)); // Implied by the overall view_index_metadata and monitor privilege assertViewIndexMetadata(kibanaRole, indexName); @@ -1121,9 +1126,12 @@ public void testKibanaSystemRole() { assertThat(kibanaRole.indices().allowedIndicesMatcher(IndexAction.NAME).test(indexAbstraction), is(true)); assertThat(kibanaRole.indices().allowedIndicesMatcher(UpdateAction.NAME).test(indexAbstraction), is(true)); assertThat(kibanaRole.indices().allowedIndicesMatcher(BulkAction.NAME).test(indexAbstraction), is(true)); - // Allow create and delete index + // Allow create and delete index, modifying aliases, and updating index settings assertThat(kibanaRole.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(indexAbstraction), is(true)); assertThat(kibanaRole.indices().allowedIndicesMatcher(DeleteIndexAction.NAME).test(indexAbstraction), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(GetAliasesAction.NAME).test(indexAbstraction), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(IndicesAliasesAction.NAME).test(indexAbstraction), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(indexAbstraction), is(true)); // Implied by the overall view_index_metadata and monitor privilege assertViewIndexMetadata(kibanaRole, indexName); From 3286bae8c6ca083f8a03918810a270b0ae3a8d07 Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Tue, 22 Nov 2022 10:19:07 +0100 Subject: [PATCH 023/919] Add new H3 api method #h3ToNoChildrenIntersecting (#91673) Given an H3 index, this method returns all the h3 index in the next resolution that intersects the provided h3 index but they are not children from it. --- docs/changelog/91673.yaml | 5 +++ .../main/java/org/elasticsearch/h3/H3.java | 28 ++++++++++++ .../java/org/elasticsearch/h3/HexRing.java | 2 +- .../h3/ParentChildNavigationTests.java | 45 ++++++++++++++++++- 4 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 docs/changelog/91673.yaml diff --git a/docs/changelog/91673.yaml b/docs/changelog/91673.yaml new file mode 100644 index 000000000000..86fb9f487658 --- /dev/null +++ b/docs/changelog/91673.yaml @@ -0,0 +1,5 @@ +pr: 91673 +summary: "Add new H3 api method #h3ToNoChildrenIntersecting" +area: Geo +type: enhancement +issues: [] diff --git a/libs/h3/src/main/java/org/elasticsearch/h3/H3.java b/libs/h3/src/main/java/org/elasticsearch/h3/H3.java index a27799855239..0f1d97e52166 100644 --- a/libs/h3/src/main/java/org/elasticsearch/h3/H3.java +++ b/libs/h3/src/main/java/org/elasticsearch/h3/H3.java @@ -248,6 +248,34 @@ public static String[] h3ToChildren(String h3Address) { return h3ToStringList(h3ToChildren(stringToH3(h3Address))); } + private static final int[] PEN_INTERSECTING_CHILDREN_DIRECTIONS = new int[] { 3, 1, 6, 4, 2 }; + private static final int[] HEX_INTERSECTING_CHILDREN_DIRECTIONS = new int[] { 3, 6, 2, 5, 1, 4 }; + + /** + * Returns the h3 bins on the level below which are not children of the given H3 index but + * intersects with it. + */ + public static long[] h3ToNoChildrenIntersecting(long h3) { + final long[] children = new long[cellToChildrenSize(h3) - 1]; + final Iterator.IterCellsChildren it = Iterator.iterInitParent(h3, H3Index.H3_get_resolution(h3) + 1); + final int[] directions = H3.isPentagon(it.h) ? PEN_INTERSECTING_CHILDREN_DIRECTIONS : HEX_INTERSECTING_CHILDREN_DIRECTIONS; + int pos = 0; + Iterator.iterStepChild(it); + while (it.h != Iterator.H3_NULL) { + children[pos] = HexRing.h3NeighborInDirection(it.h, directions[pos++]); + Iterator.iterStepChild(it); + } + return children; + } + + /** + * Returns the h3 addresses on the level below which are not children of the given H3 address but + * intersects with it. + */ + public static String[] h3ToNoChildrenIntersecting(String h3Address) { + return h3ToStringList(h3ToNoChildrenIntersecting(stringToH3(h3Address))); + } + /** * Returns the neighbor indexes. * diff --git a/libs/h3/src/main/java/org/elasticsearch/h3/HexRing.java b/libs/h3/src/main/java/org/elasticsearch/h3/HexRing.java index 3dfe2417be06..0fc623b0bb62 100644 --- a/libs/h3/src/main/java/org/elasticsearch/h3/HexRing.java +++ b/libs/h3/src/main/java/org/elasticsearch/h3/HexRing.java @@ -700,7 +700,7 @@ public static boolean areNeighbours(long origin, long destination) { * @param dir Direction to move in * @return H3Index of the specified neighbor or -1 if there is no more neighbor */ - private static long h3NeighborInDirection(long origin, int dir) { + static long h3NeighborInDirection(long origin, int dir) { long current = origin; int newRotations = 0; diff --git a/libs/h3/src/test/java/org/elasticsearch/h3/ParentChildNavigationTests.java b/libs/h3/src/test/java/org/elasticsearch/h3/ParentChildNavigationTests.java index 4e8ed3e4b646..8785050b32cc 100644 --- a/libs/h3/src/test/java/org/elasticsearch/h3/ParentChildNavigationTests.java +++ b/libs/h3/src/test/java/org/elasticsearch/h3/ParentChildNavigationTests.java @@ -20,8 +20,15 @@ import com.carrotsearch.randomizedtesting.generators.RandomPicks; +import org.apache.lucene.spatial3d.geom.GeoPoint; +import org.apache.lucene.spatial3d.geom.GeoPolygon; +import org.apache.lucene.spatial3d.geom.GeoPolygonFactory; +import org.apache.lucene.spatial3d.geom.PlanetModel; import org.elasticsearch.test.ESTestCase; +import java.util.ArrayList; +import java.util.List; + public class ParentChildNavigationTests extends ESTestCase { public void testParentChild() { @@ -44,11 +51,11 @@ public void testParentChild() { public void testHexRing() { String[] h3Addresses = H3.getStringRes0Cells(); - String h3Address = RandomPicks.randomFrom(random(), h3Addresses); for (int i = 1; i < H3.MAX_H3_RES; i++) { + String h3Address = RandomPicks.randomFrom(random(), h3Addresses); + assertEquals(i - 1, H3.getResolution(h3Address)); h3Addresses = H3.h3ToChildren(h3Address); assertHexRing(i, h3Address, h3Addresses); - h3Address = RandomPicks.randomFrom(random(), h3Addresses); } } @@ -65,4 +72,38 @@ private void assertHexRing(int res, String h3Address, String[] children) { assertEquals(children[i], ring[positions[i - 1]]); } } + + public void testNoChildrenIntersecting() { + String[] h3Addresses = H3.getStringRes0Cells(); + String h3Address = RandomPicks.randomFrom(random(), h3Addresses); + for (int i = 1; i <= H3.MAX_H3_RES; i++) { + h3Addresses = H3.h3ToChildren(h3Address); + assertIntersectingChildren(h3Address, h3Addresses); + h3Address = RandomPicks.randomFrom(random(), h3Addresses); + } + } + + private void assertIntersectingChildren(String h3Address, String[] children) { + String[] intersectingNotChildren = H3.h3ToNoChildrenIntersecting(h3Address); + for (String noChild : intersectingNotChildren) { + GeoPolygon p = getGeoPolygon(noChild); + int intersections = 0; + for (String o : children) { + if (p.intersects(getGeoPolygon(o))) { + intersections++; + } + } + assertEquals(2, intersections); + } + } + + private GeoPolygon getGeoPolygon(String h3Address) { + CellBoundary cellBoundary = H3.h3ToGeoBoundary(h3Address); + List points = new ArrayList<>(cellBoundary.numPoints()); + for (int i = 0; i < cellBoundary.numPoints(); i++) { + LatLng latLng = cellBoundary.getLatLon(i); + points.add(new GeoPoint(PlanetModel.SPHERE, latLng.getLatRad(), latLng.getLonRad())); + } + return GeoPolygonFactory.makeGeoPolygon(PlanetModel.SPHERE, points); + } } From b51aa9c22583271db7a7d81240a115693a716ef0 Mon Sep 17 00:00:00 2001 From: Artem Prigoda Date: Tue, 22 Nov 2022 10:31:16 +0100 Subject: [PATCH 024/919] Make DesiredBalanceResponse JSON representation chunked (#91766) DesiredBalanceResponse dumps the cluster's routing table which can be quite large. Let's allow us to stream it to the client in chunks. --- .../allocation/DesiredBalanceResponse.java | 33 +++++++++---------- .../cluster/RestGetDesiredBalanceAction.java | 4 +-- .../DesiredBalanceResponseTests.java | 16 ++++++++- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/allocation/DesiredBalanceResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/allocation/DesiredBalanceResponse.java index a7695f5512fd..9a0f39e8680f 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/allocation/DesiredBalanceResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/allocation/DesiredBalanceResponse.java @@ -11,20 +11,24 @@ import org.elasticsearch.cluster.routing.AllocationId; import org.elasticsearch.cluster.routing.ShardRoutingState; import org.elasticsearch.cluster.routing.allocation.allocator.DesiredBalanceStats; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.core.Nullable; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; -public class DesiredBalanceResponse extends ActionResponse implements ToXContentObject { +public class DesiredBalanceResponse extends ActionResponse implements ChunkedToXContent { private final DesiredBalanceStats stats; private final Map> routingTable; @@ -56,26 +60,21 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - { + public Iterator toXContentChunked() { + return Iterators.concat(Iterators.single((builder, params) -> { + builder.startObject(); builder.startObject("stats"); stats.toXContent(builder, params); builder.endObject(); - } - { - builder.startObject("routing_table"); - for (Map.Entry> indexEntry : routingTable.entrySet()) { - builder.startObject(indexEntry.getKey()); - for (Map.Entry shardEntry : indexEntry.getValue().entrySet()) { - builder.field(String.valueOf(shardEntry.getKey())); - shardEntry.getValue().toXContent(builder, params); - } - builder.endObject(); + return builder.startObject("routing_table"); + }), routingTable.entrySet().stream().map(indexEntry -> (ToXContent) (builder, params) -> { + builder.startObject(indexEntry.getKey()); + for (Map.Entry shardEntry : indexEntry.getValue().entrySet()) { + builder.field(String.valueOf(shardEntry.getKey())); + shardEntry.getValue().toXContent(builder, params); } - builder.endObject(); - } - return builder.endObject(); + return builder.endObject(); + }).iterator(), Iterators.single((builder, params) -> builder.endObject().endObject())); } public DesiredBalanceStats getStats() { diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestGetDesiredBalanceAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestGetDesiredBalanceAction.java index cdc21f2e7c44..11732747b2b3 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestGetDesiredBalanceAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestGetDesiredBalanceAction.java @@ -13,7 +13,7 @@ import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; -import org.elasticsearch.rest.action.RestToXContentListener; +import org.elasticsearch.rest.action.RestChunkedToXContentListener; import java.io.IOException; import java.util.List; @@ -35,7 +35,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli return restChannel -> client.execute( GetDesiredBalanceAction.INSTANCE, new DesiredBalanceRequest(), - new RestToXContentListener<>(restChannel) + new RestChunkedToXContentListener<>(restChannel) ); } } diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/allocation/DesiredBalanceResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/allocation/DesiredBalanceResponseTests.java index 3403a3e244ba..6bc6cecbaf3d 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/allocation/DesiredBalanceResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/allocation/DesiredBalanceResponseTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.cluster.routing.ShardRoutingState; import org.elasticsearch.cluster.routing.allocation.allocator.DesiredBalanceStats; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentFactory; @@ -104,7 +105,9 @@ protected DesiredBalanceResponse mutateInstance(DesiredBalanceResponse instance) public void testToXContent() throws IOException { DesiredBalanceResponse response = new DesiredBalanceResponse(randomStats(), randomRoutingTable()); - Map json = createParser(response.toXContent(XContentFactory.jsonBuilder(), ToXContent.EMPTY_PARAMS)).map(); + Map json = createParser( + ChunkedToXContent.wrapAsXContentObject(response).toXContent(XContentFactory.jsonBuilder(), ToXContent.EMPTY_PARAMS) + ).map(); assertEquals(Set.of("stats", "routing_table"), json.keySet()); Map stats = (Map) json.get("stats"); @@ -152,4 +155,15 @@ public void testToXContent() throws IOException { } } } + + public void testToChunkedXContent() { + DesiredBalanceResponse response = new DesiredBalanceResponse(randomStats(), randomRoutingTable()); + var toXContentChunked = response.toXContentChunked(); + int chunks = 0; + while (toXContentChunked.hasNext()) { + toXContentChunked.next(); + chunks++; + } + assertEquals(response.getRoutingTable().size() + 2, chunks); + } } From 53e500db539a52bb714c9ec7dd27de52032ba69a Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Tue, 22 Nov 2022 10:14:50 +0000 Subject: [PATCH 025/919] Tidy up painless exception strings (#91748) Compress exception messages spread out over lots of lines --- .../lookup/PainlessLookupBuilder.java | 1207 ++++++----------- .../lookup/PainlessLookupUtility.java | 5 +- ...faultConstantFoldingOptimizationPhase.java | 405 ++---- .../phase/DefaultIRTreeToASMBytesPhase.java | 89 +- .../phase/DefaultSemanticAnalysisPhase.java | 238 ++-- .../phase/DefaultSemanticHeaderPhase.java | 39 +- .../phase/PainlessSemanticAnalysisPhase.java | 25 +- .../phase/PainlessSemanticHeaderPhase.java | 2 +- 8 files changed, 733 insertions(+), 1277 deletions(-) diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java index 2d84c9d2baab..db380f69c7c0 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java @@ -9,6 +9,7 @@ package org.elasticsearch.painless.lookup; import org.elasticsearch.common.util.Maps; +import org.elasticsearch.core.Strings; import org.elasticsearch.painless.Def; import org.elasticsearch.painless.spi.Whitelist; import org.elasticsearch.painless.spi.WhitelistClass; @@ -260,6 +261,14 @@ public void addPainlessClass(ClassLoader classLoader, String javaClassName, Map< addPainlessClass(clazz, annotations); } + private static IllegalArgumentException lookupException(String formatText, Object... args) { + return new IllegalArgumentException(Strings.format(formatText, args)); + } + + private static IllegalArgumentException lookupException(Throwable cause, String formatText, Object... args) { + return new IllegalArgumentException(Strings.format(formatText, args), cause); + } + public void addPainlessClass(Class clazz, Map, Object> annotations) { Objects.requireNonNull(clazz); Objects.requireNonNull(annotations); @@ -283,22 +292,18 @@ public void addPainlessClass(Class clazz, Map, Object> annotations) if (existingClass == null) { javaClassNamesToClasses.put(clazz.getName().intern(), clazz); } else if (existingClass != clazz) { - throw new IllegalArgumentException( - "class [" - + canonicalClassName - + "] " - + "cannot represent multiple java classes with the same name from different class loaders" + throw lookupException( + "class [%s] cannot represent multiple java classes with the same name from different class loaders", + canonicalClassName ); } existingClass = canonicalClassNamesToClasses.get(canonicalClassName); if (existingClass != null && existingClass != clazz) { - throw new IllegalArgumentException( - "class [" - + canonicalClassName - + "] " - + "cannot represent multiple java classes with the same name from different class loaders" + throw lookupException( + "class [%s] cannot represent multiple java classes with the same name from different class loaders", + canonicalClassName ); } @@ -333,22 +338,16 @@ public void addPainlessClass(Class clazz, Map, Object> annotations) if (annotations.get(AliasAnnotation.class)instanceof AliasAnnotation alias) { Class existing = canonicalClassNamesToClasses.put(alias.alias(), clazz); if (existing != null) { - throw new IllegalArgumentException( - "Cannot add alias [" + alias.alias() + "] for [" + clazz + "] that shadows class [" + existing + "]" - ); + throw lookupException("Cannot add alias [%s] for [%s] that shadows class [%s]", alias.alias(), clazz, existing); } } } } else if (importedClass != clazz) { - throw new IllegalArgumentException( - "imported class [" - + importedCanonicalClassName - + "] cannot represent multiple " - + "classes [" - + canonicalClassName - + "] and [" - + typeToCanonicalTypeName(importedClass) - + "]" + throw lookupException( + "imported class [%s] cannot represent multiple classes [%s] and [%s]", + importedCanonicalClassName, + canonicalClassName, + typeToCanonicalTypeName(importedClass) ); } else if (importClassName == false) { throw new IllegalArgumentException("inconsistent no_import parameter found for class [" + canonicalClassName + "]"); @@ -367,15 +366,11 @@ public void addPainlessConstructor( Class targetClass = canonicalClassNamesToClasses.get(targetCanonicalClassName); if (targetClass == null) { - throw new IllegalArgumentException( - "target class [" - + targetCanonicalClassName - + "] not found" - + "for constructor [[" - + targetCanonicalClassName - + "], " - + canonicalTypeNameParameters - + "]" + throw lookupException( + "target class [%s] not found for constructor [[%s], %s]", + targetCanonicalClassName, + targetCanonicalClassName, + canonicalTypeNameParameters ); } @@ -385,15 +380,11 @@ public void addPainlessConstructor( Class typeParameter = canonicalTypeNameToType(canonicalTypeNameParameter); if (typeParameter == null) { - throw new IllegalArgumentException( - "type parameter [" - + canonicalTypeNameParameter - + "] not found " - + "for constructor [[" - + targetCanonicalClassName - + "], " - + canonicalTypeNameParameters - + "]" + throw lookupException( + "type parameter [%s] not found for constructor [[%s], %s]", + canonicalTypeNameParameter, + targetCanonicalClassName, + canonicalTypeNameParameters ); } @@ -415,15 +406,11 @@ public void addPainlessConstructor(Class targetClass, List> typePara PainlessClassBuilder painlessClassBuilder = classesToPainlessClassBuilders.get(targetClass); if (painlessClassBuilder == null) { - throw new IllegalArgumentException( - "target class [" - + targetCanonicalClassName - + "] not found" - + "for constructor [[" - + targetCanonicalClassName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" + throw lookupException( + "target class [%s] not found for constructor [[%s], %s]", + targetCanonicalClassName, + targetCanonicalClassName, + typesToCanonicalTypeNames(typeParameters) ); } @@ -432,15 +419,11 @@ public void addPainlessConstructor(Class targetClass, List> typePara for (Class typeParameter : typeParameters) { if (isValidType(typeParameter) == false) { - throw new IllegalArgumentException( - "type parameter [" - + typeToCanonicalTypeName(typeParameter) - + "] not found " - + "for constructor [[" - + targetCanonicalClassName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" + throw lookupException( + "type parameter [%s] not found for constructor [[%s], %s]", + typeToCanonicalTypeName(typeParameter), + targetCanonicalClassName, + typesToCanonicalTypeNames(typeParameters) ); } @@ -452,14 +435,11 @@ public void addPainlessConstructor(Class targetClass, List> typePara try { javaConstructor = targetClass.getConstructor(javaTypeParameters.toArray(Class[]::new)); } catch (NoSuchMethodException nsme) { - throw new IllegalArgumentException( - "reflection object not found for constructor " - + "[[" - + targetCanonicalClassName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]", - nsme + throw lookupException( + nsme, + "reflection object not found for constructor [[%s], %s]", + targetCanonicalClassName, + typesToCanonicalTypeNames(typeParameters) ); } @@ -468,14 +448,11 @@ public void addPainlessConstructor(Class targetClass, List> typePara try { methodHandle = lookup(targetClass).unreflectConstructor(javaConstructor); } catch (IllegalAccessException iae) { - throw new IllegalArgumentException( - "method handle not found for constructor " - + "[[" - + targetCanonicalClassName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]", - iae + throw lookupException( + iae, + "method handle not found for constructor [[%s], %s]", + targetCanonicalClassName, + typesToCanonicalTypeNames(typeParameters) ); } @@ -499,18 +476,12 @@ public void addPainlessConstructor(Class targetClass, List> typePara newPainlessConstructor = painlessConstructorCache.computeIfAbsent(newPainlessConstructor, Function.identity()); painlessClassBuilder.constructors.put(painlessConstructorKey.intern(), newPainlessConstructor); } else if (newPainlessConstructor.equals(existingPainlessConstructor) == false) { - throw new IllegalArgumentException( - "cannot add constructors with the same arity but are not equivalent for constructors " - + "[[" - + targetCanonicalClassName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "] and " - + "[[" - + targetCanonicalClassName - + "], " - + typesToCanonicalTypeNames(existingPainlessConstructor.typeParameters()) - + "]" + throw lookupException( + "cannot add constructors with the same arity but are not equivalent for constructors [[%s], %s] and [[%s], %s]", + targetCanonicalClassName, + typesToCanonicalTypeNames(typeParameters), + targetCanonicalClassName, + typesToCanonicalTypeNames(existingPainlessConstructor.typeParameters()) ); } } @@ -535,17 +506,12 @@ public void addPainlessMethod( Class targetClass = canonicalClassNamesToClasses.get(targetCanonicalClassName); if (targetClass == null) { - throw new IllegalArgumentException( - "target class [" - + targetCanonicalClassName - + "] not found for method " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + canonicalTypeNameParameters - + "]" + throw lookupException( + "target class [%s] not found for method [[%s], [%s], %s]", + targetCanonicalClassName, + targetCanonicalClassName, + methodName, + canonicalTypeNameParameters ); } @@ -555,16 +521,13 @@ public void addPainlessMethod( augmentedClass = loadClass( classLoader, augmentedCanonicalClassName, - () -> "augmented class [" - + augmentedCanonicalClassName - + "] not found for method " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + canonicalTypeNameParameters - + "]" + () -> Strings.format( + "augmented class [%s] not found for method [[%s], [%s], %s]", + augmentedCanonicalClassName, + targetCanonicalClassName, + methodName, + canonicalTypeNameParameters + ) ); } @@ -574,17 +537,12 @@ public void addPainlessMethod( Class typeParameter = canonicalTypeNameToType(canonicalTypeNameParameter); if (typeParameter == null) { - throw new IllegalArgumentException( - "type parameter [" - + canonicalTypeNameParameter - + "] not found for method " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + canonicalTypeNameParameters - + "]" + throw lookupException( + "type parameter [%s] not found for method [[%s], [%s], %s]", + canonicalTypeNameParameter, + targetCanonicalClassName, + methodName, + canonicalTypeNameParameters ); } @@ -594,17 +552,12 @@ public void addPainlessMethod( Class returnType = canonicalTypeNameToType(returnCanonicalTypeName); if (returnType == null) { - throw new IllegalArgumentException( - "return type [" - + returnCanonicalTypeName - + "] not found for method " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + canonicalTypeNameParameters - + "]" + throw lookupException( + "return type [%s] not found for method [[%s], [%s], %s]", + returnCanonicalTypeName, + targetCanonicalClassName, + methodName, + canonicalTypeNameParameters ); } @@ -641,17 +594,12 @@ public void addPainlessMethod( PainlessClassBuilder painlessClassBuilder = classesToPainlessClassBuilders.get(targetClass); if (painlessClassBuilder == null) { - throw new IllegalArgumentException( - "target class [" - + targetCanonicalClassName - + "] not found for method " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" + throw lookupException( + "target class [%s] not found for method [[%s], [%s], %s]", + targetCanonicalClassName, + targetCanonicalClassName, + methodName, + typesToCanonicalTypeNames(typeParameters) ); } @@ -665,17 +613,12 @@ public void addPainlessMethod( for (Class typeParameter : typeParameters) { if (isValidType(typeParameter) == false) { - throw new IllegalArgumentException( - "type parameter [" - + typeToCanonicalTypeName(typeParameter) - + "] " - + "not found for method [[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" + throw lookupException( + "type parameter [%s] not found for method [[%s], [%s], %s]", + typeToCanonicalTypeName(typeParameter), + targetCanonicalClassName, + methodName, + typesToCanonicalTypeNames(typeParameters) ); } @@ -683,17 +626,12 @@ public void addPainlessMethod( } if (isValidType(returnType) == false) { - throw new IllegalArgumentException( - "return type [" - + typeToCanonicalTypeName(returnType) - + "] not found for method " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" + throw lookupException( + "return type [%s] not found for method [[%s], [%s], %s]", + typeToCanonicalTypeName(returnType), + targetCanonicalClassName, + methodName, + typesToCanonicalTypeNames(typeParameters) ); } @@ -703,16 +641,12 @@ public void addPainlessMethod( try { javaMethod = targetClass.getMethod(methodName, javaTypeParameters.toArray(Class[]::new)); } catch (NoSuchMethodException nsme) { - throw new IllegalArgumentException( - "reflection object not found for method [[" - + targetCanonicalClassName - + "], " - + "[" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]", - nsme + throw lookupException( + nsme, + "reflection object not found for method [[%s], [%s], %s]", + targetCanonicalClassName, + methodName, + typesToCanonicalTypeNames(typeParameters) ); } } else { @@ -720,33 +654,22 @@ public void addPainlessMethod( javaMethod = augmentedClass.getMethod(methodName, javaTypeParameters.toArray(Class[]::new)); if (Modifier.isStatic(javaMethod.getModifiers()) == false) { - throw new IllegalArgumentException( - "method [[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "] with augmented class " - + "[" - + typeToCanonicalTypeName(augmentedClass) - + "] must be static" + throw lookupException( + "method [[%s], [%s], %s] with augmented class [%s] must be static", + targetCanonicalClassName, + methodName, + typesToCanonicalTypeNames(typeParameters), + typeToCanonicalTypeName(augmentedClass) ); } } catch (NoSuchMethodException nsme) { - throw new IllegalArgumentException( - "reflection object not found for method " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "] " - + "with augmented class [" - + typeToCanonicalTypeName(augmentedClass) - + "]", - nsme + throw lookupException( + nsme, + "reflection object not found for method [[%s], [%s], %s] with augmented class [%s]", + targetCanonicalClassName, + methodName, + typesToCanonicalTypeNames(typeParameters), + typeToCanonicalTypeName(augmentedClass) ); } } @@ -764,20 +687,13 @@ public void addPainlessMethod( } if (javaMethod.getReturnType() != typeToJavaType(returnType)) { - throw new IllegalArgumentException( - "return type [" - + typeToCanonicalTypeName(javaMethod.getReturnType()) - + "] " - + "does not match the specified returned type [" - + typeToCanonicalTypeName(returnType) - + "] " - + "for method [[" - + targetClass.getCanonicalName() - + "], [" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" + throw lookupException( + "return type [%s] does not match the specified returned type [%s] for method [[%s], [%s], %s]", + typeToCanonicalTypeName(javaMethod.getReturnType()), + typeToCanonicalTypeName(returnType), + targetClass.getCanonicalName(), + methodName, + typesToCanonicalTypeNames(typeParameters) ); } @@ -787,38 +703,26 @@ public void addPainlessMethod( try { methodHandle = lookup(targetClass).unreflect(javaMethod); } catch (IllegalAccessException iae) { - throw new IllegalArgumentException( - "method handle not found for method " - + "[[" - + targetClass.getCanonicalName() - + "], [" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "], " - + "with lookup [" - + lookup(targetClass) - + "]", - iae + throw lookupException( + iae, + "method handle not found for method [[%s], [%s], %s], with lookup [%s]", + targetClass.getCanonicalName(), + methodName, + typesToCanonicalTypeNames(typeParameters), + lookup(targetClass) ); } } else { try { methodHandle = lookup(augmentedClass).unreflect(javaMethod); } catch (IllegalAccessException iae) { - throw new IllegalArgumentException( - "method handle not found for method " - + "[[" - + targetClass.getCanonicalName() - + "], [" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" - + "with augmented class [" - + typeToCanonicalTypeName(augmentedClass) - + "]", - iae + throw lookupException( + iae, + "method handle not found for method [[%s], [%s], %s] with augmented class [%s]", + targetClass.getCanonicalName(), + methodName, + typesToCanonicalTypeNames(typeParameters), + typeToCanonicalTypeName(augmentedClass) ); } } @@ -852,28 +756,17 @@ public void addPainlessMethod( painlessClassBuilder.methods.put(painlessMethodKey.intern(), newPainlessMethod); } } else if (newPainlessMethod.equals(existingPainlessMethod) == false) { - throw new IllegalArgumentException( + throw lookupException( "cannot add methods with the same name and arity but are not equivalent for methods " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + "[" - + typeToCanonicalTypeName(returnType) - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "] and " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + "[" - + typeToCanonicalTypeName(existingPainlessMethod.returnType()) - + "], " - + typesToCanonicalTypeNames(existingPainlessMethod.typeParameters()) - + "]" + + "[[%s], [%s], [%s], %s] and [[%s], [%s], [%s], %s]", + targetCanonicalClassName, + methodName, + typeToCanonicalTypeName(returnType), + typesToCanonicalTypeNames(typeParameters), + targetCanonicalClassName, + methodName, + typeToCanonicalTypeName(existingPainlessMethod.returnType()), + typesToCanonicalTypeNames(existingPainlessMethod.typeParameters()) ); } } @@ -895,17 +788,12 @@ public void addPainlessField( Class targetClass = canonicalClassNamesToClasses.get(targetCanonicalClassName); if (targetClass == null) { - throw new IllegalArgumentException( - "target class [" - + targetCanonicalClassName - + "] not found for field " - + "[[" - + targetCanonicalClassName - + "], [" - + fieldName - + "], [" - + canonicalTypeNameParameter - + "]]" + throw lookupException( + "target class [%s] not found for field [[%s], [%s], [%s]]", + targetCanonicalClassName, + targetCanonicalClassName, + fieldName, + canonicalTypeNameParameter ); } @@ -919,29 +807,23 @@ public void addPainlessField( augmentedClass = loadClass( classLoader, augmentedCanonicalClassName, - () -> "augmented class [" - + augmentedCanonicalClassName - + "] not found for field " - + "[[" - + targetCanonicalClassName - + "], [" - + fieldName - + "]" + () -> Strings.format( + "augmented class [%s] not found for field [[%s], [%s]]", + augmentedCanonicalClassName, + targetCanonicalClassName, + fieldName + ) ); } Class typeParameter = canonicalTypeNameToType(canonicalTypeNameParameter); if (typeParameter == null) { - throw new IllegalArgumentException( - "type parameter [" - + canonicalTypeNameParameter - + "] not found " - + "for field [[" - + targetCanonicalClassName - + "], [" - + fieldName - + "]" + throw lookupException( + "type parameter [%s] not found for field [[%s], [%s]]", + canonicalTypeNameParameter, + targetCanonicalClassName, + fieldName ); } @@ -976,32 +858,22 @@ public void addPainlessField( PainlessClassBuilder painlessClassBuilder = classesToPainlessClassBuilders.get(targetClass); if (painlessClassBuilder == null) { - throw new IllegalArgumentException( - "target class [" - + targetCanonicalClassName - + "] not found for field " - + "[[" - + targetCanonicalClassName - + "], [" - + fieldName - + "], [" - + typeToCanonicalTypeName(typeParameter) - + "]]" + throw lookupException( + "target class [%s] not found for field [[%s], [%s], [%s]]", + targetCanonicalClassName, + targetCanonicalClassName, + fieldName, + typeToCanonicalTypeName(typeParameter) ); } if (isValidType(typeParameter) == false) { - throw new IllegalArgumentException( - "type parameter [" - + typeToCanonicalTypeName(typeParameter) - + "] not found for field " - + "[[" - + targetCanonicalClassName - + "], [" - + fieldName - + "], [" - + typeToCanonicalTypeName(typeParameter) - + "]]" + throw lookupException( + "type parameter [%s] not found for field [[%s], [%s], [%s]]", + typeToCanonicalTypeName(typeParameter), + targetCanonicalClassName, + fieldName, + typeToCanonicalTypeName(typeParameter) ); } @@ -1011,16 +883,12 @@ public void addPainlessField( try { javaField = targetClass.getField(fieldName); } catch (NoSuchFieldException nsfe) { - throw new IllegalArgumentException( - "reflection object not found for field " - + "[[" - + targetCanonicalClassName - + "], [" - + fieldName - + "], [" - + typeToCanonicalTypeName(typeParameter) - + "]]", - nsfe + throw lookupException( + nsfe, + "reflection object not found for field [[%s], [%s], [%s]]", + targetCanonicalClassName, + fieldName, + typeToCanonicalTypeName(typeParameter) ); } } else { @@ -1028,48 +896,32 @@ public void addPainlessField( javaField = augmentedClass.getField(fieldName); if (Modifier.isStatic(javaField.getModifiers()) == false || Modifier.isFinal(javaField.getModifiers()) == false) { - throw new IllegalArgumentException( - "field [[" - + targetCanonicalClassName - + "], [" - + fieldName - + "] " - + "with augmented class [" - + typeToCanonicalTypeName(augmentedClass) - + "] must be static and final" + throw lookupException( + "field [[%s], [%s]] with augmented class [%s] must be static and final", + targetCanonicalClassName, + fieldName, + typeToCanonicalTypeName(augmentedClass) ); } } catch (NoSuchFieldException nsfe) { - throw new IllegalArgumentException( - "reflection object not found for field " - + "[[" - + targetCanonicalClassName - + "], [" - + fieldName - + "], [" - + typeToCanonicalTypeName(typeParameter) - + "]]" - + "with augmented class [" - + typeToCanonicalTypeName(augmentedClass) - + "]", - nsfe + throw lookupException( + nsfe, + "reflection object not found for field [[%s], [%s], [%s]] with augmented class [%s]", + targetCanonicalClassName, + fieldName, + typeToCanonicalTypeName(typeParameter), + typeToCanonicalTypeName(augmentedClass) ); } } if (javaField.getType() != typeToJavaType(typeParameter)) { - throw new IllegalArgumentException( - "type parameter [" - + typeToCanonicalTypeName(javaField.getType()) - + "] " - + "does not match the specified type parameter [" - + typeToCanonicalTypeName(typeParameter) - + "] " - + "for field [[" - + targetCanonicalClassName - + "], [" - + fieldName - + "]" + throw lookupException( + "type parameter [%s] does not match the specified type parameter [%s] for field [[%s], [%s]]", + typeToCanonicalTypeName(javaField.getType()), + typeToCanonicalTypeName(typeParameter), + targetCanonicalClassName, + fieldName ); } @@ -1094,26 +946,18 @@ public void addPainlessField( PainlessField newPainlessField = new PainlessField(javaField, typeParameter, annotations, methodHandleGetter, null); if (existingPainlessField == null) { - newPainlessField = painlessFieldCache.computeIfAbsent(newPainlessField, key -> key); + newPainlessField = painlessFieldCache.computeIfAbsent(newPainlessField, Function.identity()); painlessClassBuilder.staticFields.put(painlessFieldKey.intern(), newPainlessField); } else if (newPainlessField.equals(existingPainlessField) == false) { - throw new IllegalArgumentException( - "cannot add fields with the same name but are not equivalent for fields " - + "[[" - + targetCanonicalClassName - + "], [" - + fieldName - + "], [" - + typeToCanonicalTypeName(typeParameter) - + "] and " - + "[[" - + targetCanonicalClassName - + "], [" - + existingPainlessField.javaField().getName() - + "], " - + typeToCanonicalTypeName(existingPainlessField.typeParameter()) - + "] " - + "with the same name and different type parameters" + throw lookupException( + "cannot add fields with the same name but are not equivalent for fields [[%s], [%s], [%s]] and [[%s], [%s], [%s]]" + + " with the same name and different type parameters", + targetCanonicalClassName, + fieldName, + typeToCanonicalTypeName(typeParameter), + targetCanonicalClassName, + existingPainlessField.javaField().getName(), + typeToCanonicalTypeName(existingPainlessField.typeParameter()) ); } } else { @@ -1140,23 +984,15 @@ public void addPainlessField( newPainlessField = painlessFieldCache.computeIfAbsent(newPainlessField, key -> key); painlessClassBuilder.fields.put(painlessFieldKey.intern(), newPainlessField); } else if (newPainlessField.equals(existingPainlessField) == false) { - throw new IllegalArgumentException( - "cannot add fields with the same name but are not equivalent for fields " - + "[[" - + targetCanonicalClassName - + "], [" - + fieldName - + "], [" - + typeToCanonicalTypeName(typeParameter) - + "] and " - + "[[" - + targetCanonicalClassName - + "], [" - + existingPainlessField.javaField().getName() - + "], " - + typeToCanonicalTypeName(existingPainlessField.typeParameter()) - + "] " - + "with the same name and different type parameters" + throw lookupException( + "cannot add fields with the same name but are not equivalent for fields [[%s], [%s], [%s]] and [[%s], [%s], [%s]]" + + " with the same name and different type parameters", + targetCanonicalClassName, + fieldName, + typeToCanonicalTypeName(typeParameter), + targetCanonicalClassName, + existingPainlessField.javaField().getName(), + typeToCanonicalTypeName(existingPainlessField.typeParameter()) ); } } @@ -1180,38 +1016,18 @@ public void addImportedPainlessMethod( Class targetClass = loadClass(classLoader, targetJavaClassName, () -> "class [" + targetJavaClassName + "] not found"); String targetCanonicalClassName = typeToCanonicalTypeName(targetClass); - if (targetClass == null) { - throw new IllegalArgumentException( - "target class [" - + targetCanonicalClassName - + "] not found for imported method " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + canonicalTypeNameParameters - + "]" - ); - } - List> typeParameters = new ArrayList<>(canonicalTypeNameParameters.size()); for (String canonicalTypeNameParameter : canonicalTypeNameParameters) { Class typeParameter = canonicalTypeNameToType(canonicalTypeNameParameter); if (typeParameter == null) { - throw new IllegalArgumentException( - "type parameter [" - + canonicalTypeNameParameter - + "] not found for imported method " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + canonicalTypeNameParameters - + "]" + throw lookupException( + "type parameter [%s] not found for imported method [[%s], [%s], %s]", + canonicalTypeNameParameter, + targetCanonicalClassName, + methodName, + canonicalTypeNameParameters ); } @@ -1221,17 +1037,12 @@ public void addImportedPainlessMethod( Class returnType = canonicalTypeNameToType(returnCanonicalTypeName); if (returnType == null) { - throw new IllegalArgumentException( - "return type [" - + returnCanonicalTypeName - + "] not found for imported method " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + canonicalTypeNameParameters - + "]" + throw lookupException( + "return type [%s] not found for imported method [[%s], [%s], %s]", + returnCanonicalTypeName, + targetCanonicalClassName, + methodName, + canonicalTypeNameParameters ); } @@ -1260,11 +1071,9 @@ public void addImportedPainlessMethod( if (existingTargetClass == null) { javaClassNamesToClasses.put(targetClass.getName().intern(), targetClass); } else if (existingTargetClass != targetClass) { - throw new IllegalArgumentException( - "class [" - + targetCanonicalClassName - + "] " - + "cannot represent multiple java classes with the same name from different class loaders" + throw lookupException( + "class [%s] cannot represent multiple java classes with the same name from different class loaders", + targetCanonicalClassName ); } @@ -1279,17 +1088,12 @@ public void addImportedPainlessMethod( for (Class typeParameter : typeParameters) { if (isValidType(typeParameter) == false) { - throw new IllegalArgumentException( - "type parameter [" - + typeToCanonicalTypeName(typeParameter) - + "] " - + "not found for imported method [[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" + throw lookupException( + "type parameter [%s] not found for imported method [[%s], [%s], %s]", + typeToCanonicalTypeName(typeParameter), + targetCanonicalClassName, + methodName, + typesToCanonicalTypeNames(typeParameters) ); } @@ -1297,17 +1101,12 @@ public void addImportedPainlessMethod( } if (isValidType(returnType) == false) { - throw new IllegalArgumentException( - "return type [" - + typeToCanonicalTypeName(returnType) - + "] not found for imported method " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" + throw lookupException( + "return type [%s] not found for imported method [[%s], [%s], %s]", + typeToCanonicalTypeName(returnType), + targetCanonicalClassName, + methodName, + typesToCanonicalTypeNames(typeParameters) ); } @@ -1316,46 +1115,32 @@ public void addImportedPainlessMethod( try { javaMethod = targetClass.getMethod(methodName, javaTypeParameters.toArray(new Class[typeParametersSize])); } catch (NoSuchMethodException nsme) { - throw new IllegalArgumentException( - "imported method reflection object [[" - + targetCanonicalClassName - + "], " - + "[" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "] not found", - nsme + throw lookupException( + nsme, + "imported method reflection object [[%s], [%s], %s] not found", + targetCanonicalClassName, + methodName, + typesToCanonicalTypeNames(typeParameters) ); } if (javaMethod.getReturnType() != typeToJavaType(returnType)) { - throw new IllegalArgumentException( - "return type [" - + typeToCanonicalTypeName(javaMethod.getReturnType()) - + "] " - + "does not match the specified returned type [" - + typeToCanonicalTypeName(returnType) - + "] " - + "for imported method [[" - + targetClass.getCanonicalName() - + "], [" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" + throw lookupException( + "return type [%s] does not match the specified returned type [%s] for imported method [[%s], [%s], %s]", + typeToCanonicalTypeName(javaMethod.getReturnType()), + typeToCanonicalTypeName(returnType), + targetClass.getCanonicalName(), + methodName, + typesToCanonicalTypeNames(typeParameters) ); } if (Modifier.isStatic(javaMethod.getModifiers()) == false) { - throw new IllegalArgumentException( - "imported method [[" - + targetClass.getCanonicalName() - + "], [" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "] must be static" + throw lookupException( + "imported method [[%s], [%s], %s] must be static", + targetClass.getCanonicalName(), + methodName, + typesToCanonicalTypeNames(typeParameters) ); } @@ -1374,16 +1159,12 @@ public void addImportedPainlessMethod( try { methodHandle = lookup(targetClass).unreflect(javaMethod); } catch (IllegalAccessException iae) { - throw new IllegalArgumentException( - "imported method handle [[" - + targetClass.getCanonicalName() - + "], " - + "[" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "] not found", - iae + throw lookupException( + iae, + "imported method handle [[%s], [%s], %s] not found", + targetClass.getCanonicalName(), + methodName, + typesToCanonicalTypeNames(typeParameters) ); } @@ -1404,29 +1185,17 @@ public void addImportedPainlessMethod( newImportedPainlessMethod = painlessMethodCache.computeIfAbsent(newImportedPainlessMethod, key -> key); painlessMethodKeysToImportedPainlessMethods.put(painlessMethodKey.intern(), newImportedPainlessMethod); } else if (newImportedPainlessMethod.equals(existingImportedPainlessMethod) == false) { - throw new IllegalArgumentException( - "cannot add imported methods with the same name and arity " - + "but do not have equivalent methods " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + "[" - + typeToCanonicalTypeName(returnType) - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "] and " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + "[" - + typeToCanonicalTypeName(existingImportedPainlessMethod.returnType()) - + "], " - + typesToCanonicalTypeNames(existingImportedPainlessMethod.typeParameters()) - + "]" + throw lookupException( + "cannot add imported methods with the same name and arity but do not have equivalent methods " + + "[[%s], [%s], [%s], %s] and [[%s], [%s], [%s], %s]", + targetCanonicalClassName, + methodName, + typeToCanonicalTypeName(returnType), + typesToCanonicalTypeNames(typeParameters), + targetCanonicalClassName, + methodName, + typeToCanonicalTypeName(existingImportedPainlessMethod.returnType()), + typesToCanonicalTypeNames(existingImportedPainlessMethod.typeParameters()) ); } } @@ -1454,17 +1223,12 @@ public void addPainlessClassBinding( Class typeParameter = canonicalTypeNameToType(canonicalTypeNameParameter); if (typeParameter == null) { - throw new IllegalArgumentException( - "type parameter [" - + canonicalTypeNameParameter - + "] not found for class binding " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + canonicalTypeNameParameters - + "]" + throw lookupException( + "type parameter [%s] not found for class binding [[%s], [%s], %s]", + canonicalTypeNameParameter, + targetCanonicalClassName, + methodName, + canonicalTypeNameParameters ); } @@ -1474,17 +1238,12 @@ public void addPainlessClassBinding( Class returnType = canonicalTypeNameToType(returnCanonicalTypeName); if (returnType == null) { - throw new IllegalArgumentException( - "return type [" - + returnCanonicalTypeName - + "] not found for class binding " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + canonicalTypeNameParameters - + "]" + throw lookupException( + "return type [%s] not found for class binding [[%s], [%s], %s]", + returnCanonicalTypeName, + targetCanonicalClassName, + methodName, + canonicalTypeNameParameters ); } @@ -1513,11 +1272,9 @@ public void addPainlessClassBinding( if (existingTargetClass == null) { javaClassNamesToClasses.put(targetClass.getName().intern(), targetClass); } else if (existingTargetClass != targetClass) { - throw new IllegalArgumentException( - "class [" - + targetCanonicalClassName - + "] " - + "cannot represent multiple java classes with the same name from different class loaders" + throw lookupException( + "class [%s] cannot represent multiple java classes with the same name from different class loaders", + targetCanonicalClassName ); } @@ -1546,46 +1303,32 @@ public void addPainlessClassBinding( Class typeParameter = typeParameters.get(typeParameterIndex); if (isValidType(typeParameter) == false) { - throw new IllegalArgumentException( - "type parameter [" - + typeToCanonicalTypeName(typeParameter) - + "] not found " - + "for class binding [[" - + targetCanonicalClassName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" + throw lookupException( + "type parameter [%s] not found for class binding [[%s], %s]", + typeToCanonicalTypeName(typeParameter), + targetCanonicalClassName, + typesToCanonicalTypeNames(typeParameters) ); } Class javaTypeParameter = constructorParameterTypes[typeParameterIndex]; if (isValidType(javaTypeParameter) == false) { - throw new IllegalArgumentException( - "type parameter [" - + typeToCanonicalTypeName(typeParameter) - + "] not found " - + "for class binding [[" - + targetCanonicalClassName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" + throw lookupException( + "type parameter [%s] not found for class binding [[%s], %s]", + typeToCanonicalTypeName(typeParameter), + targetCanonicalClassName, + typesToCanonicalTypeNames(typeParameters) ); } if (javaTypeParameter != typeToJavaType(typeParameter)) { - throw new IllegalArgumentException( - "type parameter [" - + typeToCanonicalTypeName(javaTypeParameter) - + "] " - + "does not match the specified type parameter [" - + typeToCanonicalTypeName(typeParameter) - + "] " - + "for class binding [[" - + targetClass.getCanonicalName() - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" + throw lookupException( + "type parameter [%s] does not match the specified type parameter [%s] for class binding [[%s], %s]", + typeToCanonicalTypeName(javaTypeParameter), + typeToCanonicalTypeName(typeParameter), + targetClass.getCanonicalName(), + typesToCanonicalTypeNames(typeParameters) ); } } @@ -1623,80 +1366,54 @@ public void addPainlessClassBinding( Class typeParameter = typeParameters.get(constructorParameterTypes.length + typeParameterIndex); if (isValidType(typeParameter) == false) { - throw new IllegalArgumentException( - "type parameter [" - + typeToCanonicalTypeName(typeParameter) - + "] not found " - + "for class binding [[" - + targetCanonicalClassName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" + throw lookupException( + "type parameter [%s] not found for class binding [[%s], %s]", + typeToCanonicalTypeName(typeParameter), + targetCanonicalClassName, + typesToCanonicalTypeNames(typeParameters) ); } Class javaTypeParameter = javaMethod.getParameterTypes()[typeParameterIndex]; if (isValidType(javaTypeParameter) == false) { - throw new IllegalArgumentException( - "type parameter [" - + typeToCanonicalTypeName(typeParameter) - + "] not found " - + "for class binding [[" - + targetCanonicalClassName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" + throw lookupException( + "type parameter [%s] not found for class binding [[%s], %s]", + typeToCanonicalTypeName(typeParameter), + targetCanonicalClassName, + typesToCanonicalTypeNames(typeParameters) ); } if (javaTypeParameter != typeToJavaType(typeParameter)) { - throw new IllegalArgumentException( - "type parameter [" - + typeToCanonicalTypeName(javaTypeParameter) - + "] " - + "does not match the specified type parameter [" - + typeToCanonicalTypeName(typeParameter) - + "] " - + "for class binding [[" - + targetClass.getCanonicalName() - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" + throw lookupException( + "type parameter [%s] does not match the specified type parameter [%s] for class binding [[%s], %s]", + typeToCanonicalTypeName(javaTypeParameter), + typeToCanonicalTypeName(typeParameter), + targetClass.getCanonicalName(), + typesToCanonicalTypeNames(typeParameters) ); } } if (isValidType(returnType) == false) { - throw new IllegalArgumentException( - "return type [" - + typeToCanonicalTypeName(returnType) - + "] not found for class binding " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" + throw lookupException( + "return type [%s] not found for class binding [[%s], [%s], %s]", + typeToCanonicalTypeName(returnType), + targetCanonicalClassName, + methodName, + typesToCanonicalTypeNames(typeParameters) ); } if (javaMethod.getReturnType() != typeToJavaType(returnType)) { - throw new IllegalArgumentException( - "return type [" - + typeToCanonicalTypeName(javaMethod.getReturnType()) - + "] " - + "does not match the specified returned type [" - + typeToCanonicalTypeName(returnType) - + "] " - + "for class binding [[" - + targetClass.getCanonicalName() - + "], [" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" + throw lookupException( + "return type [%s] does not match the specified returned type [%s] for class binding [[%s], [%s], %s]", + typeToCanonicalTypeName(javaMethod.getReturnType()), + typeToCanonicalTypeName(returnType), + targetClass.getCanonicalName(), + methodName, + typesToCanonicalTypeNames(typeParameters) ); } @@ -1711,14 +1428,11 @@ public void addPainlessClassBinding( } if (Modifier.isStatic(javaMethod.getModifiers())) { - throw new IllegalArgumentException( - "class binding [[" - + targetClass.getCanonicalName() - + "], [" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "] cannot be static" + throw lookupException( + "class binding [[%s], [%s], %s] cannot be static", + targetClass.getCanonicalName(), + methodName, + typesToCanonicalTypeNames(typeParameters) ); } @@ -1735,31 +1449,17 @@ public void addPainlessClassBinding( newPainlessClassBinding = painlessClassBindingCache.computeIfAbsent(newPainlessClassBinding, Function.identity()); painlessMethodKeysToPainlessClassBindings.put(painlessMethodKey.intern(), newPainlessClassBinding); } else if (newPainlessClassBinding.equals(existingPainlessClassBinding) == false) { - throw new IllegalArgumentException( - "cannot add class bindings with the same name and arity " - + "but do not have equivalent methods " - + "[[" - + targetCanonicalClassName - + "], " - + "[" - + methodName - + "], " - + "[" - + typeToCanonicalTypeName(returnType) - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "] and " - + "[[" - + targetCanonicalClassName - + "], " - + "[" - + methodName - + "], " - + "[" - + typeToCanonicalTypeName(existingPainlessClassBinding.returnType()) - + "], " - + typesToCanonicalTypeNames(existingPainlessClassBinding.typeParameters()) - + "]" + throw lookupException( + "cannot add class bindings with the same name and arity but do not have equivalent methods " + + "[[%s], [%s], [%s], %s] and [[%s], [%s], [%s], %s]", + targetCanonicalClassName, + methodName, + typeToCanonicalTypeName(returnType), + typesToCanonicalTypeNames(typeParameters), + targetCanonicalClassName, + methodName, + typeToCanonicalTypeName(existingPainlessClassBinding.returnType()), + typesToCanonicalTypeNames(existingPainlessClassBinding.typeParameters()) ); } } @@ -1785,17 +1485,12 @@ public void addPainlessInstanceBinding( Class typeParameter = canonicalTypeNameToType(canonicalTypeNameParameter); if (typeParameter == null) { - throw new IllegalArgumentException( - "type parameter [" - + canonicalTypeNameParameter - + "] not found for instance binding " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + canonicalTypeNameParameters - + "]" + throw lookupException( + "type parameter [%s] not found for instance binding [[%s], [%s], %s]", + canonicalTypeNameParameter, + targetCanonicalClassName, + methodName, + canonicalTypeNameParameters ); } @@ -1805,17 +1500,12 @@ public void addPainlessInstanceBinding( Class returnType = canonicalTypeNameToType(returnCanonicalTypeName); if (returnType == null) { - throw new IllegalArgumentException( - "return type [" - + returnCanonicalTypeName - + "] not found for class binding " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + canonicalTypeNameParameters - + "]" + throw lookupException( + "return type [%s] not found for class binding [[%s], [%s], %s]", + returnCanonicalTypeName, + targetCanonicalClassName, + methodName, + canonicalTypeNameParameters ); } @@ -1846,11 +1536,9 @@ public void addPainlessInstanceBinding( if (existingTargetClass == null) { javaClassNamesToClasses.put(targetClass.getName().intern(), targetClass); } else if (existingTargetClass != targetClass) { - throw new IllegalArgumentException( - "class [" - + targetCanonicalClassName - + "] " - + "cannot represent multiple java classes with the same name from different class loaders" + throw lookupException( + "class [%s] cannot represent multiple java classes with the same name from different class loaders", + targetCanonicalClassName ); } @@ -1865,17 +1553,12 @@ public void addPainlessInstanceBinding( for (Class typeParameter : typeParameters) { if (isValidType(typeParameter) == false) { - throw new IllegalArgumentException( - "type parameter [" - + typeToCanonicalTypeName(typeParameter) - + "] " - + "not found for instance binding [[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" + throw lookupException( + "type parameter [%s] not found for instance binding [[%s], [%s], %s]", + typeToCanonicalTypeName(typeParameter), + targetCanonicalClassName, + methodName, + typesToCanonicalTypeNames(typeParameters) ); } @@ -1883,17 +1566,12 @@ public void addPainlessInstanceBinding( } if (isValidType(returnType) == false) { - throw new IllegalArgumentException( - "return type [" - + typeToCanonicalTypeName(returnType) - + "] not found for imported method " - + "[[" - + targetCanonicalClassName - + "], [" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" + throw lookupException( + "return type [%s] not found for imported method [[%s], [%s], %s]", + typeToCanonicalTypeName(returnType), + targetCanonicalClassName, + methodName, + typesToCanonicalTypeNames(typeParameters) ); } @@ -1902,46 +1580,32 @@ public void addPainlessInstanceBinding( try { javaMethod = targetClass.getMethod(methodName, javaTypeParameters.toArray(new Class[typeParametersSize])); } catch (NoSuchMethodException nsme) { - throw new IllegalArgumentException( - "instance binding reflection object [[" - + targetCanonicalClassName - + "], " - + "[" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "] not found", - nsme + throw lookupException( + nsme, + "instance binding reflection object [[%s], [%s], %s] not found", + targetCanonicalClassName, + methodName, + typesToCanonicalTypeNames(typeParameters) ); } if (javaMethod.getReturnType() != typeToJavaType(returnType)) { - throw new IllegalArgumentException( - "return type [" - + typeToCanonicalTypeName(javaMethod.getReturnType()) - + "] " - + "does not match the specified returned type [" - + typeToCanonicalTypeName(returnType) - + "] " - + "for instance binding [[" - + targetClass.getCanonicalName() - + "], [" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "]" + throw lookupException( + "return type [%s] does not match the specified returned type [%s] for instance binding [[%s], [%s], %s]", + typeToCanonicalTypeName(javaMethod.getReturnType()), + typeToCanonicalTypeName(returnType), + targetClass.getCanonicalName(), + methodName, + typesToCanonicalTypeNames(typeParameters) ); } if (Modifier.isStatic(javaMethod.getModifiers())) { - throw new IllegalArgumentException( - "instance binding [[" - + targetClass.getCanonicalName() - + "], [" - + methodName - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "] cannot be static" + throw lookupException( + "instance binding [[%s], [%s], %s] cannot be static", + targetClass.getCanonicalName(), + methodName, + typesToCanonicalTypeNames(typeParameters) ); } @@ -1968,34 +1632,19 @@ public void addPainlessInstanceBinding( newPainlessInstanceBinding = painlessInstanceBindingCache.computeIfAbsent(newPainlessInstanceBinding, key -> key); painlessMethodKeysToPainlessInstanceBindings.put(painlessMethodKey.intern(), newPainlessInstanceBinding); } else if (newPainlessInstanceBinding.equals(existingPainlessInstanceBinding) == false) { - throw new IllegalArgumentException( - "cannot add instances bindings with the same name and arity " - + "but do not have equivalent methods " - + "[[" - + targetCanonicalClassName - + "], " - + "[" - + methodName - + "], " - + "[" - + typeToCanonicalTypeName(returnType) - + "], " - + typesToCanonicalTypeNames(typeParameters) - + "], " - + painlessAnnotations - + " and " - + "[[" - + targetCanonicalClassName - + "], " - + "[" - + methodName - + "], " - + "[" - + typeToCanonicalTypeName(existingPainlessInstanceBinding.returnType()) - + "], " - + typesToCanonicalTypeNames(existingPainlessInstanceBinding.typeParameters()) - + "], " - + existingPainlessInstanceBinding.annotations() + throw lookupException( + "cannot add instances bindings with the same name and arity but do not have equivalent methods " + + "[[%s], [%s], [%s], %s], %s and [[%s], [%s], [%s], %s], %s", + targetCanonicalClassName, + methodName, + typeToCanonicalTypeName(returnType), + typesToCanonicalTypeNames(typeParameters), + painlessAnnotations, + targetCanonicalClassName, + methodName, + typeToCanonicalTypeName(existingPainlessInstanceBinding.returnType()), + typesToCanonicalTypeNames(existingPainlessInstanceBinding.typeParameters()), + existingPainlessInstanceBinding.annotations() ); } } @@ -2014,20 +1663,20 @@ public PainlessLookup build() { if (javaClassNamesToClasses.values().containsAll(canonicalClassNamesToClasses.values()) == false) { throw new IllegalArgumentException( - "the values of java class names to classes " + "must be a superset of the values of canonical class names to classes" + "the values of java class names to classes must be a superset of the values of canonical class names to classes" ); } if (javaClassNamesToClasses.values().containsAll(classesToPainlessClasses.keySet()) == false) { throw new IllegalArgumentException( - "the values of java class names to classes " + "must be a superset of the keys of classes to painless classes" + "the values of java class names to classes must be a superset of the keys of classes to painless classes" ); } if (canonicalClassNamesToClasses.values().containsAll(classesToPainlessClasses.keySet()) == false || classesToPainlessClasses.keySet().containsAll(canonicalClassNamesToClasses.values()) == false) { throw new IllegalArgumentException( - "the values of canonical class names to classes " + "must have the same classes as the keys of classes to painless classes" + "the values of canonical class names to classes must have the same classes as the keys of classes to painless classes" ); } @@ -2048,7 +1697,7 @@ private void buildPainlessClassHierarchy() { } for (Class subClass : classesToPainlessClassBuilders.keySet()) { - List> superInterfaces = new ArrayList<>(Arrays.asList(subClass.getInterfaces())); + Deque> superInterfaces = new ArrayDeque<>(Arrays.asList(subClass.getInterfaces())); // we check for Object.class as part of the allow listed classes because // it is possible for the compiler to work without Object @@ -2087,7 +1736,7 @@ private void buildPainlessClassHierarchy() { Set> resolvedInterfaces = new HashSet<>(); while (superInterfaces.isEmpty() == false) { - Class superInterface = superInterfaces.remove(0); + Class superInterface = superInterfaces.removeFirst(); if (resolvedInterfaces.add(superInterface)) { if (classesToPainlessClassBuilders.containsKey(superInterface)) { @@ -2119,12 +1768,10 @@ private void setFunctionalInterfaceMethod(Class targetClass, PainlessClassBui } if (javaMethods.size() != 1 && targetClass.isAnnotationPresent(FunctionalInterface.class)) { - throw new IllegalArgumentException( - "class [" - + typeToCanonicalTypeName(targetClass) - + "] " - + "is illegally marked as a FunctionalInterface with java methods " - + javaMethods + throw lookupException( + "class [%s] is illegally marked as a FunctionalInterface with java methods %s", + typeToCanonicalTypeName(targetClass), + javaMethods ); } else if (javaMethods.size() == 1) { java.lang.reflect.Method javaMethod = javaMethods.get(0); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupUtility.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupUtility.java index be8bb9902a9a..766714a45469 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupUtility.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupUtility.java @@ -340,11 +340,12 @@ public static String buildPainlessFieldKey(String fieldName) { * derived from an {@link org.elasticsearch.painless.spi.annotation.InjectConstantAnnotation}. */ public static Object[] buildInjections(PainlessMethod painlessMethod, Map constants) { - if (painlessMethod.annotations().containsKey(InjectConstantAnnotation.class) == false) { + InjectConstantAnnotation injects = (InjectConstantAnnotation) painlessMethod.annotations().get(InjectConstantAnnotation.class); + if (injects == null) { return new Object[0]; } - List names = ((InjectConstantAnnotation) painlessMethod.annotations().get(InjectConstantAnnotation.class)).injects(); + List names = injects.injects(); Object[] injections = new Object[names.size()]; for (int i = 0; i < names.size(); i++) { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultConstantFoldingOptimizationPhase.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultConstantFoldingOptimizationPhase.java index df57f9d3da65..2097a3e2995f 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultConstantFoldingOptimizationPhase.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultConstantFoldingOptimizationPhase.java @@ -8,6 +8,7 @@ package org.elasticsearch.painless.phase; +import org.elasticsearch.core.Strings; import org.elasticsearch.painless.AnalyzerCaster; import org.elasticsearch.painless.Operation; import org.elasticsearch.painless.ir.BinaryMathNode; @@ -45,6 +46,42 @@ */ public class DefaultConstantFoldingOptimizationPhase extends IRExpressionModifyingVisitor { + private static IllegalStateException unaryError(String type, String operation, String constant) { + return new IllegalStateException( + Strings.format( + "constant folding error: unexpected type [%s] for unary operation [%s] on constant [%s]", + type, + operation, + constant + ) + ); + } + + private static IllegalStateException binaryError(String type, String operation, String constant1, String constant2) { + return error(type, "binary", operation, constant1, constant2); + } + + private static IllegalStateException booleanError(String type, String operation, String constant1, String constant2) { + return error(type, "boolean", operation, constant1, constant2); + } + + private static IllegalStateException comparisonError(String type, String operation, String constant1, String constant2) { + return error(type, "comparison", operation, constant1, constant2); + } + + private static IllegalStateException error(String type, String opType, String operation, String constant1, String constant2) { + return new IllegalStateException( + Strings.format( + "constant folding error: unexpected type [%s] for %s operation [%s] on constants [%s] and [%s]", + type, + opType, + operation, + constant1, + constant2 + ) + ); + } + @Override public void visitUnaryMath(UnaryMathNode irUnaryMathNode, Consumer scope) { irUnaryMathNode.getChildNode().visit(this, irUnaryMathNode::setChildNode); @@ -67,17 +104,10 @@ public void visitUnaryMath(UnaryMathNode irUnaryMathNode, Consumer sco } else { throw irBooleanNode.getLocation() .createError( - new IllegalStateException( - "constant folding error: " - + "unexpected type [" - + PainlessLookupUtility.typeToCanonicalTypeName(type) - + "] for " - + "binary operation [" - + operation.symbol - + "] on " - + "constants [" - + irLeftConstantNode.getDecorationString(IRDConstant.class) - + "] " - + "and [" - + irRightConstantNode.getDecorationString(IRDConstant.class) - + "]" + binaryError( + PainlessLookupUtility.typeToCanonicalTypeName(type), + operation.symbol, + irLeftConstantNode.getDecorationString(IRDConstant.class), + irRightConstantNode.getDecorationString(IRDConstant.class) ) ); } @@ -580,20 +488,11 @@ public void visitBoolean(BooleanNode irBooleanNode, Consumer sco } else { throw irBooleanNode.getLocation() .createError( - new IllegalStateException( - "constant folding error: " - + "unexpected type [" - + PainlessLookupUtility.typeToCanonicalTypeName(type) - + "] for " - + "boolean operation [" - + operation.symbol - + "] on " - + "constants [" - + irLeftConstantNode.getDecorationString(IRDConstant.class) - + "] " - + "and [" - + irRightConstantNode.getDecorationString(IRDConstant.class) - + "]" + booleanError( + PainlessLookupUtility.typeToCanonicalTypeName(type), + operation.symbol, + irLeftConstantNode.getDecorationString(IRDConstant.class), + irRightConstantNode.getDecorationString(IRDConstant.class) ) ); } @@ -687,20 +586,11 @@ public void visitComparison(ComparisonNode irComparisonNode, Consumer { + private static ClassCastException castError(String formatText, Object... arguments) { + return new ClassCastException(Strings.format(formatText, arguments)); + } + /** * Decorates a user expression node with a PainlessCast. */ @@ -252,13 +257,11 @@ public void visitFunction(SFunction userFunctionNode, ScriptScope scriptScope) { if (userBlockNode.getStatementNodes().isEmpty()) { throw userFunctionNode.createError( new IllegalArgumentException( - "invalid function definition: " - + "found no statements for function " - + "[" - + functionName - + "] with [" - + typeParameters.size() - + "] parameters" + Strings.format( + "invalid function definition: found no statements for function [%s] with [%d] parameters", + functionName, + typeParameters.size() + ) ) ); } @@ -271,13 +274,11 @@ public void visitFunction(SFunction userFunctionNode, ScriptScope scriptScope) { if (methodEscape == false && isAutoReturnEnabled == false && returnType != void.class) { throw userFunctionNode.createError( new IllegalArgumentException( - "invalid function definition: " - + "not all paths provide a return value for function " - + "[" - + functionName - + "] with [" - + typeParameters.size() - + "] parameters" + Strings.format( + "invalid function definition: not all paths provide a return value for function [%s] with [%d] parameters", + functionName, + typeParameters.size() + ) ) ); } @@ -751,14 +752,10 @@ public void visitReturn(SReturn userReturnNode, SemanticScope semanticScope) { if (userValueNode == null) { if (semanticScope.getReturnType() != void.class) { throw userReturnNode.createError( - new ClassCastException( - "cannot cast from " - + "[" - + semanticScope.getReturnCanonicalTypeName() - + "] to " - + "[" - + PainlessLookupUtility.typeToCanonicalTypeName(void.class) - + "]" + castError( + "cannot cast from [%s] to [%s]", + semanticScope.getReturnCanonicalTypeName(), + PainlessLookupUtility.typeToCanonicalTypeName(void.class) ) ); } @@ -893,14 +890,12 @@ public void visitCatch(SCatch userCatchNode, SemanticScope semanticScope) { if (userCatchNode.getBaseException().isAssignableFrom(type) == false) { throw userCatchNode.createError( - new ClassCastException( - "cannot cast from [" - + PainlessLookupUtility.typeToCanonicalTypeName(type) - + "] " - + "to [" - + PainlessLookupUtility.typeToCanonicalTypeName(baseException) - + "]" + castError( + "cannot cast from [%s] to [%s]", + PainlessLookupUtility.typeToCanonicalTypeName(type), + PainlessLookupUtility.typeToCanonicalTypeName(baseException) ) + ); } @@ -1032,15 +1027,11 @@ public void visitAssignment(EAssignment userAssignmentNode, SemanticScope semant if (compoundType == null || (isShift && shiftType == null)) { throw userAssignmentNode.createError( - new ClassCastException( - "invalid compound assignment: " - + "cannot apply [" - + operation.symbol - + "=] to types [" - + leftValueType - + "] and [" - + rightValueType - + "]" + castError( + "invalid compound assignment: cannot apply [%s=] to types [%s] and [%s]", + operation.symbol, + leftValueType, + rightValueType ) ); } @@ -1163,17 +1154,13 @@ public void visitUnary(EUnary userUnaryNode, SemanticScope semanticScope) { if (unaryType == null) { throw userUnaryNode.createError( - new ClassCastException( - "cannot apply the " - + operation.name - + " operator " - + "[" - + operation.symbol - + "] to the type " - + "[" - + PainlessLookupUtility.typeToCanonicalTypeName(childValueType) - + "]" + castError( + "cannot apply the %s operator [%s] to the type [%s]", + operation.name, + operation.symbol, + PainlessLookupUtility.typeToCanonicalTypeName(childValueType) ) + ); } @@ -1268,20 +1255,14 @@ public void visitBinary(EBinary userBinaryNode, SemanticScope semanticScope) { if (binaryType == null) { throw userBinaryNode.createError( - new ClassCastException( - "cannot apply the " - + operation.name - + " operator " - + "[" - + operation.symbol - + "] to the types " - + "[" - + PainlessLookupUtility.typeToCanonicalTypeName(leftValueType) - + "] and " - + "[" - + PainlessLookupUtility.typeToCanonicalTypeName(rightValueType) - + "]" + castError( + "cannot apply the %s operator [%s] to the types [%s] and [%s]", + operation.name, + operation.symbol, + PainlessLookupUtility.typeToCanonicalTypeName(leftValueType), + PainlessLookupUtility.typeToCanonicalTypeName(rightValueType) ) + ); } @@ -1405,19 +1386,12 @@ public void visitComp(EComp userCompNode, SemanticScope semanticScope) { if (promotedType == null) { throw userCompNode.createError( - new ClassCastException( - "cannot apply the " - + operation.name - + " operator " - + "[" - + operation.symbol - + "] to the types " - + "[" - + PainlessLookupUtility.typeToCanonicalTypeName(leftValueType) - + "] and " - + "[" - + PainlessLookupUtility.typeToCanonicalTypeName(rightValueType) - + "]" + castError( + "cannot apply the %s operator [%s] to the types [%s] and [%s]", + operation.name, + operation.symbol, + PainlessLookupUtility.typeToCanonicalTypeName(leftValueType), + PainlessLookupUtility.typeToCanonicalTypeName(rightValueType) ) ); } @@ -1566,13 +1540,11 @@ public void visitConditional(EConditional userConditionalNode, SemanticScope sem if (promote == null) { throw userConditionalNode.createError( new ClassCastException( - "cannot apply the conditional operator [?:] to the types " - + "[" - + PainlessLookupUtility.typeToCanonicalTypeName(leftValueType) - + "] and " - + "[" - + PainlessLookupUtility.typeToCanonicalTypeName(rightValueType) - + "]" + Strings.format( + "cannot apply the conditional operator [?:] to the types [%s] and [%s]", + PainlessLookupUtility.typeToCanonicalTypeName(leftValueType), + PainlessLookupUtility.typeToCanonicalTypeName(rightValueType) + ) ) ); } @@ -1822,12 +1794,11 @@ public void visitNewObj(ENewObj userNewObjNode, SemanticScope semanticScope) { if (semanticScope.getCondition(userNewObjNode, Write.class)) { throw userNewObjNode.createError( new IllegalArgumentException( - "invalid assignment cannot assign a value to new object with constructor " - + "[" - + canonicalTypeName - + "/" - + userArgumentsSize - + "]" + Strings.format( + "invalid assignment cannot assign a value to new object with constructor [%s/%d]", + canonicalTypeName, + userArgumentsSize + ) ) ); } @@ -1844,7 +1815,7 @@ public void visitNewObj(ENewObj userNewObjNode, SemanticScope semanticScope) { if (constructor == null) { throw userNewObjNode.createError( new IllegalArgumentException( - "constructor [" + typeToCanonicalTypeName(valueType) + ", /" + userArgumentsSize + "] not found" + Strings.format("constructor [%s, /%d] not found", typeToCanonicalTypeName(valueType), userArgumentsSize) ) ); } @@ -1855,14 +1826,12 @@ public void visitNewObj(ENewObj userNewObjNode, SemanticScope semanticScope) { if (constructor.typeParameters().size() != userArgumentsSize) { throw userNewObjNode.createError( new IllegalArgumentException( - "When calling constructor on type [" - + PainlessLookupUtility.typeToCanonicalTypeName(valueType) - + "] " - + "expected [" - + constructor.typeParameters().size() - + "] arguments, but found [" - + userArgumentsSize - + "]." + Strings.format( + "When calling constructor on type [%s] expected [%d] arguments, but found [%d].", + PainlessLookupUtility.typeToCanonicalTypeName(valueType), + constructor.typeParameters().size(), + userArgumentsSize + ) ) ); } @@ -2295,13 +2264,12 @@ public void visitRegex(ERegex userRegexNode, SemanticScope semanticScope) { } catch (PatternSyntaxException pse) { throw new Location(location.getSourceName(), location.getOffset() + 1 + pse.getIndex()).createError( new IllegalArgumentException( - "invalid regular expression: " - + "could not compile regex constant [" - + pattern - + "] with flags [" - + flags - + "]: " - + pse.getDescription(), + Strings.format( + "invalid regular expression: could not compile regex constant [%s] with flags [%s]: %s", + pattern, + flags, + pse.getDescription() + ), pse ) ); @@ -2364,11 +2332,11 @@ public void visitLambda(ELambda userLambdaNode, SemanticScope semanticScope) { } // check arity before we manipulate parameters if (interfaceMethod.typeParameters().size() != canonicalTypeNameParameters.size()) throw new IllegalArgumentException( - "Incorrect number of parameters for [" - + interfaceMethod.javaMethod().getName() - + "] in [" - + targetType.getTargetCanonicalTypeName() - + "]" + Strings.format( + "Incorrect number of parameters for [%s] in [%s]", + interfaceMethod.javaMethod().getName(), + targetType.getTargetCanonicalTypeName() + ) ); // for method invocation, its allowed to ignore the return value if (interfaceMethod.returnType() == void.class) { @@ -2725,13 +2693,11 @@ public void visitDot(EDot userDotNode, SemanticScope semanticScope) { if (prefixValueType != null && prefixStaticType != null) { throw userDotNode.createError( new IllegalStateException( - "cannot have both " - + "value [" - + prefixValueType.getValueCanonicalTypeName() - + "] " - + "and type [" - + prefixStaticType.getStaticCanonicalTypeName() - + "]" + Strings.format( + "cannot have both value [%s] and type [%s]", + prefixValueType.getValueCanonicalTypeName(), + prefixStaticType.getStaticCanonicalTypeName() + ) ) ); } @@ -3249,13 +3215,11 @@ public void visitCall(ECall userCallNode, SemanticScope semanticScope) { if (prefixValueType != null && prefixStaticType != null) { throw userCallNode.createError( new IllegalStateException( - "cannot have both " - + "value [" - + prefixValueType.getValueCanonicalTypeName() - + "] " - + "and type [" - + prefixStaticType.getStaticCanonicalTypeName() - + "]" + Strings.format( + "cannot have both value [%s] and type [%s]", + prefixValueType.getValueCanonicalTypeName(), + prefixStaticType.getStaticCanonicalTypeName() + ) ) ); } @@ -3292,15 +3256,12 @@ public void visitCall(ECall userCallNode, SemanticScope semanticScope) { if (dynamic == false) { throw userCallNode.createError( new IllegalArgumentException( - "member method " - + "[" - + prefixValueType.getValueCanonicalTypeName() - + ", " - + methodName - + "/" - + userArgumentsSize - + "] " - + "not found" + Strings.format( + "member method [%s, %s/%d] not found", + prefixValueType.getValueCanonicalTypeName(), + methodName, + userArgumentsSize + ) ) ); } @@ -3314,15 +3275,12 @@ public void visitCall(ECall userCallNode, SemanticScope semanticScope) { if (method == null) { throw userCallNode.createError( new IllegalArgumentException( - "static method " - + "[" - + prefixStaticType.getStaticCanonicalTypeName() - + ", " - + methodName - + "/" - + userArgumentsSize - + "] " - + "not found" + Strings.format( + "static method [%s, %s/%d] not found", + prefixStaticType.getStaticCanonicalTypeName(), + methodName, + userArgumentsSize + ) ) ); } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultSemanticHeaderPhase.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultSemanticHeaderPhase.java index 24d5508c5de7..61be12039cbe 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultSemanticHeaderPhase.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultSemanticHeaderPhase.java @@ -8,6 +8,7 @@ package org.elasticsearch.painless.phase; +import org.elasticsearch.core.Strings; import org.elasticsearch.painless.lookup.PainlessLookup; import org.elasticsearch.painless.node.SClass; import org.elasticsearch.painless.node.SFunction; @@ -36,15 +37,13 @@ public void visitFunction(SFunction userFunctionNode, ScriptScope scriptScope) { if (parameterCount != parameterNames.size()) { throw userFunctionNode.createError( new IllegalStateException( - "invalid function definition: " - + "parameter types size [" - + canonicalTypeNameParameters.size() - + "] is not equal to " - + "parameter names size [" - + parameterNames.size() - + "] for function [" - + functionName - + "]" + Strings.format( + "invalid function definition: " + + "parameter types size [%d] is not equal to parameter names size [%d] for function [%s]", + canonicalTypeNameParameters.size(), + parameterNames.size(), + functionName + ) ) ); } @@ -65,12 +64,11 @@ public void visitFunction(SFunction userFunctionNode, ScriptScope scriptScope) { if (returnType == null) { throw userFunctionNode.createError( new IllegalArgumentException( - "invalid function definition: " - + "return type [" - + returnCanonicalTypeName - + "] not found for function [" - + functionKey - + "]" + Strings.format( + "invalid function definition: return type [%s] not found for function [%s]", + returnCanonicalTypeName, + functionKey + ) ) ); } @@ -83,12 +81,11 @@ public void visitFunction(SFunction userFunctionNode, ScriptScope scriptScope) { if (paramType == null) { throw userFunctionNode.createError( new IllegalArgumentException( - "invalid function definition: " - + "parameter type [" - + typeParameter - + "] not found for function [" - + functionKey - + "]" + Strings.format( + "invalid function definition: parameter type [%s] not found for function [%s]", + typeParameter, + functionKey + ) ) ); } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/PainlessSemanticAnalysisPhase.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/PainlessSemanticAnalysisPhase.java index 89eeca52be4b..a7b4771a1fbf 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/PainlessSemanticAnalysisPhase.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/PainlessSemanticAnalysisPhase.java @@ -8,6 +8,7 @@ package org.elasticsearch.painless.phase; +import org.elasticsearch.core.Strings; import org.elasticsearch.painless.AnalyzerCaster; import org.elasticsearch.painless.Location; import org.elasticsearch.painless.ScriptClassInfo; @@ -77,13 +78,11 @@ public void visitFunction(SFunction userFunctionNode, ScriptScope scriptScope) { if (userBlockNode.getStatementNodes().isEmpty()) { throw userFunctionNode.createError( new IllegalArgumentException( - "invalid function definition: " - + "found no statements for function " - + "[" - + functionName - + "] with [" - + typeParameters.size() - + "] parameters" + Strings.format( + "invalid function definition: found no statements for function [%s] with [%d] parameters", + functionName, + typeParameters.size() + ) ) ); } @@ -161,13 +160,11 @@ public void visitReturn(SReturn userReturnNode, SemanticScope semanticScope) { if (semanticScope.getReturnType() != void.class) { throw userReturnNode.createError( new ClassCastException( - "cannot cast from " - + "[" - + semanticScope.getReturnCanonicalTypeName() - + "] to " - + "[" - + PainlessLookupUtility.typeToCanonicalTypeName(void.class) - + "]" + Strings.format( + "cannot cast from [%s] to [%s]", + semanticScope.getReturnCanonicalTypeName(), + PainlessLookupUtility.typeToCanonicalTypeName(void.class) + ) ) ); } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/PainlessSemanticHeaderPhase.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/PainlessSemanticHeaderPhase.java index ed5ca0a63f09..083f14d88b08 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/PainlessSemanticHeaderPhase.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/PainlessSemanticHeaderPhase.java @@ -31,7 +31,7 @@ public void visitFunction(SFunction userFunctionNode, ScriptScope scriptScope) { if (functionTable.getFunction(functionKey) != null) { throw userFunctionNode.createError( - new IllegalArgumentException("invalid function definition: " + "found duplicate function [" + functionKey + "].") + new IllegalArgumentException("invalid function definition: found duplicate function [" + functionKey + "].") ); } From 6a240b9e23d9f169e65dc531263cac8b29e4d000 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Tue, 22 Nov 2022 11:55:22 +0100 Subject: [PATCH 026/919] Trigger index settings providers when updating component templates. (#91615) If index templates make use of the auto index.routing_path generation and use component templates then making any chang to component templates will fail. This commit addresses this, by changing the logic that creates/updates component templates to use the index settings provider when validating index templates that use the component templates being updated. Closes #91592 --- docs/changelog/91615.yaml | 6 ++ .../datastreams/TsdbDataStreamRestIT.java | 58 ++++++++++++------- .../MetadataIndexTemplateService.java | 9 +-- .../MetadataIndexTemplateServiceTests.java | 7 ++- 4 files changed, 49 insertions(+), 31 deletions(-) create mode 100644 docs/changelog/91615.yaml diff --git a/docs/changelog/91615.yaml b/docs/changelog/91615.yaml new file mode 100644 index 000000000000..f0129f8423ee --- /dev/null +++ b/docs/changelog/91615.yaml @@ -0,0 +1,6 @@ +pr: 91615 +summary: Trigger index settings providers when updating component templates +area: Indices APIs +type: bug +issues: + - 91592 diff --git a/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/TsdbDataStreamRestIT.java b/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/TsdbDataStreamRestIT.java index 13d0c067b0cb..457d34e57397 100644 --- a/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/TsdbDataStreamRestIT.java +++ b/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/TsdbDataStreamRestIT.java @@ -13,6 +13,7 @@ import org.elasticsearch.common.time.FormatNames; import org.elasticsearch.test.rest.ESRestTestCase; import org.elasticsearch.test.rest.ObjectPath; +import org.junit.Before; import java.io.IOException; import java.time.Instant; @@ -35,6 +36,14 @@ public class TsdbDataStreamRestIT extends ESRestTestCase { + private static final String COMPONENT_TEMPLATE = """ + { + "template": { + "settings": {} + } + } + """; + private static final String TEMPLATE = """ { "index_patterns": ["k8s*"], @@ -97,6 +106,7 @@ public class TsdbDataStreamRestIT extends ESRestTestCase { } } }, + "composed_of": ["custom_template"], "data_stream": { } }"""; @@ -190,12 +200,19 @@ public class TsdbDataStreamRestIT extends ESRestTestCase { {"@timestamp": "$now", "metricset": "pod", "k8s": {"pod": {"name": "elephant", "uid":"df3145b3-0563-4d3b-a0f7-897eb2876eb4", "ip": "10.10.55.3", "network": {"tx": 1434595272, "rx": 530605511}}}} """; - public void testTsdbDataStreams() throws Exception { - // Create a template - var putComposableIndexTemplateRequest = new Request("POST", "/_index_template/1"); - putComposableIndexTemplateRequest.setJsonEntity(TEMPLATE); - assertOK(client().performRequest(putComposableIndexTemplateRequest)); + @Before + public void setup() throws IOException { + // Add component template: + var request = new Request("POST", "/_component_template/custom_template"); + request.setJsonEntity(COMPONENT_TEMPLATE); + assertOK(client().performRequest(request)); + // Add composable index template + request = new Request("POST", "/_index_template/1"); + request.setJsonEntity(TEMPLATE); + assertOK(client().performRequest(request)); + } + public void testTsdbDataStreams() throws Exception { var bulkRequest = new Request("POST", "/k8s/_bulk"); bulkRequest.setJsonEntity(BULK.replace("$now", formatInstant(Instant.now()))); bulkRequest.addParameter("refresh", "true"); @@ -331,10 +348,6 @@ public void testTsdbDataStreamsNanos() throws Exception { } public void testSimulateTsdbDataStreamTemplate() throws Exception { - var putComposableIndexTemplateRequest = new Request("POST", "/_index_template/1"); - putComposableIndexTemplateRequest.setJsonEntity(TEMPLATE); - assertOK(client().performRequest(putComposableIndexTemplateRequest)); - var simulateIndexTemplateRequest = new Request("POST", "/_index_template/_simulate_index/k8s"); var response = client().performRequest(simulateIndexTemplateRequest); assertOK(response); @@ -353,11 +366,6 @@ public void testSimulateTsdbDataStreamTemplate() throws Exception { } public void testSubsequentRollovers() throws Exception { - // Create a template - var putComposableIndexTemplateRequest = new Request("POST", "/_index_template/1"); - putComposableIndexTemplateRequest.setJsonEntity(TEMPLATE); - assertOK(client().performRequest(putComposableIndexTemplateRequest)); - var createDataStreamRequest = new Request("PUT", "/_data_stream/k8s"); assertOK(client().performRequest(createDataStreamRequest)); @@ -461,12 +469,6 @@ public void testMigrateRegularDataStreamToTsdbDataStream() throws Exception { } public void testChangeTemplateIndexMode() throws Exception { - // Create a template - { - var putComposableIndexTemplateRequest = new Request("POST", "/_index_template/1"); - putComposableIndexTemplateRequest.setJsonEntity(TEMPLATE); - assertOK(client().performRequest(putComposableIndexTemplateRequest)); - } { var indexRequest = new Request("POST", "/k8s/_doc"); var time = Instant.now(); @@ -489,6 +491,22 @@ public void testChangeTemplateIndexMode() throws Exception { } } + public void testUpdateComponentTemplateDoesNotFailIndexTemplateValidation() throws IOException { + var request = new Request("POST", "/_component_template/custom_template"); + request.setJsonEntity(""" + { + "template": { + "settings": { + "index": { + "number_of_replicas": 1 + } + } + } + } + """); + client().performRequest(request); + } + private static Map getIndex(String indexName) throws IOException { var getIndexRequest = new Request("GET", "/" + indexName + "?human"); var response = client().performRequest(getIndexRequest); diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java index 2e5cdab353da..a4f3f9e5573a 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java @@ -309,14 +309,7 @@ public ClusterState addComponentTemplate( final String composableTemplateName = entry.getKey(); final ComposableIndexTemplate composableTemplate = entry.getValue(); try { - validateCompositeTemplate( - tempStateWithComponentTemplateAdded, - composableTemplateName, - composableTemplate, - indicesService, - xContentRegistry, - systemIndices - ); + validateIndexTemplateV2(composableTemplateName, composableTemplate, tempStateWithComponentTemplateAdded); } catch (Exception e) { if (validationFailure == null) { validationFailure = new IllegalArgumentException( diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateServiceTests.java index 91bf2f1b8090..9702810e3afc 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateServiceTests.java @@ -1723,11 +1723,12 @@ public void testUpdateComponentTemplateFailsIfResolvedIndexTemplatesWouldBeInval ); assertNotNull(e.getCause()); - assertThat(e.getCause().getMessage(), containsString("invalid composite mappings for [my-template]")); - assertNotNull(e.getCause().getCause()); + assertThat(e.getCause().getCause().getMessage(), containsString("invalid composite mappings for [my-template]")); + + assertNotNull(e.getCause().getCause().getCause()); assertThat( - e.getCause().getCause().getMessage(), + e.getCause().getCause().getCause().getMessage(), containsString("can't merge a non object mapping [field2] with an object mapping") ); } From 8816fdab59246767a758c70e079819897ed7245f Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Tue, 22 Nov 2022 13:28:10 +0200 Subject: [PATCH 027/919] [ML] Increase timeout in PyTorchModelIT.testTruncation (#91791) We've seen the inference request in the `testTruncation` test time out on busy workers, thus we give it a bit more time to do its work. --- .../org/elasticsearch/xpack/ml/integration/PyTorchModelIT.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/PyTorchModelIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/PyTorchModelIT.java index 99e38e756796..5fbc3f6d0f29 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/PyTorchModelIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/PyTorchModelIT.java @@ -534,7 +534,8 @@ public void testTruncation() throws IOException { containsString("Input too large. The tokenized input length [3] exceeds the maximum sequence length [2]") ); - request = new Request("POST", "/_ml/trained_models/" + modelId + "/_infer"); + // We set timeout to 20s as we've seen this test time out on some busy workers. + request = new Request("POST", "/_ml/trained_models/" + modelId + "/_infer?timeout=20s"); request.setJsonEntity(formatted(""" { "docs": [ From ea988358c7e64461436b40d3ca6a81925bb19d83 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 22 Nov 2022 11:37:35 +0000 Subject: [PATCH 028/919] Unmute test muted for #89860 (#91789) This issue is resolved. --- .../org/elasticsearch/cluster/coordination/CoordinatorTests.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java index 7d208c508395..f1c5aa078785 100644 --- a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java @@ -117,7 +117,6 @@ public void testCanUpdateClusterStateAfterStabilisation() { } } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/89860") public void testDoesNotElectNonMasterNode() { try (Cluster cluster = new Cluster(randomIntBetween(1, 5), false, Settings.EMPTY)) { cluster.runRandomly(); From 343c5c1ef7d722577085382bf84fb93cbfa2eccd Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Tue, 22 Nov 2022 14:57:54 +0200 Subject: [PATCH 029/919] Always test explicit names with name predicate when ignore unavailable (#91618) The "is authorized" check for explicit names must take into account that a name can be authorized even if it doesn't exist. To that end, the "is authorized" check for explicit names, during request rewriting, should not fail if the index is unavailable. This PR is a refactoring that allows changing the predicate without changing the wildcard expansion test. --- .../example/CustomAuthorizationEngine.java | 27 ++- .../metadata/IndexAbstractionResolver.java | 21 +- .../indices/resolve/ResolveIndexTests.java | 4 +- .../security/authz/AuthorizationEngine.java | 22 +- .../ProfileCancellationIntegTests.java | 2 +- .../authz/IndicesAndAliasesResolver.java | 57 +++--- .../xpack/security/authz/RBACEngine.java | 111 ++-------- .../authz/AuthorizationServiceTests.java | 2 +- .../authz/AuthorizedIndicesTests.java | 114 +++++++---- .../authz/IndicesAndAliasesResolverTests.java | 189 ++++++++++-------- .../xpack/security/authz/RBACEngineTests.java | 79 ++------ 11 files changed, 304 insertions(+), 324 deletions(-) diff --git a/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java index 998fe8894ee3..1eeb32ed1346 100644 --- a/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java +++ b/plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java @@ -31,6 +31,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.function.Supplier; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -102,12 +103,30 @@ public void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo auth } @Override - public void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, - Map indicesLookup, ActionListener> listener) { + public void loadAuthorizedIndices( + RequestInfo requestInfo, + AuthorizationInfo authorizationInfo, + Map indicesLookup, + ActionListener listener + ) { if (isSuperuser(requestInfo.getAuthentication().getEffectiveSubject().getUser())) { - listener.onResponse(indicesLookup.keySet()); + listener.onResponse(new AuthorizedIndices() { + public Supplier> all() { + return () -> indicesLookup.keySet(); + } + public boolean check(String name) { + return indicesLookup.containsKey(name); + } + }); } else { - listener.onResponse(Collections.emptySet()); + listener.onResponse(new AuthorizedIndices() { + public Supplier> all() { + return () -> Set.of(); + } + public boolean check(String name) { + return false; + } + }); } } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java index 76f9643b0fb4..7b18e7cac61c 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexAbstractionResolver.java @@ -15,10 +15,11 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.Predicate; +import java.util.function.Supplier; public class IndexAbstractionResolver { @@ -43,15 +44,23 @@ public List resolveIndexAbstractions( Metadata metadata, boolean includeDataStreams ) { - Set availableIndexAbstractions = metadata.getIndicesLookup().keySet(); - return resolveIndexAbstractions(indices, indicesOptions, metadata, availableIndexAbstractions, includeDataStreams); + final Set availableIndexAbstractions = metadata.getIndicesLookup().keySet(); + return resolveIndexAbstractions( + indices, + indicesOptions, + metadata, + () -> availableIndexAbstractions, + availableIndexAbstractions::contains, + includeDataStreams + ); } public List resolveIndexAbstractions( Iterable indices, IndicesOptions indicesOptions, Metadata metadata, - Collection availableIndexAbstractions, + Supplier> allAuthorizedAndAvailable, + Predicate isAuthorized, boolean includeDataStreams ) { List finalIndices = new ArrayList<>(); @@ -72,7 +81,7 @@ public List resolveIndexAbstractions( if (indicesOptions.expandWildcardExpressions() && Regex.isSimpleMatchPattern(indexAbstraction)) { wildcardSeen = true; Set resolvedIndices = new HashSet<>(); - for (String authorizedIndex : availableIndexAbstractions) { + for (String authorizedIndex : allAuthorizedAndAvailable.get()) { if (Regex.simpleMatch(indexAbstraction, authorizedIndex) && isIndexVisible( indexAbstraction, @@ -100,7 +109,7 @@ && isIndexVisible( } else { if (minus) { finalIndices.remove(indexAbstraction); - } else if (indicesOptions.ignoreUnavailable() == false || availableIndexAbstractions.contains(indexAbstraction)) { + } else if (indicesOptions.ignoreUnavailable() == false || isAuthorized.test(indexAbstraction)) { finalIndices.add(indexAbstraction); } } diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexTests.java index b70affac4c09..46c459a66815 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/resolve/ResolveIndexTests.java @@ -211,13 +211,15 @@ public void testResolveHiddenProperlyWithDateMath() { { "logs-pgsql-prod-" + todaySuffix, false, true, false, false, null, Strings.EMPTY_ARRAY }, { "logs-pgsql-prod-" + tomorrowSuffix, false, true, false, false, null, Strings.EMPTY_ARRAY } }; Metadata metadata = buildMetadata(new Object[][] {}, indices); + Set authorizedIndices = Set.of("logs-pgsql-prod-" + todaySuffix, "logs-pgsql-prod-" + tomorrowSuffix); String requestedIndex = ""; List resolvedIndices = resolver.resolveIndexAbstractions( List.of(requestedIndex), IndicesOptions.LENIENT_EXPAND_OPEN, metadata, - List.of("logs-pgsql-prod-" + todaySuffix, "logs-pgsql-prod-" + tomorrowSuffix), + () -> authorizedIndices, + authorizedIndices::contains, randomBoolean() ); assertThat(resolvedIndices.size(), is(1)); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index 4c5bd39cc774..7c69ee63d7d8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -32,6 +32,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Supplier; import java.util.stream.Collectors; import static org.elasticsearch.action.ValidateActions.addValidationError; @@ -179,7 +180,7 @@ void loadAuthorizedIndices( RequestInfo requestInfo, AuthorizationInfo authorizationInfo, Map indicesLookup, - ActionListener> listener + ActionListener listener ); /** @@ -257,6 +258,25 @@ default AuthorizationInfo getAuthenticatedUserAuthorizationInfo() { } } + /** + * Used to retrieve index-like resources that the user has access to, for a specific access action type, + * at a specific point in time (for a fixed cluster state view). + * It can also be used to check if a specific resource name is authorized (access to the resource name + * can be authorized even if it doesn't exist). + */ + interface AuthorizedIndices { + /** + * Returns all the index-like resource names that are available and accessible for an action type by a user, + * at a fixed point in time (for a single cluster state view). + */ + Supplier> all(); + + /** + * Checks if an index-like resource name is authorized, for an action by a user. The resource might or might not exist. + */ + boolean check(String name); + } + /** * This encapsulates the privileges that can be checked for access. It's intentional that the privileges to be checked are specified * in the same manner that they are granted in the {@link RoleDescriptor}. The privilege check can be detailed or not, per the diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/profile/ProfileCancellationIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/profile/ProfileCancellationIntegTests.java index 58be2dffcc3d..749c99207a93 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/profile/ProfileCancellationIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/profile/ProfileCancellationIntegTests.java @@ -417,7 +417,7 @@ public void loadAuthorizedIndices( RequestInfo requestInfo, AuthorizationInfo authorizationInfo, Map indicesLookup, - ActionListener> listener + ActionListener listener ) { listener.onFailure(new UnsupportedOperationException("not implemented")); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index 3927be8a6ba6..6a693fe357f9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -28,6 +28,7 @@ import org.elasticsearch.transport.RemoteClusterAware; import org.elasticsearch.transport.RemoteConnectionStrategy; import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField; import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; @@ -41,6 +42,8 @@ import java.util.Set; import java.util.SortedMap; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.stream.Stream; import static org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField.NO_INDEX_PLACEHOLDER; @@ -87,7 +90,12 @@ class IndicesAndAliasesResolver { * Otherwise, N will be added to the local index list. */ - ResolvedIndices resolve(String action, TransportRequest request, Metadata metadata, Set authorizedIndices) { + ResolvedIndices resolve( + String action, + TransportRequest request, + Metadata metadata, + AuthorizationEngine.AuthorizedIndices authorizedIndices + ) { if (request instanceof IndicesAliasesRequest indicesAliasesRequest) { ResolvedIndices.Builder resolvedIndicesBuilder = new ResolvedIndices.Builder(); for (IndicesRequest indicesRequest : indicesAliasesRequest.getAliasActions()) { @@ -181,7 +189,7 @@ ResolvedIndices resolveIndicesAndAliases( String action, IndicesRequest indicesRequest, Metadata metadata, - Set authorizedIndices + AuthorizationEngine.AuthorizedIndices authorizedIndices ) { final ResolvedIndices.Builder resolvedIndicesBuilder = new ResolvedIndices.Builder(); boolean indicesReplacedWithNoIndices = false; @@ -193,14 +201,16 @@ ResolvedIndices resolveIndicesAndAliases( */ assert indicesRequest.indices() == null || indicesRequest.indices().length == 0 : "indices are: " + Arrays.toString(indicesRequest.indices()); // Arrays.toString() can handle null values - all good - resolvedIndicesBuilder.addLocal(getPutMappingIndexOrAlias((PutMappingRequest) indicesRequest, authorizedIndices, metadata)); + resolvedIndicesBuilder.addLocal( + getPutMappingIndexOrAlias((PutMappingRequest) indicesRequest, authorizedIndices::check, metadata) + ); } else if (indicesRequest instanceof final IndicesRequest.Replaceable replaceable) { final IndicesOptions indicesOptions = indicesRequest.indicesOptions(); // check for all and return list of authorized indices if (IndexNameExpressionResolver.isAllIndices(indicesList(indicesRequest.indices()))) { if (indicesOptions.expandWildcardExpressions()) { - for (String authorizedIndex : authorizedIndices) { + for (String authorizedIndex : authorizedIndices.all().get()) { if (IndexAbstractionResolver.isIndexVisible( "*", authorizedIndex, @@ -226,7 +236,8 @@ ResolvedIndices resolveIndicesAndAliases( split.getLocal(), indicesOptions, metadata, - authorizedIndices, + authorizedIndices.all(), + authorizedIndices::check, indicesRequest.includeDataStreams() ); resolvedIndicesBuilder.addLocal(replaced); @@ -262,7 +273,7 @@ ResolvedIndices resolveIndicesAndAliases( if (aliasesRequest.expandAliasesWildcards()) { List aliases = replaceWildcardsWithAuthorizedAliases( aliasesRequest.aliases(), - loadAuthorizedAliases(authorizedIndices, metadata) + loadAuthorizedAliases(authorizedIndices.all(), metadata) ); aliasesRequest.replaceAliases(aliases.toArray(new String[aliases.size()])); } @@ -304,7 +315,7 @@ ResolvedIndices resolveIndicesAndAliases( * request's concrete index is not in the list of authorized indices, then we need to look to * see if this can be authorized against an alias */ - static String getPutMappingIndexOrAlias(PutMappingRequest request, Set authorizedIndicesList, Metadata metadata) { + static String getPutMappingIndexOrAlias(PutMappingRequest request, Predicate isAuthorized, Metadata metadata) { final String concreteIndexName = request.getConcreteIndex().getName(); // validate that the concrete index exists, otherwise there is no remapping that we could do @@ -320,7 +331,7 @@ static String getPutMappingIndexOrAlias(PutMappingRequest request, Set a + indexAbstraction.getType().getDisplayName() + "], but a concrete index is expected" ); - } else if (authorizedIndicesList.contains(concreteIndexName)) { + } else if (isAuthorized.test(concreteIndexName)) { // user is authorized to put mappings for this index resolvedAliasOrIndex = concreteIndexName; } else { @@ -329,21 +340,17 @@ static String getPutMappingIndexOrAlias(PutMappingRequest request, Set a Map> foundAliases = metadata.findAllAliases(new String[] { concreteIndexName }); List aliasMetadata = foundAliases.get(concreteIndexName); if (aliasMetadata != null) { - Optional foundAlias = aliasMetadata.stream() - .map(AliasMetadata::alias) - .filter(authorizedIndicesList::contains) - .filter(aliasName -> { - IndexAbstraction alias = metadata.getIndicesLookup().get(aliasName); - List indices = alias.getIndices(); - if (indices.size() == 1) { - return true; - } else { - assert alias.getType() == IndexAbstraction.Type.ALIAS; - Index writeIndex = alias.getWriteIndex(); - return writeIndex != null && writeIndex.getName().equals(concreteIndexName); - } - }) - .findFirst(); + Optional foundAlias = aliasMetadata.stream().map(AliasMetadata::alias).filter(isAuthorized).filter(aliasName -> { + IndexAbstraction alias = metadata.getIndicesLookup().get(aliasName); + List indices = alias.getIndices(); + if (indices.size() == 1) { + return true; + } else { + assert alias.getType() == IndexAbstraction.Type.ALIAS; + Index writeIndex = alias.getWriteIndex(); + return writeIndex != null && writeIndex.getName().equals(concreteIndexName); + } + }).findFirst(); resolvedAliasOrIndex = foundAlias.orElse(concreteIndexName); } else { resolvedAliasOrIndex = concreteIndexName; @@ -353,10 +360,10 @@ static String getPutMappingIndexOrAlias(PutMappingRequest request, Set a return resolvedAliasOrIndex; } - private static List loadAuthorizedAliases(Set authorizedIndices, Metadata metadata) { + private static List loadAuthorizedAliases(Supplier> authorizedIndices, Metadata metadata) { List authorizedAliases = new ArrayList<>(); SortedMap existingAliases = metadata.getIndicesLookup(); - for (String authorizedIndex : authorizedIndices) { + for (String authorizedIndex : authorizedIndices.get()) { IndexAbstraction indexAbstraction = existingAliases.get(authorizedIndex); if (indexAbstraction != null && indexAbstraction.getType() == IndexAbstraction.Type.ALIAS) { authorizedAliases.add(authorizedIndex); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index a2a1564e8667..d6d4b5c60d59 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -34,6 +34,7 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.CachedSupplier; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.index.Index; import org.elasticsearch.transport.TransportActionProxy; @@ -86,7 +87,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -455,7 +455,7 @@ public void loadAuthorizedIndices( RequestInfo requestInfo, AuthorizationInfo authorizationInfo, Map indicesLookup, - ActionListener> listener + ActionListener listener ) { if (authorizationInfo instanceof RBACAuthorizationInfo) { final Role role = ((RBACAuthorizationInfo) authorizationInfo).getRole(); @@ -741,11 +741,7 @@ private static GetUserPrivilegesResponse.Indices toIndices(final IndicesPermissi ); } - static Set resolveAuthorizedIndicesFromRole(Role role, RequestInfo requestInfo, Map lookup) { - return resolveAuthorizedIndicesFromRole(role, requestInfo, lookup, () -> LoadAuthorizedIndicesTimeChecker.NO_OP_CONSUMER); - } - - static Set resolveAuthorizedIndicesFromRole( + static AuthorizedIndices resolveAuthorizedIndicesFromRole( Role role, RequestInfo requestInfo, Map lookup, @@ -757,7 +753,7 @@ static Set resolveAuthorizedIndicesFromRole( TransportRequest request = requestInfo.getRequest(); final boolean includeDataStreams = (request instanceof IndicesRequest) && ((IndicesRequest) request).includeDataStreams(); - return new AuthorizedIndicesSet(() -> { + return new AuthorizedIndices(() -> { Consumer> timeChecker = timerSupplier.get(); Set indicesAndAliases = new HashSet<>(); // TODO: can this be done smarter? I think there are usually more indices/aliases in the cluster then indices defined a roles? @@ -931,103 +927,24 @@ private static boolean isAsyncRelatedAction(String action) { || action.equals(SqlAsyncActionNames.SQL_ASYNC_GET_RESULT_ACTION_NAME); } - /** - * A lazily loaded Set for authorized indices. It avoids loading the set if only contains check is required. - * It only loads the set if iterating through it is necessary, i.e. when expanding wildcards. - *

- * NOTE that the lazy loading is NOT thread-safe and must NOT be used by multi-threads. - * The current usage has it wrapped inside a CachingAsyncSupplier which guarantees it to be accessed - * from a single thread. Extra caution is needed if moving or using this class in other places. - */ - static class AuthorizedIndicesSet implements Set { - - private final Supplier> supplier; - private final Predicate predicate; - private Set authorizedIndices = null; - - AuthorizedIndicesSet(Supplier> supplier, Predicate predicate) { - this.supplier = Objects.requireNonNull(supplier); - this.predicate = Objects.requireNonNull(predicate); - } - - private Set getAuthorizedIndices() { - if (authorizedIndices == null) { - authorizedIndices = supplier.get(); - } - return authorizedIndices; - } - - @Override - public int size() { - return getAuthorizedIndices().size(); - } - - @Override - public boolean isEmpty() { - return getAuthorizedIndices().isEmpty(); - } - - @Override - public boolean contains(Object o) { - if (authorizedIndices == null) { - return predicate.test((String) o); - } else { - return authorizedIndices.contains(o); - } - } - - @Override - public Iterator iterator() { - return getAuthorizedIndices().iterator(); - } - - @Override - public Object[] toArray() { - return getAuthorizedIndices().toArray(); - } - - @Override - public T[] toArray(T[] a) { - return getAuthorizedIndices().toArray(a); - } - - @Override - public boolean add(String s) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean remove(Object o) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean containsAll(Collection c) { - if (authorizedIndices == null) { - return c.stream().allMatch(this::contains); - } else { - return authorizedIndices.containsAll(c); - } - } + static final class AuthorizedIndices implements AuthorizationEngine.AuthorizedIndices { - @Override - public boolean addAll(Collection c) { - throw new UnsupportedOperationException(); - } + private final CachedSupplier> allAuthorizedAndAvailableSupplier; + private final Predicate isAuthorizedPredicate; - @Override - public boolean retainAll(Collection c) { - throw new UnsupportedOperationException(); + AuthorizedIndices(Supplier> allAuthorizedAndAvailableSupplier, Predicate isAuthorizedPredicate) { + this.allAuthorizedAndAvailableSupplier = new CachedSupplier<>(allAuthorizedAndAvailableSupplier); + this.isAuthorizedPredicate = Objects.requireNonNull(isAuthorizedPredicate); } @Override - public boolean removeAll(Collection c) { - throw new UnsupportedOperationException(); + public Supplier> all() { + return allAuthorizedAndAvailableSupplier; } @Override - public void clear() { - throw new UnsupportedOperationException(); + public boolean check(String name) { + return this.isAuthorizedPredicate.test(name); } } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index d69b2df3a67a..ae9a52a03dfe 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -2879,7 +2879,7 @@ public void loadAuthorizedIndices( RequestInfo requestInfo, AuthorizationInfo authorizationInfo, Map indicesLookup, - ActionListener> listener + ActionListener listener ) { throw new UnsupportedOperationException("not implemented"); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java index caaf78bf346b..2d896f0a51e9 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java @@ -23,6 +23,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizedIndices; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; @@ -41,17 +42,19 @@ import static org.elasticsearch.xpack.core.security.test.TestRestrictedIndices.RESTRICTED_INDICES; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; public class AuthorizedIndicesTests extends ESTestCase { public void testAuthorizedIndicesUserWithoutRoles() { - Set authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( + AuthorizedIndices authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( Role.EMPTY, getRequestInfo(""), - Metadata.EMPTY_METADATA.getIndicesLookup() + Metadata.EMPTY_METADATA.getIndicesLookup(), + () -> ignore -> {} ); - assertTrue(authorizedIndices.isEmpty()); + assertTrue(authorizedIndices.all().get().isEmpty()); } public void testAuthorizedIndicesUserWithSomeRoles() { @@ -105,36 +108,43 @@ public void testAuthorizedIndicesUserWithSomeRoles() { future ); Role roles = future.actionGet(); - Set list = RBACEngine.resolveAuthorizedIndicesFromRole( + AuthorizedIndices authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( roles, getRequestInfo(SearchAction.NAME), - metadata.getIndicesLookup() + metadata.getIndicesLookup(), + () -> ignore -> {} ); - assertThat(list, containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); - assertFalse(list.contains("bbbbb")); - assertFalse(list.contains("ba")); - assertThat(list, not(contains(internalSecurityIndex))); - assertThat(list, not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.all().get(), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); + assertThat(authorizedIndices.all().get(), not(contains("bbbbb"))); + assertThat(authorizedIndices.check("bbbbb"), is(false)); + assertThat(authorizedIndices.all().get(), not(contains("ba"))); + assertThat(authorizedIndices.check("ba"), is(false)); + assertThat(authorizedIndices.all().get(), not(contains(internalSecurityIndex))); + assertThat(authorizedIndices.check(internalSecurityIndex), is(false)); + assertThat(authorizedIndices.all().get(), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS), is(false)); } public void testAuthorizedIndicesUserWithSomeRolesEmptyMetadata() { Role role = Role.builder(RESTRICTED_INDICES, "role").add(IndexPrivilege.ALL, "*").build(); - Set authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( + AuthorizedIndices authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( role, getRequestInfo(SearchAction.NAME), - Metadata.EMPTY_METADATA.getIndicesLookup() + Metadata.EMPTY_METADATA.getIndicesLookup(), + () -> ignore -> {} ); - assertTrue(authorizedIndices.isEmpty()); + assertTrue(authorizedIndices.all().get().isEmpty()); } public void testSecurityIndicesAreRemovedFromRegularUser() { Role role = Role.builder(RESTRICTED_INDICES, "user_role").add(IndexPrivilege.ALL, "*").cluster(Set.of("all"), Set.of()).build(); - Set authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( + AuthorizedIndices authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( role, getRequestInfo(SearchAction.NAME), - Metadata.EMPTY_METADATA.getIndicesLookup() + Metadata.EMPTY_METADATA.getIndicesLookup(), + () -> ignore -> {} ); - assertTrue(authorizedIndices.isEmpty()); + assertTrue(authorizedIndices.all().get().isEmpty()); } public void testSecurityIndicesAreRestrictedForDefaultRole() { @@ -160,14 +170,19 @@ public void testSecurityIndicesAreRestrictedForDefaultRole() { ) .build(); - Set authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( + AuthorizedIndices authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( role, getRequestInfo(SearchAction.NAME), - metadata.getIndicesLookup() + metadata.getIndicesLookup(), + () -> ignore -> {} ); - assertThat(authorizedIndices, containsInAnyOrder("an-index", "another-index")); - assertThat(authorizedIndices, not(contains(internalSecurityIndex))); - assertThat(authorizedIndices, not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.all().get(), containsInAnyOrder("an-index", "another-index")); + assertThat(authorizedIndices.check("an-index"), is(true)); + assertThat(authorizedIndices.check("another-index"), is(true)); + assertThat(authorizedIndices.all().get(), not(contains(internalSecurityIndex))); + assertThat(authorizedIndices.check(internalSecurityIndex), is(false)); + assertThat(authorizedIndices.all().get(), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS), is(false)); } public void testSecurityIndicesAreNotRemovedFromUnrestrictedRole() { @@ -193,23 +208,25 @@ public void testSecurityIndicesAreNotRemovedFromUnrestrictedRole() { ) .build(); - Set authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( + AuthorizedIndices authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( role, getRequestInfo(SearchAction.NAME), - metadata.getIndicesLookup() + metadata.getIndicesLookup(), + () -> ignore -> {} ); assertThat( - authorizedIndices, + authorizedIndices.all().get(), containsInAnyOrder("an-index", "another-index", SecuritySystemIndices.SECURITY_MAIN_ALIAS, internalSecurityIndex) ); - Set authorizedIndicesSuperUser = RBACEngine.resolveAuthorizedIndicesFromRole( + AuthorizedIndices authorizedIndicesSuperUser = RBACEngine.resolveAuthorizedIndicesFromRole( role, getRequestInfo(SearchAction.NAME), - metadata.getIndicesLookup() + metadata.getIndicesLookup(), + () -> ignore -> {} ); assertThat( - authorizedIndicesSuperUser, + authorizedIndicesSuperUser.all().get(), containsInAnyOrder("an-index", "another-index", SecuritySystemIndices.SECURITY_MAIN_ALIAS, internalSecurityIndex) ); } @@ -273,17 +290,23 @@ public void testDataStreamsAreNotIncludedInAuthorizedIndices() { future ); Role roles = future.actionGet(); - Set list = RBACEngine.resolveAuthorizedIndicesFromRole( + AuthorizedIndices authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( roles, getRequestInfo(SearchAction.NAME), - metadata.getIndicesLookup() + metadata.getIndicesLookup(), + () -> ignore -> {} ); - assertThat(list, containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); - assertFalse(list.contains("bbbbb")); - assertFalse(list.contains("ba")); - assertFalse(list.contains("adatastream1")); - assertThat(list, not(contains(internalSecurityIndex))); - assertThat(list, not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.all().get(), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab")); + assertThat(authorizedIndices.all().get(), not(contains("bbbbb"))); + assertThat(authorizedIndices.check("bbbbb"), is(false)); + assertThat(authorizedIndices.all().get(), not(contains("ba"))); + assertThat(authorizedIndices.check("ba"), is(false)); + assertThat(authorizedIndices.all().get(), not(contains("adatastream1"))); + assertThat(authorizedIndices.check("adatastream1"), is(false)); + assertThat(authorizedIndices.all().get(), not(contains(internalSecurityIndex))); + assertThat(authorizedIndices.check(internalSecurityIndex), is(false)); + assertThat(authorizedIndices.all().get(), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS), is(false)); } public void testDataStreamsAreIncludedInAuthorizedIndices() { @@ -347,12 +370,21 @@ public void testDataStreamsAreIncludedInAuthorizedIndices() { Role roles = future.actionGet(); TransportRequest request = new ResolveIndexAction.Request(new String[] { "a*" }); AuthorizationEngine.RequestInfo requestInfo = getRequestInfo(request, SearchAction.NAME); - Set list = RBACEngine.resolveAuthorizedIndicesFromRole(roles, requestInfo, metadata.getIndicesLookup()); - assertThat(list, containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab", "adatastream1", backingIndex)); - assertFalse(list.contains("bbbbb")); - assertFalse(list.contains("ba")); - assertThat(list, not(contains(internalSecurityIndex))); - assertThat(list, not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + AuthorizedIndices authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( + roles, + requestInfo, + metadata.getIndicesLookup(), + () -> ignore -> {} + ); + assertThat(authorizedIndices.all().get(), containsInAnyOrder("a1", "a2", "aaaaaa", "b", "ab", "adatastream1", backingIndex)); + assertThat(authorizedIndices.all().get(), not(contains("bbbbb"))); + assertThat(authorizedIndices.check("bbbbb"), is(false)); + assertThat(authorizedIndices.all().get(), not(contains("ba"))); + assertThat(authorizedIndices.check("ba"), is(false)); + assertThat(authorizedIndices.all().get(), not(contains(internalSecurityIndex))); + assertThat(authorizedIndices.check(internalSecurityIndex), is(false)); + assertThat(authorizedIndices.all().get(), not(contains(SecuritySystemIndices.SECURITY_MAIN_ALIAS))); + assertThat(authorizedIndices.check(SecuritySystemIndices.SECURITY_MAIN_ALIAS), is(false)); } public static AuthorizationEngine.RequestInfo getRequestInfo(String action) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java index 4b55d5894adc..8b0d2a568bc7 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java @@ -57,6 +57,7 @@ import org.elasticsearch.xpack.core.graph.action.GraphExploreAction; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.Subject; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizedIndices; import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField; import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; @@ -80,7 +81,6 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -804,7 +804,7 @@ public void testSearchWithRemoteAndLocalIndices() { public void testSearchWithRemoteAndLocalWildcards() { SearchRequest request = new SearchRequest("*:foo", "r*:bar*", "remote:baz*", "bar*", "foofoo"); request.indicesOptions(IndicesOptions.fromOptions(randomBoolean(), randomBoolean(), true, false)); - final Set authorizedIndices = buildAuthorizedIndices(user, SearchAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, SearchAction.NAME); final ResolvedIndices resolved = resolveIndices(request, authorizedIndices); assertThat(resolved.getRemote(), containsInAnyOrder("remote:foo", "other_remote:foo", "remote:bar*", "remote:baz*")); assertThat(resolved.getLocal(), containsInAnyOrder("bar", "foofoo")); @@ -922,7 +922,7 @@ public void testResolveIndicesAliasesRequestDeleteActions() { IndicesAliasesRequest request = new IndicesAliasesRequest(); request.addAliasAction(AliasActions.remove().index("foo").alias("foofoobar")); request.addAliasAction(AliasActions.remove().index("foofoo").alias("barbaz")); - final Set authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); // the union of all indices and aliases gets returned String[] expectedIndices = new String[] { "foo", "foofoobar", "foofoo", "barbaz" }; @@ -938,7 +938,7 @@ public void testResolveIndicesAliasesRequestDeleteActionsMissingIndex() { IndicesAliasesRequest request = new IndicesAliasesRequest(); request.addAliasAction(AliasActions.remove().index("foo").alias("foofoobar")); request.addAliasAction(AliasActions.remove().index("missing_index").alias("missing_alias")); - final Set authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); // the union of all indices and aliases gets returned, doesn't matter is some of them don't exist String[] expectedIndices = new String[] { "foo", "foofoobar", "missing_index", "missing_alias" }; @@ -954,7 +954,7 @@ public void testResolveWildcardsIndicesAliasesRequestDeleteActions() { IndicesAliasesRequest request = new IndicesAliasesRequest(); request.addAliasAction(AliasActions.remove().index("foo*").alias("foofoobar")); request.addAliasAction(AliasActions.remove().index("bar*").alias("barbaz")); - final Set authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); // union of all resolved indices and aliases gets returned, based on what user is authorized for String[] expectedIndices = new String[] { "foofoobar", "foofoo", "bar", "barbaz" }; @@ -971,7 +971,7 @@ public void testResolveAliasesWildcardsIndicesAliasesRequestDeleteActions() { IndicesAliasesRequest request = new IndicesAliasesRequest(); request.addAliasAction(AliasActions.remove().index("*").alias("foo*")); request.addAliasAction(AliasActions.remove().index("*bar").alias("foo*")); - final Set authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); // union of all resolved indices and aliases gets returned, based on what user is authorized for // note that the index side will end up containing matching aliases too, which is fine, as es core would do @@ -989,7 +989,7 @@ public void testResolveAllAliasesWildcardsIndicesAliasesRequestDeleteActions() { IndicesAliasesRequest request = new IndicesAliasesRequest(); request.addAliasAction(AliasActions.remove().index("*").alias("_all")); request.addAliasAction(AliasActions.remove().index("_all").aliases("_all", "explicit")); - final Set authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); // union of all resolved indices and aliases gets returned, based on what user is authorized for // note that the index side will end up containing matching aliases too, which is fine, as es core would do @@ -1027,7 +1027,7 @@ public void testResolveWildcardsIndicesAliasesRequestAddAndDeleteActions() { IndicesAliasesRequest request = new IndicesAliasesRequest(); request.addAliasAction(AliasActions.remove().index("foo*").alias("foofoobar")); request.addAliasAction(AliasActions.add().index("bar*").alias("foofoobar")); - final Set authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, IndicesAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); // union of all resolved indices and aliases gets returned, based on what user is authorized for String[] expectedIndices = new String[] { "foofoobar", "foofoo", "bar" }; @@ -1042,7 +1042,7 @@ public void testResolveWildcardsIndicesAliasesRequestAddAndDeleteActions() { public void testResolveGetAliasesRequestStrict() { GetAliasesRequest request = new GetAliasesRequest("alias1").indices("foo", "foofoo"); request.indicesOptions(IndicesOptions.fromOptions(false, randomBoolean(), randomBoolean(), randomBoolean())); - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); // the union of all indices and aliases gets returned String[] expectedIndices = new String[] { "alias1", "foo", "foofoo" }; @@ -1055,7 +1055,7 @@ public void testResolveGetAliasesRequestStrict() { public void testResolveGetAliasesRequestIgnoreUnavailable() { GetAliasesRequest request = new GetAliasesRequest("alias1").indices("foo", "foofoo"); request.indicesOptions(IndicesOptions.fromOptions(true, randomBoolean(), randomBoolean(), randomBoolean())); - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); String[] expectedIndices = new String[] { "alias1", "foofoo" }; assertThat(indices, hasSize(expectedIndices.length)); @@ -1069,7 +1069,7 @@ public void testResolveGetAliasesRequestMissingIndexStrict() { request.indicesOptions(IndicesOptions.fromOptions(false, randomBoolean(), true, randomBoolean())); request.indices("missing"); request.aliases("alias2"); - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); // the union of all indices and aliases gets returned, missing is not an existing index/alias but that doesn't make any difference String[] expectedIndices = new String[] { "alias2", "missing" }; @@ -1104,7 +1104,7 @@ public void testGetAliasesRequestMissingIndexStrict() { request.indicesOptions(IndicesOptions.fromOptions(false, randomBoolean(), randomBoolean(), randomBoolean())); request.indices("missing"); request.aliases("alias2"); - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); String[] expectedIndices = new String[] { "alias2", "missing" }; assertThat(indices, hasSize(expectedIndices.length)); @@ -1118,7 +1118,7 @@ public void testResolveWildcardsGetAliasesRequestStrictExpand() { request.indicesOptions(IndicesOptions.fromOptions(false, randomBoolean(), true, true)); request.aliases("alias1"); request.indices("foo*"); - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); // the union of all resolved indices and aliases gets returned, based on indices and aliases that user is authorized for String[] expectedIndices = new String[] { "alias1", "foofoo", "foofoo-closed", "foofoobar", "foobarfoo" }; @@ -1134,7 +1134,7 @@ public void testResolveWildcardsGetAliasesRequestStrictExpandOpen() { request.indicesOptions(IndicesOptions.fromOptions(false, randomBoolean(), true, false)); request.aliases("alias1"); request.indices("foo*"); - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); // the union of all resolved indices and aliases gets returned, based on indices and aliases that user is authorized for String[] expectedIndices = new String[] { "alias1", "foofoo", "foofoobar", "foobarfoo" }; @@ -1150,7 +1150,7 @@ public void testResolveWildcardsGetAliasesRequestLenientExpandOpen() { request.indicesOptions(IndicesOptions.fromOptions(true, randomBoolean(), true, false)); request.aliases("alias1"); request.indices("foo*", "bar", "missing"); - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); // the union of all resolved indices and aliases gets returned, based on indices and aliases that user is authorized for String[] expectedIndices = new String[] { "alias1", "foofoo", "foofoobar", "foobarfoo", "bar" }; @@ -1188,7 +1188,7 @@ public void testResolveAllGetAliasesRequest() { request.indices("_all"); } request.aliases("alias1"); - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); // the union of all resolved indices and aliases gets returned, including hidden indices as Get Aliases includes hidden by default String[] expectedIndices = new String[] { @@ -1233,7 +1233,7 @@ public void testResolveAllGetAliasesRequestExpandWildcardsOpenOnly() { request.indices("_all"); } request.aliases("alias1"); - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); // the union of all resolved indices and aliases gets returned String[] expectedIndices = new String[] { "bar", "foofoobar", "foobarfoo", "foofoo", "alias1" }; @@ -1294,7 +1294,7 @@ public void testResolveAllAliasesGetAliasesRequest() { if (randomBoolean()) { request.indices("_all"); } - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); // the union of all resolved indices and aliases gets returned, including hidden indices as Get Aliases includes hidden by default String[] expectedIndices = new String[] { @@ -1321,7 +1321,7 @@ public void testResolveAllAndExplicitAliasesGetAliasesRequest() { if (randomBoolean()) { request.indices("_all"); } - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); // the union of all resolved indices and aliases gets returned, including hidden indices as Get Aliases includes hidden by default String[] expectedIndices = new String[] { @@ -1366,7 +1366,7 @@ public void testResolveAllAndWildcardsAliasesGetAliasesRequest() { if (randomBoolean()) { request.indices("_all"); } - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); // the union of all resolved indices and aliases gets returned, including hidden indices as Get Aliases includes hidden by default String[] expectedIndices = new String[] { @@ -1392,7 +1392,7 @@ public void testResolveAliasesWildcardsGetAliasesRequest() { GetAliasesRequest request = new GetAliasesRequest(); request.indices("*bar"); request.aliases("foo*"); - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); // union of all resolved indices and aliases gets returned, based on what user is authorized for // note that the index side will end up containing matching aliases too, which is fine, as es core would do @@ -1416,7 +1416,7 @@ public void testResolveAliasesWildcardsGetAliasesRequestNoAuthorizedIndices() { public void testResolveAliasesExclusionWildcardsGetAliasesRequest() { GetAliasesRequest request = new GetAliasesRequest(); request.aliases("foo*", "-foobar*"); - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); // union of all resolved indices and aliases gets returned, based on what user is authorized for // note that the index side will end up containing matching aliases too, which is fine, as es core would do @@ -1533,7 +1533,7 @@ public void testCompositeIndicesRequestIsNotSupported() { } public void testResolveAdminAction() { - final Set authorizedIndices = buildAuthorizedIndices(user, DeleteIndexAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, DeleteIndexAction.NAME); { RefreshRequest request = new RefreshRequest("*"); List indices = resolveIndices(request, authorizedIndices).getLocal(); @@ -1555,14 +1555,14 @@ public void testResolveAdminAction() { public void testXPackSecurityUserHasAccessToSecurityIndex() { SearchRequest request = new SearchRequest(); { - final Set authorizedIndices = buildAuthorizedIndices(XPackSecurityUser.INSTANCE, SearchAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(XPackSecurityUser.INSTANCE, SearchAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); assertThat(indices, hasItem(SECURITY_MAIN_ALIAS)); } { IndicesAliasesRequest aliasesRequest = new IndicesAliasesRequest(); aliasesRequest.addAliasAction(AliasActions.add().alias("security_alias").index(SECURITY_MAIN_ALIAS)); - final Set authorizedIndices = buildAuthorizedIndices(XPackSecurityUser.INSTANCE, IndicesAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(XPackSecurityUser.INSTANCE, IndicesAliasesAction.NAME); List indices = resolveIndices(aliasesRequest, authorizedIndices).getLocal(); assertThat(indices, hasItem(SECURITY_MAIN_ALIAS)); } @@ -1570,7 +1570,7 @@ public void testXPackSecurityUserHasAccessToSecurityIndex() { public void testXPackUserDoesNotHaveAccessToSecurityIndex() { SearchRequest request = new SearchRequest(); - final Set authorizedIndices = buildAuthorizedIndices(XPackUser.INSTANCE, SearchAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(XPackUser.INSTANCE, SearchAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); assertThat(indices, not(hasItem(SECURITY_MAIN_ALIAS))); } @@ -1589,7 +1589,7 @@ public void testNonXPackUserAccessingSecurityIndex() { { SearchRequest request = new SearchRequest(); - final Set authorizedIndices = buildAuthorizedIndices(allAccessUser, SearchAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(allAccessUser, SearchAction.NAME); List indices = resolveIndices(request, authorizedIndices).getLocal(); assertThat(indices, not(hasItem(SECURITY_MAIN_ALIAS))); } @@ -1597,7 +1597,7 @@ public void testNonXPackUserAccessingSecurityIndex() { { IndicesAliasesRequest aliasesRequest = new IndicesAliasesRequest(); aliasesRequest.addAliasAction(AliasActions.add().alias("security_alias1").index("*")); - final Set authorizedIndices = buildAuthorizedIndices(allAccessUser, IndicesAliasesAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(allAccessUser, IndicesAliasesAction.NAME); List indices = resolveIndices(aliasesRequest, authorizedIndices).getLocal(); assertThat(indices, not(hasItem(SECURITY_MAIN_ALIAS))); } @@ -1710,25 +1710,23 @@ public void testAliasDateMathExpressionNotSupported() { public void testDynamicPutMappingRequestFromAlias() { PutMappingRequest request = new PutMappingRequest(Strings.EMPTY_ARRAY).setConcreteIndex(new Index("foofoo", UUIDs.base64UUID())); User user = new User("alias-writer", "alias_read_write"); - Set authorizedIndices = buildAuthorizedIndices(user, PutMappingAction.NAME); + AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, PutMappingAction.NAME); - String putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias(request, authorizedIndices, metadata); + String putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias(request, authorizedIndices::check, metadata); assertEquals("barbaz", putMappingIndexOrAlias); // multiple indices map to an alias so we can only return the concrete index final String index = randomFrom("foo", "foobar"); request = new PutMappingRequest(Strings.EMPTY_ARRAY).setConcreteIndex(new Index(index, UUIDs.base64UUID())); - putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias(request, authorizedIndices, metadata); + putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias(request, authorizedIndices::check, metadata); assertEquals(index, putMappingIndexOrAlias); - } public void testWhenAliasToMultipleIndicesAndUserIsAuthorizedUsingAliasReturnsAliasNameForDynamicPutMappingRequestOnWriteIndex() { String index = "logs-00003"; // write index PutMappingRequest request = new PutMappingRequest(Strings.EMPTY_ARRAY).setConcreteIndex(new Index(index, UUIDs.base64UUID())); - Set authorizedIndices = Collections.singleton("logs-alias"); assert metadata.getIndicesLookup().get("logs-alias").getIndices().size() == 3; - String putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias(request, authorizedIndices, metadata); + String putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias(request, "logs-alias"::equals, metadata); String message = "user is authorized to access `logs-alias` and the put mapping request is for a write index" + "so this should have returned the alias name"; assertEquals(message, "logs-alias", putMappingIndexOrAlias); @@ -1737,9 +1735,8 @@ public void testWhenAliasToMultipleIndicesAndUserIsAuthorizedUsingAliasReturnsAl public void testWhenAliasToMultipleIndicesAndUserIsAuthorizedUsingAliasReturnsIndexNameForDynamicPutMappingRequestOnReadIndex() { String index = "logs-00002"; // read index PutMappingRequest request = new PutMappingRequest(Strings.EMPTY_ARRAY).setConcreteIndex(new Index(index, UUIDs.base64UUID())); - Set authorizedIndices = Collections.singleton("logs-alias"); assert metadata.getIndicesLookup().get("logs-alias").getIndices().size() == 3; - String putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias(request, authorizedIndices, metadata); + String putMappingIndexOrAlias = IndicesAndAliasesResolver.getPutMappingIndexOrAlias(request, "logs-alias"::equals, metadata); String message = "user is authorized to access `logs-alias` and the put mapping request is for a read index" + "so this should have returned the concrete index as fallback"; assertEquals(message, index, putMappingIndexOrAlias); @@ -1748,7 +1745,7 @@ public void testWhenAliasToMultipleIndicesAndUserIsAuthorizedUsingAliasReturnsIn public void testHiddenIndicesResolution() { SearchRequest searchRequest = new SearchRequest(); searchRequest.indicesOptions(IndicesOptions.fromOptions(false, false, true, true, true)); - Set authorizedIndices = buildAuthorizedIndices(user, SearchAction.NAME); + AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, SearchAction.NAME); ResolvedIndices resolvedIndices = defaultIndicesResolver.resolveIndicesAndAliases( SearchAction.NAME, searchRequest, @@ -1835,7 +1832,7 @@ public void testHiddenIndicesResolution() { public void testHiddenAliasesResolution() { final User user = new User("hidden-alias-tester", "hidden_alias_test"); - final Set authorizedIndices = buildAuthorizedIndices(user, SearchAction.NAME); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, SearchAction.NAME); // Visible only SearchRequest searchRequest = new SearchRequest(); @@ -1904,7 +1901,7 @@ public void testDataStreamResolution() { SearchRequest searchRequest = new SearchRequest(); searchRequest.indices("logs-*"); searchRequest.indicesOptions(IndicesOptions.fromOptions(false, false, true, false, false, true, true, true, true)); - final Set authorizedIndices = buildAuthorizedIndices(user, SearchAction.NAME, searchRequest); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, SearchAction.NAME, searchRequest); ResolvedIndices resolvedIndices = defaultIndicesResolver.resolveIndicesAndAliases( SearchAction.NAME, searchRequest, @@ -1935,7 +1932,7 @@ public void testDataStreamResolution() { SearchRequest searchRequest = new SearchRequest(); searchRequest.indices("logs-*"); searchRequest.indicesOptions(IndicesOptions.fromOptions(false, false, true, false, false, true, true, true, true)); - final Set authorizedIndices = buildAuthorizedIndices(user, SearchAction.NAME, searchRequest); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, SearchAction.NAME, searchRequest); ResolvedIndices resolvedIndices = defaultIndicesResolver.resolveIndicesAndAliases( SearchAction.NAME, searchRequest, @@ -1956,13 +1953,16 @@ public void testDataStreamsAreNotVisibleWhenNotIncludedByRequestWithWildcard() { // data streams and their backing indices should _not_ be in the authorized list since the backing indices // do not match the requested pattern List dataStreams = List.of("logs-foo", "logs-foobar"); - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); for (String dsName : dataStreams) { - assertThat(authorizedIndices, hasItem(dsName)); + assertThat(authorizedIndices.all().get(), hasItem(dsName)); + assertThat(authorizedIndices.check(dsName), is(true)); DataStream dataStream = metadata.dataStreams().get(dsName); - assertThat(authorizedIndices, hasItem(dsName)); + assertThat(authorizedIndices.all().get(), hasItem(dsName)); + assertThat(authorizedIndices.check(dsName), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices, hasItem(i.getName())); + assertThat(authorizedIndices.all().get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName()), is(true)); } } @@ -1993,12 +1993,15 @@ public void testDataStreamsAreNotVisibleWhenNotIncludedByRequestWithoutWildcard( // data streams and their backing indices should _not_ be in the authorized list since the backing indices // do not match the requested name - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices, hasItem(dataStreamName)); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); + assertThat(authorizedIndices.all().get(), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName), is(true)); DataStream dataStream = metadata.dataStreams().get(dataStreamName); - assertThat(authorizedIndices, hasItem(dataStreamName)); + assertThat(authorizedIndices.all().get(), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices, hasItem(i.getName())); + assertThat(authorizedIndices.all().get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName()), is(true)); } // neither data streams nor their backing indices will be in the resolved list since the backing indices do not match the @@ -2023,12 +2026,14 @@ public void testDataStreamsAreVisibleWhenIncludedByRequestWithWildcard() { // data streams and their backing indices should be in the authorized list List expectedDataStreams = List.of("logs-foo", "logs-foobar"); - final Set authorizedIndices = buildAuthorizedIndices(user, SearchAction.NAME, request); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, SearchAction.NAME, request); for (String dsName : expectedDataStreams) { DataStream dataStream = metadata.dataStreams().get(dsName); - assertThat(authorizedIndices, hasItem(dsName)); + assertThat(authorizedIndices.all().get(), hasItem(dsName)); + assertThat(authorizedIndices.check(dsName), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices, hasItem(i.getName())); + assertThat(authorizedIndices.all().get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName()), is(true)); } } @@ -2063,11 +2068,13 @@ public void testDataStreamsAreVisibleWhenIncludedByRequestWithoutWildcard() { assertThat(request, instanceOf(IndicesRequest.Replaceable.class)); assertThat(request.includeDataStreams(), is(true)); - final Set authorizedIndices = buildAuthorizedIndices(user, SearchAction.NAME, request); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, SearchAction.NAME, request); // data streams and their backing indices should be in the authorized list - assertThat(authorizedIndices, hasItem(dataStreamName)); + assertThat(authorizedIndices.all().get(), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices, hasItem(i.getName())); + assertThat(authorizedIndices.all().get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName()), is(true)); } ResolvedIndices resolvedIndices = defaultIndicesResolver.resolveIndicesAndAliases( @@ -2092,12 +2099,14 @@ public void testBackingIndicesAreVisibleWhenIncludedByRequestWithWildcard() { // data streams and their backing indices should be included in the authorized list List expectedDataStreams = List.of("logs-foo", "logs-foobar"); - final Set authorizedIndices = buildAuthorizedIndices(user, SearchAction.NAME, request); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, SearchAction.NAME, request); for (String dsName : expectedDataStreams) { DataStream dataStream = metadata.dataStreams().get(dsName); - assertThat(authorizedIndices, hasItem(dsName)); + assertThat(authorizedIndices.all().get(), hasItem(dsName)); + assertThat(authorizedIndices.check(dsName), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices, hasItem(i.getName())); + assertThat(authorizedIndices.all().get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName()), is(true)); } } @@ -2127,12 +2136,15 @@ public void testBackingIndicesAreNotVisibleWhenNotIncludedByRequestWithoutWildca // data streams and their backing indices should _not_ be in the authorized list since the backing indices // did not match the requested pattern and the request does not support data streams - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices, hasItem(dataStreamName)); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); + assertThat(authorizedIndices.all().get(), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName), is(true)); DataStream dataStream = metadata.dataStreams().get(dataStreamName); - assertThat(authorizedIndices, hasItem(dataStreamName)); + assertThat(authorizedIndices.all().get(), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName), is(true)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices, hasItem(i.getName())); + assertThat(authorizedIndices.all().get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName()), is(true)); } // neither data streams nor their backing indices will be in the resolved list since the request does not support data streams @@ -2158,12 +2170,15 @@ public void testDataStreamNotAuthorizedWhenBackingIndicesAreAuthorizedViaWildcar // data streams should _not_ be in the authorized list but their backing indices that matched both the requested pattern // and the authorized pattern should be in the list - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices, not(hasItem("logs-foobar"))); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); + assertThat(authorizedIndices.all().get(), not(hasItem("logs-foobar"))); + assertThat(authorizedIndices.check("logs-foobar"), is(false)); DataStream dataStream = metadata.dataStreams().get("logs-foobar"); - assertThat(authorizedIndices, not(hasItem(indexName))); + assertThat(authorizedIndices.all().get(), not(hasItem(indexName))); + assertThat(authorizedIndices.check(indexName), is(false)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices, hasItem(i.getName())); + assertThat(authorizedIndices.all().get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName()), is(true)); } // only the backing indices will be in the resolved list since the request does not support data streams @@ -2176,7 +2191,8 @@ public void testDataStreamNotAuthorizedWhenBackingIndicesAreAuthorizedViaWildcar ); assertThat(resolvedIndices.getLocal(), not(hasItem(dataStream.getName()))); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices, hasItem(i.getName())); + assertThat(authorizedIndices.all().get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName()), is(true)); } } @@ -2189,9 +2205,11 @@ public void testDataStreamNotAuthorizedWhenBackingIndicesAreAuthorizedViaNameAnd // data streams should _not_ be in the authorized list but a single backing index that matched the requested pattern // and the authorized name should be in the list - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices, not(hasItem("logs-foobar"))); - assertThat(authorizedIndices, contains(DataStream.getDefaultBackingIndexName("logs-foobar", 1))); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); + assertThat(authorizedIndices.all().get(), not(hasItem("logs-foobar"))); + assertThat(authorizedIndices.check("logs-foobar"), is(false)); + assertThat(authorizedIndices.all().get(), contains(DataStream.getDefaultBackingIndexName("logs-foobar", 1))); + assertThat(authorizedIndices.check(DataStream.getDefaultBackingIndexName("logs-foobar", 1)), is(true)); // only the single backing index will be in the resolved list since the request does not support data streams // but one of the backing indices matched the requested pattern @@ -2214,12 +2232,15 @@ public void testDataStreamNotAuthorizedWhenBackingIndicesAreAuthorizedViaWildcar // data streams should _not_ be in the authorized list but their backing indices that matched both the requested pattern // and the authorized pattern should be in the list - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices, not(hasItem("logs-foobar"))); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); + assertThat(authorizedIndices.all().get(), not(hasItem("logs-foobar"))); + assertThat(authorizedIndices.check("logs-foobar"), is(false)); DataStream dataStream = metadata.dataStreams().get("logs-foobar"); - assertThat(authorizedIndices, not(hasItem(indexName))); + assertThat(authorizedIndices.all().get(), not(hasItem(indexName))); + assertThat(authorizedIndices.check(indexName), is(false)); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices, hasItem(i.getName())); + assertThat(authorizedIndices.all().get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName()), is(true)); } // only the backing indices will be in the resolved list since the request does not support data streams @@ -2232,7 +2253,8 @@ public void testDataStreamNotAuthorizedWhenBackingIndicesAreAuthorizedViaWildcar ); assertThat(resolvedIndices.getLocal(), not(hasItem(dataStream.getName()))); for (Index i : dataStream.getIndices()) { - assertThat(authorizedIndices, hasItem(i.getName())); + assertThat(authorizedIndices.all().get(), hasItem(i.getName())); + assertThat(authorizedIndices.check(i.getName()), is(true)); } } @@ -2245,9 +2267,11 @@ public void testDataStreamNotAuthorizedWhenBackingIndicesAreAuthorizedViaNameAnd // data streams should _not_ be in the authorized list but a single backing index that matched the requested pattern // and the authorized name should be in the list - final Set authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); - assertThat(authorizedIndices, not(hasItem("logs-foobar"))); - assertThat(authorizedIndices, contains(DataStream.getDefaultBackingIndexName("logs-foobar", 1))); + final AuthorizedIndices authorizedIndices = buildAuthorizedIndices(user, GetAliasesAction.NAME, request); + assertThat(authorizedIndices.all().get(), not(hasItem("logs-foobar"))); + assertThat(authorizedIndices.check("logs-foobar"), is(false)); + assertThat(authorizedIndices.all().get(), contains(DataStream.getDefaultBackingIndexName("logs-foobar", 1))); + assertThat(authorizedIndices.check(DataStream.getDefaultBackingIndexName("logs-foobar", 1)), is(true)); // only the single backing index will be in the resolved list since the request does not support data streams // but one of the backing indices matched the requested pattern @@ -2261,18 +2285,19 @@ public void testDataStreamNotAuthorizedWhenBackingIndicesAreAuthorizedViaNameAnd assertThat(resolvedIndices.getLocal(), contains(DataStream.getDefaultBackingIndexName("logs-foobar", 1))); } - private Set buildAuthorizedIndices(User user, String action) { + private AuthorizedIndices buildAuthorizedIndices(User user, String action) { return buildAuthorizedIndices(user, action, TransportRequest.Empty.INSTANCE); } - private Set buildAuthorizedIndices(User user, String action, TransportRequest request) { + private AuthorizedIndices buildAuthorizedIndices(User user, String action, TransportRequest request) { PlainActionFuture rolesListener = new PlainActionFuture<>(); final Subject subject = new Subject(user, new RealmRef("test", "indices-aliases-resolver-tests", "node")); rolesStore.getRole(subject, rolesListener); return RBACEngine.resolveAuthorizedIndicesFromRole( rolesListener.actionGet(), getRequestInfo(request, action), - metadata.getIndicesLookup() + metadata.getIndicesLookup(), + () -> ignore -> {} ); } @@ -2281,11 +2306,11 @@ public static IndexMetadata.Builder indexBuilder(String index) { .settings(Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1).put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0)); } - private ResolvedIndices resolveIndices(TransportRequest request, Set authorizedIndices) { + private ResolvedIndices resolveIndices(TransportRequest request, AuthorizedIndices authorizedIndices) { return resolveIndices("indices:/" + randomAlphaOfLength(8), request, authorizedIndices); } - private ResolvedIndices resolveIndices(String action, TransportRequest request, Set authorizedIndices) { + private ResolvedIndices resolveIndices(String action, TransportRequest request, AuthorizedIndices authorizedIndices) { return defaultIndicesResolver.resolve(action, request, this.metadata, authorizedIndices); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index 1cb84b4dcada..bec673e26ac9 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -57,6 +57,7 @@ import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizedIndices; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.PrivilegesCheckResult; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.PrivilegesToCheck; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.ApplicationResourcePrivileges; @@ -89,14 +90,11 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.TreeMap; -import java.util.function.Predicate; -import java.util.function.Supplier; import java.util.stream.Collectors; import static java.util.Collections.emptyMap; @@ -120,7 +118,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -1462,7 +1459,6 @@ public void testBuildUserPrivilegeResponse() { public void testBackingIndicesAreIncludedForAuthorizedDataStreams() { final String dataStreamName = "my_data_stream"; User user = new User(randomAlphaOfLengthBetween(4, 12)); - Authentication authentication = AuthenticationTestHelper.builder().user(user).build(); Role role = Role.builder(RESTRICTED_INDICES, "test1") .cluster(Collections.singleton("all"), Collections.emptyList()) .add(IndexPrivilege.READ, dataStreamName) @@ -1485,24 +1481,26 @@ public void testBackingIndicesAreIncludedForAuthorizedDataStreams() { } SearchRequest request = new SearchRequest("*"); - Set authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( + AuthorizedIndices authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( role, getRequestInfo(request, SearchAction.NAME), - lookup + lookup, + () -> ignore -> {} ); - // The authorized indices is the lazily loading set implementation - assertThat(authorizedIndices, instanceOf(RBACEngine.AuthorizedIndicesSet.class)); - assertThat(authorizedIndices, hasItem(dataStreamName)); + assertThat(authorizedIndices.all().get(), hasItem(dataStreamName)); + assertThat(authorizedIndices.check(dataStreamName), is(true)); assertThat( - authorizedIndices, + authorizedIndices.all().get(), hasItems(backingIndices.stream().map(im -> im.getIndex().getName()).collect(Collectors.toList()).toArray(Strings.EMPTY_ARRAY)) ); + for (String index : backingIndices.stream().map(im -> im.getIndex().getName()).toList()) { + assertThat(authorizedIndices.check(index), is(true)); + } } public void testExplicitMappingUpdatesAreNotGrantedWithIngestPrivileges() { final String dataStreamName = "my_data_stream"; User user = new User(randomAlphaOfLengthBetween(4, 12)); - Authentication authentication = AuthenticationTestHelper.builder().user(user).build(); Role role = Role.builder(RESTRICTED_INDICES, "test1") .cluster(Collections.emptySet(), Collections.emptyList()) .add(IndexPrivilege.CREATE, "my_*") @@ -1527,14 +1525,13 @@ public void testExplicitMappingUpdatesAreNotGrantedWithIngestPrivileges() { PutMappingRequest request = new PutMappingRequest("*"); request.source("{ \"properties\": { \"message\": { \"type\": \"text\" } } }", XContentType.JSON); - Set authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( + AuthorizedIndices authorizedIndices = RBACEngine.resolveAuthorizedIndicesFromRole( role, getRequestInfo(request, PutMappingAction.NAME), - lookup + lookup, + () -> ignore -> {} ); - // The authorized indices is the lazily loading set implementation - assertThat(authorizedIndices, instanceOf(RBACEngine.AuthorizedIndicesSet.class)); - assertThat(authorizedIndices.isEmpty(), is(true)); + assertThat(authorizedIndices.all().get().isEmpty(), is(true)); } public void testNoInfiniteRecursionForRBACAuthorizationInfoHashCode() { @@ -1544,54 +1541,6 @@ public void testNoInfiniteRecursionForRBACAuthorizationInfoHashCode() { new RBACAuthorizationInfo(role, null).hashCode(); } - @SuppressWarnings("unchecked") - public void testLazinessForAuthorizedIndicesSet() { - final Set authorizedNames = Set.of("foo", "bar", "baz"); - final HashSet allNames = new HashSet<>(authorizedNames); - allNames.addAll(Set.of("buzz", "fiz")); - - final Supplier> supplier = mock(Supplier.class); - when(supplier.get()).thenReturn(authorizedNames); - final Predicate predicate = mock(Predicate.class); - doAnswer(invocation -> { - final String name = (String) invocation.getArguments()[0]; - return authorizedNames.contains(name); - }).when(predicate).test(anyString()); - final RBACEngine.AuthorizedIndicesSet authorizedIndicesSet = new RBACEngine.AuthorizedIndicesSet(supplier, predicate); - - // Check with contains or containsAll do not trigger loading - final String name1 = randomFrom(allNames); - final String name2 = randomValueOtherThan(name1, () -> randomFrom(allNames)); - final boolean containsAll = randomBoolean(); - if (containsAll) { - assertThat(authorizedIndicesSet.containsAll(Set.of(name1, name2)), equalTo(authorizedNames.containsAll(Set.of(name1, name2)))); - } else { - assertThat(authorizedIndicesSet.contains(name1), equalTo(authorizedNames.contains(name1))); - } - verify(supplier, never()).get(); - verify(predicate, atLeastOnce()).test(anyString()); - - // Iterating through the set triggers loading - Mockito.clearInvocations(predicate); - final Set collectedNames = new HashSet<>(); - for (String name : authorizedIndicesSet) { - collectedNames.add(name); - } - verify(supplier).get(); - assertThat(collectedNames, equalTo(authorizedNames)); - - // Check with contains and containsAll again now uses the loaded set not the predicate anymore - Mockito.clearInvocations(supplier); - if (containsAll) { - assertThat(authorizedIndicesSet.containsAll(Set.of(name1, name2)), equalTo(authorizedNames.containsAll(Set.of(name1, name2)))); - } else { - assertThat(authorizedIndicesSet.contains(name1), equalTo(authorizedNames.contains(name1))); - } - verify(predicate, never()).test(anyString()); - // It also does not load twice - verify(supplier, never()).get(); - } - public void testGetUserPrivilegesThrowsIaeForUnsupportedOperation() { final RBACAuthorizationInfo authorizationInfo = mock(RBACAuthorizationInfo.class); final Role role = mock(Role.class); From 5b62b1f4e24937b9c3f19e203207009fbd134300 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Tue, 22 Nov 2022 14:36:10 +0100 Subject: [PATCH 030/919] Add byte order bootstrap check (#91801) Move little endian byte order checks to a single bootstrap check. Originated from #90745 --- .../bootstrap/BootstrapChecks.java | 17 ++++++++++++++++ .../common/util/BigDoubleArray.java | 6 ------ .../common/util/BigIntArray.java | 6 ------ .../bootstrap/BootstrapChecksTests.java | 20 +++++++++++++++++++ 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/bootstrap/BootstrapChecks.java b/server/src/main/java/org/elasticsearch/bootstrap/BootstrapChecks.java index 0399624609c4..b9610c689f92 100644 --- a/server/src/main/java/org/elasticsearch/bootstrap/BootstrapChecks.java +++ b/server/src/main/java/org/elasticsearch/bootstrap/BootstrapChecks.java @@ -25,6 +25,7 @@ import java.io.BufferedReader; import java.io.IOException; +import java.nio.ByteOrder; import java.nio.file.Files; import java.nio.file.Path; import java.security.AllPermission; @@ -205,6 +206,7 @@ static List checks() { checks.add(new EarlyAccessCheck()); checks.add(new AllPermissionCheck()); checks.add(new DiscoveryConfiguredCheck()); + checks.add(new ByteOrderCheck()); return Collections.unmodifiableList(checks); } @@ -702,4 +704,19 @@ public BootstrapCheckResult check(BootstrapContext context) { ); } } + + static class ByteOrderCheck implements BootstrapCheck { + + @Override + public BootstrapCheckResult check(BootstrapContext context) { + if (nativeByteOrder() != ByteOrder.LITTLE_ENDIAN) { + return BootstrapCheckResult.failure("Little-endian native byte order is required to run Elasticsearch"); + } + return BootstrapCheckResult.success(); + } + + ByteOrder nativeByteOrder() { + return ByteOrder.nativeOrder(); + } + } } diff --git a/server/src/main/java/org/elasticsearch/common/util/BigDoubleArray.java b/server/src/main/java/org/elasticsearch/common/util/BigDoubleArray.java index ecfbfc5b9c6b..5a411563660d 100644 --- a/server/src/main/java/org/elasticsearch/common/util/BigDoubleArray.java +++ b/server/src/main/java/org/elasticsearch/common/util/BigDoubleArray.java @@ -26,12 +26,6 @@ */ final class BigDoubleArray extends AbstractBigArray implements DoubleArray { - static { - if (ByteOrder.nativeOrder() != ByteOrder.LITTLE_ENDIAN) { - throw new Error("The deserialization assumes this class is written with little-endian numbers."); - } - } - private static final BigDoubleArray ESTIMATOR = new BigDoubleArray(0, BigArrays.NON_RECYCLING_INSTANCE, false); static final VarHandle VH_PLATFORM_NATIVE_DOUBLE = MethodHandles.byteArrayViewVarHandle(double[].class, ByteOrder.nativeOrder()); diff --git a/server/src/main/java/org/elasticsearch/common/util/BigIntArray.java b/server/src/main/java/org/elasticsearch/common/util/BigIntArray.java index e3cf7389f7ed..b659524a8829 100644 --- a/server/src/main/java/org/elasticsearch/common/util/BigIntArray.java +++ b/server/src/main/java/org/elasticsearch/common/util/BigIntArray.java @@ -25,12 +25,6 @@ * configurable length. */ final class BigIntArray extends AbstractBigArray implements IntArray { - static { - if (ByteOrder.nativeOrder() != ByteOrder.LITTLE_ENDIAN) { - throw new Error("The deserialization assumes this class is written with little-endian ints."); - } - } - private static final BigIntArray ESTIMATOR = new BigIntArray(0, BigArrays.NON_RECYCLING_INSTANCE, false); static final VarHandle VH_PLATFORM_NATIVE_INT = MethodHandles.byteArrayViewVarHandle(int[].class, ByteOrder.nativeOrder()); diff --git a/server/src/test/java/org/elasticsearch/bootstrap/BootstrapChecksTests.java b/server/src/test/java/org/elasticsearch/bootstrap/BootstrapChecksTests.java index a56c8531ee74..843a65aa877a 100644 --- a/server/src/test/java/org/elasticsearch/bootstrap/BootstrapChecksTests.java +++ b/server/src/test/java/org/elasticsearch/bootstrap/BootstrapChecksTests.java @@ -24,6 +24,7 @@ import org.hamcrest.Matcher; import java.net.InetAddress; +import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -725,4 +726,23 @@ public void testDiscoveryConfiguredCheck() throws NodeValidationException { ensureChecksPass.accept(Settings.builder().putList(DiscoveryModule.DISCOVERY_SEED_PROVIDERS_SETTING.getKey())); ensureChecksPass.accept(Settings.builder().putList(SettingsBasedSeedHostsProvider.DISCOVERY_SEED_HOSTS_SETTING.getKey())); } + + public void testByteOrderCheck() throws NodeValidationException { + ByteOrder[] reference = new ByteOrder[] { ByteOrder.BIG_ENDIAN }; + BootstrapChecks.ByteOrderCheck byteOrderCheck = new BootstrapChecks.ByteOrderCheck() { + @Override + ByteOrder nativeByteOrder() { + return reference[0]; + } + }; + + final NodeValidationException e = expectThrows( + NodeValidationException.class, + () -> BootstrapChecks.check(emptyContext, true, List.of(byteOrderCheck)) + ); + assertThat(e.getMessage(), containsString("Little-endian native byte order is required to run Elasticsearch")); + + reference[0] = ByteOrder.LITTLE_ENDIAN; + BootstrapChecks.check(emptyContext, true, List.of(byteOrderCheck)); + } } From e7662d828b68cb52d1f96566ff621b945692f1e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Witek?= Date: Tue, 22 Nov 2022 14:45:57 +0100 Subject: [PATCH 031/919] [Transform] Skip remote clusters when performing up front privileges validation (#91788) --- docs/changelog/91788.yaml | 5 ++ .../test/multi_cluster/80_transform.yml | 17 ++---- .../action/TransformPrivilegeChecker.java | 27 ++++++--- .../TransformPrivilegeCheckerTests.java | 57 ++++++++++++++++++- 4 files changed, 85 insertions(+), 21 deletions(-) create mode 100644 docs/changelog/91788.yaml diff --git a/docs/changelog/91788.yaml b/docs/changelog/91788.yaml new file mode 100644 index 000000000000..e3be7b23ef82 --- /dev/null +++ b/docs/changelog/91788.yaml @@ -0,0 +1,5 @@ +pr: 91788 +summary: Skip remote clusters when performing up front privileges validation +area: Transform +type: bug +issues: [] diff --git a/x-pack/plugin/transform/qa/multi-cluster-tests-with-security/src/test/resources/rest-api-spec/test/multi_cluster/80_transform.yml b/x-pack/plugin/transform/qa/multi-cluster-tests-with-security/src/test/resources/rest-api-spec/test/multi_cluster/80_transform.yml index fe0b930cc193..0fc5b7fa3b3a 100644 --- a/x-pack/plugin/transform/qa/multi-cluster-tests-with-security/src/test/resources/rest-api-spec/test/multi_cluster/80_transform.yml +++ b/x-pack/plugin/transform/qa/multi-cluster-tests-with-security/src/test/resources/rest-api-spec/test/multi_cluster/80_transform.yml @@ -26,7 +26,6 @@ setup: - do: security.put_role: name: "x_cluster_role" - # gh#72715: the my_remote_cluster privileges should not be needed body: > { "cluster": [], @@ -38,10 +37,6 @@ setup: { "names": ["simple-remote-transform*", "simple-local-remote-transform", "same-index-local-and-remote-transform"], "privileges": ["create_index", "index", "read"] - }, - { - "names": ["my_remote_cluster:remote_test_i*", "my_remote_cluster:aliased_test_index", "my_remote_cluster:same_index_local_and_remote"], - "privileges": ["read", "view_index_metadata"] } ] } @@ -156,7 +151,7 @@ teardown: transform_id: "simple-remote-transform" - do: - catch: /Cannot preview transform \[simple-remote-transform\] because user bob lacks the required permissions \[my_remote_cluster:remote_test_index\*:\[read, view_index_metadata\], simple-remote-transform:\[\]\]/ + catch: /Source indices have been deleted or closed./ headers: { Authorization: "Basic Ym9iOnRyYW5zZm9ybS1wYXNzd29yZA==" } # This is bob transform.preview_transform: transform_id: "simple-remote-transform" @@ -311,14 +306,13 @@ teardown: --- "Batch transform from remote cluster when the user is not authorized": - do: - catch: /Cannot create transform \[simple-remote-transform\] because user bob lacks the required permissions \[my_remote_cluster:remote_test_index:\[read, view_index_metadata\], simple-remote-transform:\[\]\]/ headers: { Authorization: "Basic Ym9iOnRyYW5zZm9ybS1wYXNzd29yZA==" } # This is bob transform.put_transform: - transform_id: "simple-remote-transform" + transform_id: "simple-remote-transform-3" body: > { "source": { "index": "my_remote_cluster:remote_test_index" }, - "dest": { "index": "simple-remote-transform" }, + "dest": { "index": "simple-remote-transform-3" }, "pivot": { "group_by": { "user": {"terms": {"field": "user"}}}, "aggs": { "avg_stars": {"avg": {"field": "stars"}}} @@ -342,7 +336,6 @@ teardown: } - match: { acknowledged: true } - do: - catch: /Cannot update transform \[simple-remote-transform-2\] because user bob lacks the required permissions \[my_remote_cluster:remote_test_index:\[read, view_index_metadata\], simple-remote-transform-2:\[\]\]/ headers: { Authorization: "Basic Ym9iOnRyYW5zZm9ybS1wYXNzd29yZA==" } # This is bob transform.update_transform: transform_id: "simple-remote-transform-2" @@ -355,7 +348,7 @@ teardown: --- "Batch transform preview from remote cluster when the user is not authorized": - do: - catch: /Cannot preview transform \[transform-preview\] because user bob lacks the required permissions \[my_remote_cluster:remote_test_index:\[read, view_index_metadata\], simple-remote-transform-2:\[\]\]/ + catch: /Source indices have been deleted or closed./ headers: { Authorization: "Basic Ym9iOnRyYW5zZm9ybS1wYXNzd29yZA==" } # This is bob transform.preview_transform: body: > @@ -368,7 +361,7 @@ teardown: } } - do: - catch: /Cannot preview transform \[transform-preview\] because user bob lacks the required permissions \[my_remote_cluster:test_index:\[read, view_index_metadata\]\]/ + catch: /Source indices have been deleted or closed./ headers: { Authorization: "Basic Ym9iOnRyYW5zZm9ybS1wYXNzd29yZA==" } # This is bob transform.preview_transform: body: > diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransformPrivilegeChecker.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransformPrivilegeChecker.java index 07ef74396672..453877e0ed31 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransformPrivilegeChecker.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransformPrivilegeChecker.java @@ -13,6 +13,7 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.common.Strings; +import org.elasticsearch.license.RemoteClusterLicenseChecker; import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest; @@ -23,9 +24,11 @@ import org.elasticsearch.xpack.core.transform.transforms.TransformConfig; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; +import static java.util.function.Predicate.not; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; import static org.elasticsearch.xpack.transform.utils.SecondaryAuthorizationUtils.useSecondaryAuthIfAvailable; @@ -60,7 +63,11 @@ static void checkPrivileges( username, checkDestIndexPrivileges ); - client.execute(HasPrivilegesAction.INSTANCE, hasPrivilegesRequest, hasPrivilegesResponseListener); + if (hasPrivilegesRequest.indexPrivileges().length == 0) { + listener.onResponse(null); + } else { + client.execute(HasPrivilegesAction.INSTANCE, hasPrivilegesRequest, hasPrivilegesResponseListener); + } }); } @@ -73,13 +80,19 @@ private static HasPrivilegesRequest buildPrivilegesRequest( ) { List indicesPrivileges = new ArrayList<>(2); - RoleDescriptor.IndicesPrivileges sourceIndexPrivileges = RoleDescriptor.IndicesPrivileges.builder() - .indices(config.getSource().getIndex()) - // We need to read the source indices mapping to deduce the destination mapping, hence the need for view_index_metadata - .privileges("read", "view_index_metadata") - .build(); - indicesPrivileges.add(sourceIndexPrivileges); + // TODO: Remove this filter once https://github.com/elastic/elasticsearch/issues/67798 is fixed. + String[] sourceIndex = Arrays.stream(config.getSource().getIndex()) + .filter(not(RemoteClusterLicenseChecker::isRemoteIndex)) + .toArray(String[]::new); + if (sourceIndex.length > 0) { + RoleDescriptor.IndicesPrivileges sourceIndexPrivileges = RoleDescriptor.IndicesPrivileges.builder() + .indices(sourceIndex) + // We need to read the source indices mapping to deduce the destination mapping, hence the need for view_index_metadata + .privileges("read", "view_index_metadata") + .build(); + indicesPrivileges.add(sourceIndexPrivileges); + } if (checkDestIndexPrivileges) { final String destIndex = config.getDestination().getIndex(); final String[] concreteDest = indexNameExpressionResolver.concreteIndexNames( diff --git a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/action/TransformPrivilegeCheckerTests.java b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/action/TransformPrivilegeCheckerTests.java index 1386a1aa1ea5..6892e68015f1 100644 --- a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/action/TransformPrivilegeCheckerTests.java +++ b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/action/TransformPrivilegeCheckerTests.java @@ -45,6 +45,7 @@ import static org.hamcrest.Matchers.emptyArray; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; public class TransformPrivilegeCheckerTests extends ESTestCase { @@ -61,6 +62,7 @@ public class TransformPrivilegeCheckerTests extends ESTestCase { .addPrivilege("view_index_metadata", true) .addPrivilege("read", false) .build(); + private static final String REMOTE_SOURCE_INDEX_NAME = "some-remote-cluster:some-remote-source-index"; private static final String DEST_INDEX_NAME = "some-dest-index"; private static final ResourcePrivileges DEST_INDEX_NAME_PRIVILEGES = ResourcePrivileges.builder(DEST_INDEX_NAME) .addPrivilege("index", true) @@ -103,20 +105,23 @@ public void tearDownClient() { } public void testCheckPrivileges_NoCheckDestIndexPrivileges() { + TransformConfig config = new TransformConfig.Builder(TRANSFORM_CONFIG).setSource( + new SourceConfig(SOURCE_INDEX_NAME, REMOTE_SOURCE_INDEX_NAME) + ).build(); TransformPrivilegeChecker.checkPrivileges( OPERATION_NAME, securityContext, indexNameExpressionResolver, ClusterState.EMPTY_STATE, client, - TRANSFORM_CONFIG, + config, false, ActionListener.wrap(aVoid -> { HasPrivilegesRequest request = client.lastHasPrivilegesRequest; assertThat(request.username(), is(equalTo(USER_NAME))); assertThat(request.applicationPrivileges(), is(emptyArray())); assertThat(request.clusterPrivileges(), is(emptyArray())); - assertThat(request.indexPrivileges(), is(arrayWithSize(1))); + assertThat(request.indexPrivileges(), is(arrayWithSize(1))); // remote index is filtered out RoleDescriptor.IndicesPrivileges sourceIndicesPrivileges = request.indexPrivileges()[0]; assertThat(sourceIndicesPrivileges.getIndices(), is(arrayContaining(SOURCE_INDEX_NAME))); assertThat(sourceIndicesPrivileges.getPrivileges(), is(arrayContaining("read", "view_index_metadata"))); @@ -124,6 +129,24 @@ public void testCheckPrivileges_NoCheckDestIndexPrivileges() { ); } + public void testCheckPrivileges_NoLocalIndices_NoCheckDestIndexPrivileges() { + TransformConfig config = new TransformConfig.Builder(TRANSFORM_CONFIG).setSource(new SourceConfig(REMOTE_SOURCE_INDEX_NAME)) + .build(); + TransformPrivilegeChecker.checkPrivileges( + OPERATION_NAME, + securityContext, + indexNameExpressionResolver, + ClusterState.EMPTY_STATE, + client, + config, + false, + ActionListener.wrap(aVoid -> { + // _has_privileges API is not called for the remote index + assertThat(client.lastHasPrivilegesRequest, is(nullValue())); + }, e -> fail(e.getMessage())) + ); + } + public void testCheckPrivileges_CheckDestIndexPrivileges_DestIndexDoesNotExist() { TransformPrivilegeChecker.checkPrivileges( OPERATION_NAME, @@ -180,6 +203,36 @@ public void testCheckPrivileges_CheckDestIndexPrivileges_DestIndexExists() { ); } + public void testCheckPrivileges_NoLocalIndices_CheckDestIndexPrivileges_DestIndexExists() { + ClusterState clusterState = ClusterState.builder(ClusterName.DEFAULT) + .metadata( + Metadata.builder() + .put(IndexMetadata.builder(DEST_INDEX_NAME).settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(0)) + ) + .build(); + TransformConfig config = new TransformConfig.Builder(TRANSFORM_CONFIG).setSource(new SourceConfig(REMOTE_SOURCE_INDEX_NAME)) + .build(); + TransformPrivilegeChecker.checkPrivileges( + OPERATION_NAME, + securityContext, + indexNameExpressionResolver, + clusterState, + client, + config, + true, + ActionListener.wrap(aVoid -> { + HasPrivilegesRequest request = client.lastHasPrivilegesRequest; + assertThat(request.username(), is(equalTo(USER_NAME))); + assertThat(request.applicationPrivileges(), is(emptyArray())); + assertThat(request.clusterPrivileges(), is(emptyArray())); + assertThat(request.indexPrivileges(), is(arrayWithSize(1))); + RoleDescriptor.IndicesPrivileges destIndicesPrivileges = request.indexPrivileges()[0]; + assertThat(destIndicesPrivileges.getIndices(), is(arrayContaining(DEST_INDEX_NAME))); + assertThat(destIndicesPrivileges.getPrivileges(), is(arrayContaining("read", "index"))); + }, e -> fail(e.getMessage())) + ); + } + public void testCheckPrivileges_CheckDestIndexPrivileges_DestIndexExistsWithRetentionPolicy() { ClusterState clusterState = ClusterState.builder(ClusterName.DEFAULT) .metadata( From 763150001ce43c55d98f345e8104627592fc77ba Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 22 Nov 2022 14:13:46 +0000 Subject: [PATCH 032/919] Execute async cleanup tasks in CoordinatorTests (#91794) Closing the `JoinValidationService` will enqueue a cache-cleaning task which must be executed to release any pages held by the cache. This commit also introduces a deterministic leak detector to replace the one based on garbage collection, because in these tests we know exactly the expected lifecycle of all allocated pages. Closes #91599 Closes #91379 Closes #90576 --- .../AbstractCoordinatorTestCase.java | 13 ++-- .../CountingPageCacheRecycler.java | 61 +++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 test/framework/src/main/java/org/elasticsearch/cluster/coordination/CountingPageCacheRecycler.java diff --git a/test/framework/src/main/java/org/elasticsearch/cluster/coordination/AbstractCoordinatorTestCase.java b/test/framework/src/main/java/org/elasticsearch/cluster/coordination/AbstractCoordinatorTestCase.java index dd3247147f67..92200734b646 100644 --- a/test/framework/src/main/java/org/elasticsearch/cluster/coordination/AbstractCoordinatorTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/cluster/coordination/AbstractCoordinatorTestCase.java @@ -53,7 +53,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.unit.ByteSizeValue; -import org.elasticsearch.common.util.MockPageCacheRecycler; import org.elasticsearch.common.util.concurrent.DeterministicTaskQueue; import org.elasticsearch.common.util.concurrent.PrioritizedEsThreadPoolExecutor; import org.elasticsearch.common.util.set.Sets; @@ -271,6 +270,7 @@ public class Cluster implements Releasable { private final Map committedStatesByVersion = new HashMap<>(); private final LinearizabilityChecker linearizabilityChecker = new LinearizabilityChecker(); private final History history = new History(); + private final CountingPageCacheRecycler countingPageCacheRecycler; private final Recycler recycler; private final NodeHealthService nodeHealthService; @@ -289,9 +289,8 @@ public Cluster(int initialNodeCount, boolean allNodesMasterEligible, Settings no Cluster(int initialNodeCount, boolean allNodesMasterEligible, Settings nodeSettings, NodeHealthService nodeHealthService) { this.nodeHealthService = nodeHealthService; - this.recycler = usually() - ? BytesRefRecycler.NON_RECYCLING_INSTANCE - : new BytesRefRecycler(new MockPageCacheRecycler(Settings.EMPTY)); + this.countingPageCacheRecycler = new CountingPageCacheRecycler(); + this.recycler = new BytesRefRecycler(countingPageCacheRecycler); deterministicTaskQueue.setExecutionDelayVariabilityMillis(DEFAULT_DELAY_VARIABILITY); assertThat(initialNodeCount, greaterThan(0)); @@ -876,6 +875,12 @@ public void close() { } clusterNodes.forEach(ClusterNode::close); + + // Closing nodes may spawn some other background cleanup tasks that must also be run + runFor(DEFAULT_DELAY_VARIABILITY, "accumulate close-time tasks"); + deterministicTaskQueue.runAllRunnableTasks(); + + countingPageCacheRecycler.assertAllPagesReleased(); } protected List extraNamedWriteables() { diff --git a/test/framework/src/main/java/org/elasticsearch/cluster/coordination/CountingPageCacheRecycler.java b/test/framework/src/main/java/org/elasticsearch/cluster/coordination/CountingPageCacheRecycler.java new file mode 100644 index 000000000000..788a943e10d2 --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/cluster/coordination/CountingPageCacheRecycler.java @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.cluster.coordination; + +import org.elasticsearch.common.recycler.Recycler; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.PageCacheRecycler; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class CountingPageCacheRecycler extends PageCacheRecycler { + + private int openPages = 0; + + public CountingPageCacheRecycler() { + super(Settings.EMPTY); + } + + @Override + public Recycler.V bytePage(boolean clear) { + final var page = super.bytePage(clear); + openPages += 1; + return new Recycler.V<>() { + boolean closed = false; + + @Override + public byte[] v() { + return page.v(); + } + + @Override + public boolean isRecycled() { + return page.isRecycled(); + } + + @Override + public void close() { + assertFalse(closed); + closed = true; + openPages -= 1; + page.close(); + } + }; + } + + @Override + public Recycler.V objectPage() { + throw new AssertionError("unexpected call to objectPage()"); + } + + public void assertAllPagesReleased() { + assertEquals(0, openPages); + } +} From e16bee0e72180c03b5d43ec651abd8f3c33bdbdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Tue, 22 Nov 2022 15:18:08 +0100 Subject: [PATCH 033/919] [DOCS] Adds bullet points to the statuses of the health object in transform stats API docs (#91790) --- docs/reference/rest-api/common-parms.asciidoc | 18 +++++++++--------- .../apis/get-transform-stats.asciidoc | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/reference/rest-api/common-parms.asciidoc b/docs/reference/rest-api/common-parms.asciidoc index 39e8d52824bc..92141b9d1952 100644 --- a/docs/reference/rest-api/common-parms.asciidoc +++ b/docs/reference/rest-api/common-parms.asciidoc @@ -126,17 +126,17 @@ tag::cluster-health-status[] Health status of the cluster, based on the state of its primary and replica shards. Statuses are: - `green`::: - All shards are assigned. + * `green`: + All shards are assigned. - `yellow`::: - All primary shards are assigned, but one or more replica shards are - unassigned. If a node in the cluster fails, some - data could be unavailable until that node is repaired. + * `yellow`: + All primary shards are assigned, but one or more replica shards are + unassigned. If a node in the cluster fails, some data could be unavailable + until that node is repaired. - `red`::: - One or more primary shards are unassigned, so some data is unavailable. This - can occur briefly during cluster startup as primary shards are assigned. + * `red`: + One or more primary shards are unassigned, so some data is unavailable. This + can occur briefly during cluster startup as primary shards are assigned. end::cluster-health-status[] tag::committed[] diff --git a/docs/reference/transform/apis/get-transform-stats.asciidoc b/docs/reference/transform/apis/get-transform-stats.asciidoc index 5ae4cf4bd497..2a7ed1913546 100644 --- a/docs/reference/transform/apis/get-transform-stats.asciidoc +++ b/docs/reference/transform/apis/get-transform-stats.asciidoc @@ -152,18 +152,18 @@ that the {transform} is failing to keep up. `status`:: (string) Health status of this transform. Statuses are: - `green`::: - The transform is healthy. + * `green`: + The transform is healthy. - `unknown`::: - The health of the transform could not be determined. + * `unknown`: + The health of the transform could not be determined. - `yellow`::: - The functionality of the transform is in a degraded state and may need remediation - to avoid the health becoming `red`. + * `yellow`: + The functionality of the transform is in a degraded state and may need + remediation to avoid the health becoming `red`. - `red`::: - The transform is experiencing an outage or is unavailable for use. + * `red`: + The transform is experiencing an outage or is unavailable for use. `issues`:: (Optional, array) If a non-healthy status is returned, contains a list of issues From df66f67165fdfb33a5a75c8f408d7217276ac327 Mon Sep 17 00:00:00 2001 From: David Kyle Date: Tue, 22 Nov 2022 14:20:48 +0000 Subject: [PATCH 034/919] [ML] Audit a message every day the datafeed has seen no data (#91774) --- docs/changelog/91774.yaml | 5 +++ .../xpack/core/ml/job/messages/Messages.java | 1 + .../xpack/ml/datafeed/DatafeedJob.java | 4 ++ .../xpack/ml/datafeed/DatafeedRunner.java | 42 ++++++++++++------- .../xpack/ml/datafeed/ProblemTracker.java | 9 ++-- .../ml/datafeed/DatafeedRunnerTests.java | 1 + .../ml/datafeed/ProblemTrackerTests.java | 24 ++++++++++- 7 files changed, 67 insertions(+), 19 deletions(-) create mode 100644 docs/changelog/91774.yaml diff --git a/docs/changelog/91774.yaml b/docs/changelog/91774.yaml new file mode 100644 index 000000000000..4e685e200b8d --- /dev/null +++ b/docs/changelog/91774.yaml @@ -0,0 +1,5 @@ +pr: 91774 +summary: Audit a message every day the datafeed has seen no data +area: Machine Learning +type: enhancement +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/messages/Messages.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/messages/Messages.java index 895bc8e261d8..e80cac65f871 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/messages/Messages.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/messages/Messages.java @@ -158,6 +158,7 @@ public final class Messages { public static final String JOB_AUDIT_DATAFEED_STARTED_FROM_TO = "Datafeed started (from: {0} to: {1}) with frequency [{2}]"; public static final String JOB_AUDIT_DATAFEED_STARTED_REALTIME = "Datafeed started in real-time"; public static final String JOB_AUDIT_DATAFEED_STOPPED = "Datafeed stopped"; + public static final String JOB_AUDIT_DATAFEED_STOPPED_WITH_REASON = "Datafeed stopped with reason [{0}]"; public static final String JOB_AUDIT_DATAFEED_ISOLATED = "Datafeed isolated"; public static final String JOB_AUDIT_DELETING = "Deleting job by task with id ''{0}''"; public static final String JOB_AUDIT_DELETING_FAILED = "Error deleting job: {0}"; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedJob.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedJob.java index 0e70fb689563..fc5c8b5b35fe 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedJob.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedJob.java @@ -137,6 +137,10 @@ public Integer getMaxEmptySearches() { return maxEmptySearches; } + public long numberOfSearchesIn24Hours() { + return (60_000 * 60 * 24) / frequencyMs; + } + public void finishReportingTimingStats() { timingStatsReporter.finishReporting(); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedRunner.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedRunner.java index e5bc6fde7847..5341b44b013c 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedRunner.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedRunner.java @@ -96,7 +96,13 @@ public DatafeedRunner( public void run(TransportStartDatafeedAction.DatafeedTask task, Consumer finishHandler) { ActionListener datafeedJobHandler = ActionListener.wrap(datafeedJob -> { String jobId = datafeedJob.getJobId(); - Holder holder = new Holder(task, task.getDatafeedId(), datafeedJob, new ProblemTracker(auditor, jobId), finishHandler); + Holder holder = new Holder( + task, + task.getDatafeedId(), + datafeedJob, + new ProblemTracker(auditor, jobId, datafeedJob.numberOfSearchesIn24Hours()), + finishHandler + ); StoppedOrIsolated stoppedOrIsolated = task.executeIfNotStoppedOrIsolated( () -> runningDatafeedsOnThisNode.put(task.getAllocationId(), holder) ); @@ -313,15 +319,15 @@ protected void doRun() { } catch (DatafeedJob.EmptyDataCountException e) { int emptyDataCount = holder.problemTracker.reportEmptyDataCount(); if (e.haveEverSeenData == false && holder.shouldStopAfterEmptyData(emptyDataCount)) { - logger.warn( - "Datafeed for [" - + jobId - + "] has seen no data in [" - + emptyDataCount - + "] attempts, and never seen any data previously, so stopping..." - ); + String noDataMessage = "Datafeed for [" + + jobId + + "] has seen no data in [" + + emptyDataCount + + "] attempts, and never seen any data previously, so stopping..."; + logger.warn(noDataMessage); + // In this case we auto-close the job, as though a lookback-only datafeed stopped - holder.stop("no_data", TimeValue.timeValueSeconds(20), e, true); + holder.stop("no_data", TimeValue.timeValueSeconds(20), e, true, noDataMessage); return; } nextDelayInMsSinceEpoch = e.nextDelayInMsSinceEpoch; @@ -432,10 +438,10 @@ boolean isIsolated() { } public void stop(String source, TimeValue timeout, Exception e) { - stop(source, timeout, e, defaultAutoCloseJob); + stop(source, timeout, e, defaultAutoCloseJob, null); } - public void stop(String source, TimeValue timeout, Exception e, boolean autoCloseJob) { + public void stop(String source, TimeValue timeout, Exception e, boolean autoCloseJob, String stoppedReason) { if (isNodeShuttingDown) { return; } @@ -466,10 +472,16 @@ public void stop(String source, TimeValue timeout, Exception e, boolean autoClos if (cancellable != null) { cancellable.cancel(); } - auditor.info( - datafeedJob.getJobId(), - Messages.getMessage(isIsolated() ? Messages.JOB_AUDIT_DATAFEED_ISOLATED : Messages.JOB_AUDIT_DATAFEED_STOPPED) - ); + String auditMessage; + if (isIsolated()) { + auditMessage = Messages.getMessage(Messages.JOB_AUDIT_DATAFEED_ISOLATED); + } else { + auditMessage = stoppedReason == null + ? Messages.getMessage(Messages.JOB_AUDIT_DATAFEED_STOPPED) + : Messages.getMessage(Messages.JOB_AUDIT_DATAFEED_STOPPED_WITH_REASON, stoppedReason); + } + auditor.info(datafeedJob.getJobId(), auditMessage); + datafeedJob.finishReportingTimingStats(); finishHandler.accept(e); logger.info( diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/ProblemTracker.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/ProblemTracker.java index 30873715ad1c..8a5ea60af89e 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/ProblemTracker.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/ProblemTracker.java @@ -35,10 +35,12 @@ class ProblemTracker { private volatile boolean hadProblems; private volatile String previousProblem; private volatile int emptyDataCount; + private final long numberOfSearchesInADay; - ProblemTracker(AnomalyDetectionAuditor auditor, String jobId) { + ProblemTracker(AnomalyDetectionAuditor auditor, String jobId, long numberOfSearchesInADay) { this.auditor = Objects.requireNonNull(auditor); this.jobId = Objects.requireNonNull(jobId); + this.numberOfSearchesInADay = Math.max(numberOfSearchesInADay, 1); } /** @@ -74,10 +76,11 @@ private void reportProblem(String template, String problemMessage) { /** * Updates the tracking of empty data cycles. If the number of consecutive empty data - * cycles reaches {@code EMPTY_DATA_WARN_COUNT}, a warning is reported. + * cycles reaches {@code EMPTY_DATA_WARN_COUNT} or the 24 hours of empty data counts + * have passed a warning is reported. */ public int reportEmptyDataCount() { - if (++emptyDataCount == EMPTY_DATA_WARN_COUNT) { + if (++emptyDataCount == EMPTY_DATA_WARN_COUNT || (emptyDataCount % numberOfSearchesInADay) == 0) { auditor.warning(jobId, Messages.getMessage(Messages.JOB_AUDIT_DATAFEED_NO_DATA)); } return emptyDataCount; diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/DatafeedRunnerTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/DatafeedRunnerTests.java index f7f993a54b63..1d2773397d80 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/DatafeedRunnerTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/DatafeedRunnerTests.java @@ -130,6 +130,7 @@ public void setUpTests() { when(datafeedJob.stop()).thenReturn(true); when(datafeedJob.getJobId()).thenReturn(job.getId()); when(datafeedJob.getMaxEmptySearches()).thenReturn(null); + when(datafeedJob.numberOfSearchesIn24Hours()).thenReturn(24L); datafeedJobBuilder = mock(DatafeedJobBuilder.class); doAnswer(invocationOnMock -> { ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/ProblemTrackerTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/ProblemTrackerTests.java index 37abb900138f..b54f3e6b0ab8 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/ProblemTrackerTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/ProblemTrackerTests.java @@ -25,10 +25,12 @@ public class ProblemTrackerTests extends ESTestCase { private ProblemTracker problemTracker; + private static final long NUM_SEARCHES_IN_DAY = 24L; + @Before public void setUpTests() { auditor = mock(AnomalyDetectionAuditor.class); - problemTracker = new ProblemTracker(auditor, "foo"); + problemTracker = new ProblemTracker(auditor, "foo", NUM_SEARCHES_IN_DAY); } public void testReportExtractionProblem() { @@ -118,6 +120,26 @@ public void testUpdateEmptyDataCount_GivenNonEmptyAfterTenEmpty() { verify(auditor).info("foo", "Datafeed has started retrieving data again"); } + public void testUpdateEmptyDataCount_DailyTrigger() { + for (int i = 0; i < NUM_SEARCHES_IN_DAY; i++) { + problemTracker.reportEmptyDataCount(); + } + verify(auditor, times(2)).warning("foo", "Datafeed has been retrieving no data for a while"); + + for (int i = 0; i < NUM_SEARCHES_IN_DAY; i++) { + problemTracker.reportEmptyDataCount(); + } + verify(auditor, times(3)).warning("foo", "Datafeed has been retrieving no data for a while"); + } + + public void testUpdateEmptyDataCount_NumSearchesInDayIsZero() { + auditor = mock(AnomalyDetectionAuditor.class); + problemTracker = new ProblemTracker(auditor, "foo", 0); + + problemTracker.reportEmptyDataCount(); + verify(auditor, times(1)).warning("foo", "Datafeed has been retrieving no data for a while"); + } + public void testFinishReport_GivenNoProblems() { problemTracker.finishReport(); From 7c6025dd274f9a40ba4e82508193d20e8c8efcca Mon Sep 17 00:00:00 2001 From: David Kilfoyle <41695641+kilfoyle@users.noreply.github.com> Date: Tue, 22 Nov 2022 09:23:40 -0500 Subject: [PATCH 035/919] Remove tech preview disclaimer from TSDS ingest docs (#91519) Co-authored-by: Elastic Machine --- docs/reference/data-streams/set-up-tsds.asciidoc | 2 -- docs/reference/data-streams/tsds-index-settings.asciidoc | 2 -- docs/reference/data-streams/tsds.asciidoc | 2 -- 3 files changed, 6 deletions(-) diff --git a/docs/reference/data-streams/set-up-tsds.asciidoc b/docs/reference/data-streams/set-up-tsds.asciidoc index bee0d40ece4a..3c15011871f8 100644 --- a/docs/reference/data-streams/set-up-tsds.asciidoc +++ b/docs/reference/data-streams/set-up-tsds.asciidoc @@ -4,8 +4,6 @@ Set up a TSDS ++++ -preview::[] - To set up a <>, follow these steps: . Check the <>. diff --git a/docs/reference/data-streams/tsds-index-settings.asciidoc b/docs/reference/data-streams/tsds-index-settings.asciidoc index dea828091c8f..8512a3841b37 100644 --- a/docs/reference/data-streams/tsds-index-settings.asciidoc +++ b/docs/reference/data-streams/tsds-index-settings.asciidoc @@ -1,8 +1,6 @@ [[tsds-index-settings]] === Time series index settings -preview::[] - Backing indices in a <> support the following index settings. diff --git a/docs/reference/data-streams/tsds.asciidoc b/docs/reference/data-streams/tsds.asciidoc index a454e1652e39..1d712853e19e 100644 --- a/docs/reference/data-streams/tsds.asciidoc +++ b/docs/reference/data-streams/tsds.asciidoc @@ -1,8 +1,6 @@ [[tsds]] == Time series data stream (TSDS) -preview::[] - A time series data stream (TSDS) models timestamped metrics data as one or more time series. From 99415818e27192f2cd8adfd9c85f44ac2734e25c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Tue, 22 Nov 2022 18:08:06 +0100 Subject: [PATCH 036/919] [DOCS] Adds semantic search API to the trained model API list (#91815) --- .../ml/trained-models/apis/ml-trained-models-apis.asciidoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/reference/ml/trained-models/apis/ml-trained-models-apis.asciidoc b/docs/reference/ml/trained-models/apis/ml-trained-models-apis.asciidoc index 83ef3c49fb5e..b45719d28d9d 100644 --- a/docs/reference/ml/trained-models/apis/ml-trained-models-apis.asciidoc +++ b/docs/reference/ml/trained-models/apis/ml-trained-models-apis.asciidoc @@ -23,3 +23,7 @@ an aggregation. Refer to the following documentation to learn more: * <> * <> + +You can use your trained model to perform semantic search: + +* <> From 9af1c278f8767f3f3fd3d979e08a726663986188 Mon Sep 17 00:00:00 2001 From: Adam Locke Date: Tue, 22 Nov 2022 12:13:36 -0500 Subject: [PATCH 037/919] [DOCS] Remove extra ports in Docker command (#91118) The current command to start the first Elasticsearch node includes `-p 9200:9200` and `-p 9300:9300`. This PR removes the extra ports so that they're not open unnecessarily. Co-authored-by: Adam Locke --- docs/reference/setup/install/docker.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/setup/install/docker.asciidoc b/docs/reference/setup/install/docker.asciidoc index 404e1f718c3b..6e51d27792de 100644 --- a/docs/reference/setup/install/docker.asciidoc +++ b/docs/reference/setup/install/docker.asciidoc @@ -96,7 +96,7 @@ endif::[] ifeval::["{release-state}"!="unreleased"] [source,sh,subs="attributes"] ---- -docker run --name es01 --net elastic -p 9200:9200 -p 9300:9300 -it {docker-image} +docker run --name es01 --net elastic -p 9200:9200 -it {docker-image} ---- endif::[] From 03391f2656d981310804ae19aa857ce6f338c7ba Mon Sep 17 00:00:00 2001 From: Bogdan Pintea Date: Tue, 22 Nov 2022 19:07:08 +0100 Subject: [PATCH 038/919] Bump versions after 8.5.2 release --- .ci/bwcVersions | 1 + .ci/snapshotBwcVersions | 2 +- server/src/main/java/org/elasticsearch/Version.java | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.ci/bwcVersions b/.ci/bwcVersions index 81181301895e..c0b3dbb12249 100644 --- a/.ci/bwcVersions +++ b/.ci/bwcVersions @@ -78,5 +78,6 @@ BWC_VERSION: - "8.5.0" - "8.5.1" - "8.5.2" + - "8.5.3" - "8.6.0" - "8.7.0" diff --git a/.ci/snapshotBwcVersions b/.ci/snapshotBwcVersions index dd700ed35e2c..2c26534044ec 100644 --- a/.ci/snapshotBwcVersions +++ b/.ci/snapshotBwcVersions @@ -1,5 +1,5 @@ BWC_VERSION: - "7.17.8" - - "8.5.2" + - "8.5.3" - "8.6.0" - "8.7.0" diff --git a/server/src/main/java/org/elasticsearch/Version.java b/server/src/main/java/org/elasticsearch/Version.java index e43664f53d72..e597516840e3 100644 --- a/server/src/main/java/org/elasticsearch/Version.java +++ b/server/src/main/java/org/elasticsearch/Version.java @@ -126,6 +126,7 @@ public class Version implements Comparable, ToXContentFragment { public static final Version V_8_5_0 = new Version(8_05_00_99, org.apache.lucene.util.Version.LUCENE_9_4_1); public static final Version V_8_5_1 = new Version(8_05_01_99, org.apache.lucene.util.Version.LUCENE_9_4_1); public static final Version V_8_5_2 = new Version(8_05_02_99, org.apache.lucene.util.Version.LUCENE_9_4_1); + public static final Version V_8_5_3 = new Version(8_05_03_99, org.apache.lucene.util.Version.LUCENE_9_4_1); public static final Version V_8_6_0 = new Version(8_06_00_99, org.apache.lucene.util.Version.LUCENE_9_4_1); public static final Version V_8_7_0 = new Version(8_07_00_99, org.apache.lucene.util.Version.LUCENE_9_4_1); public static final Version CURRENT = V_8_7_0; From b00f54739f1ac2226594387038b262bf336e3540 Mon Sep 17 00:00:00 2001 From: Bogdan Pintea Date: Tue, 22 Nov 2022 19:38:45 +0100 Subject: [PATCH 039/919] Add 8.5.2 release notes (#91797) (#91826) Add 8.5.2 release notes (cherry picked from commit 63fa4184ec1965c3223ce15dd618c3c508de1162) --- docs/reference/release-notes.asciidoc | 2 ++ docs/reference/release-notes/8.5.2.asciidoc | 39 +++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 docs/reference/release-notes/8.5.2.asciidoc diff --git a/docs/reference/release-notes.asciidoc b/docs/reference/release-notes.asciidoc index b2af3c1db205..9f5a32625a3f 100644 --- a/docs/reference/release-notes.asciidoc +++ b/docs/reference/release-notes.asciidoc @@ -7,6 +7,7 @@ This section summarizes the changes in each release. * <> +* <> * <> * <> * <> @@ -36,6 +37,7 @@ This section summarizes the changes in each release. -- include::release-notes/8.7.0.asciidoc[] +include::release-notes/8.5.2.asciidoc[] include::release-notes/8.5.1.asciidoc[] include::release-notes/8.5.0.asciidoc[] include::release-notes/8.4.3.asciidoc[] diff --git a/docs/reference/release-notes/8.5.2.asciidoc b/docs/reference/release-notes/8.5.2.asciidoc new file mode 100644 index 000000000000..473cc7625374 --- /dev/null +++ b/docs/reference/release-notes/8.5.2.asciidoc @@ -0,0 +1,39 @@ +[[release-notes-8.5.2]] +== {es} version 8.5.2 + + +Also see <>. + +[[bug-8.5.2]] +[float] +=== Bug fixes + +Authorization:: +* Avoid potential unsupported operation exception in doc bitset cache {es-pull}91490[#91490] + +EQL:: +* Refine bwc version checks on `EqlSearchRequest` {es-pull}91510[#91510] + +Health:: +* SLM uneahlthy policies diagnosis recommends correct URL in action {es-pull}91506[#91506] + +Ingest Node:: +* Refactor `DatabaseNodeService` as a cluster state listener {es-pull}91567[#91567] (issue: {es-issue}86999[#86999]) + +Stats:: +* Fix NPE in IndexService getNodeMappingStats {es-pull}91334[#91334] (issue: {es-issue}91259[#91259]) + +Transform:: +* Fix failure when resolving indices from CCS {es-pull}91622[#91622] (issue: {es-issue}91550[#91550]) + +[[enhancement-8.5.2]] +[float] +=== Enhancements + +EQL:: +* Remove version limitations for CCS {es-pull}91409[#91409] + +Ingest Node:: +* Refactor enrich maintenance coordination logic {es-pull}90931[#90931] + + From 27f18492c4d7ef907676aa8c4badb8c826ec68b7 Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Tue, 22 Nov 2022 11:00:09 -0800 Subject: [PATCH 040/919] Update DRA release job triggers --- ...apshots.yml => elastic+elasticsearch+dra-snapshot.yml} | 4 ++-- ....yml => elastic+elasticsearch+dra-staging-trigger.yml} | 2 +- .ci/jobs.t/elastic+elasticsearch+dra-staging.yml | 8 +++++++- .ci/jobs.t/elastic+elasticsearch+intake.yml | 7 +++++++ 4 files changed, 17 insertions(+), 4 deletions(-) rename .ci/jobs.t/{elastic+elasticsearch+periodic+dra-snapshots.yml => elastic+elasticsearch+dra-snapshot.yml} (96%) rename .ci/jobs.t/{elastic+elasticsearch+periodic+dra-snapshots-trigger.yml => elastic+elasticsearch+dra-staging-trigger.yml} (63%) diff --git a/.ci/jobs.t/elastic+elasticsearch+periodic+dra-snapshots.yml b/.ci/jobs.t/elastic+elasticsearch+dra-snapshot.yml similarity index 96% rename from .ci/jobs.t/elastic+elasticsearch+periodic+dra-snapshots.yml rename to .ci/jobs.t/elastic+elasticsearch+dra-snapshot.yml index e0c7e7d122d3..37fdd85ee656 100644 --- a/.ci/jobs.t/elastic+elasticsearch+periodic+dra-snapshots.yml +++ b/.ci/jobs.t/elastic+elasticsearch+dra-snapshot.yml @@ -1,7 +1,7 @@ --- - job: - name: elastic+elasticsearch+%BRANCH%+periodic+dra-snapshot - workspace: /dev/shm/elastic+elasticsearch+%BRANCH%+periodic+dra-snapshot + name: elastic+elasticsearch+%BRANCH%+dra-snapshot + workspace: /dev/shm/elastic+elasticsearch+%BRANCH%+dra-snapshot display-name: "elastic / elasticsearch # %BRANCH% - DRA snapshot" description: "Publishing Daily Releasable Artifacts (DRAs) of Elasticsearch %BRANCH% snapshots.\n" node: "ubuntu-20.04" diff --git a/.ci/jobs.t/elastic+elasticsearch+periodic+dra-snapshots-trigger.yml b/.ci/jobs.t/elastic+elasticsearch+dra-staging-trigger.yml similarity index 63% rename from .ci/jobs.t/elastic+elasticsearch+periodic+dra-snapshots-trigger.yml rename to .ci/jobs.t/elastic+elasticsearch+dra-staging-trigger.yml index 7f1c639770ae..87df5e07c2e4 100644 --- a/.ci/jobs.t/elastic+elasticsearch+periodic+dra-snapshots-trigger.yml +++ b/.ci/jobs.t/elastic+elasticsearch+dra-staging-trigger.yml @@ -1,6 +1,6 @@ --- jjbb-template: periodic-trigger-lgc.yml vars: - - periodic-job: elastic+elasticsearch+%BRANCH%+periodic+dra-snapshot + - periodic-job: elastic+elasticsearch+%BRANCH%+dra-staging - lgc-job: elastic+elasticsearch+%BRANCH%+intake - cron: "H H/12 * * *" diff --git a/.ci/jobs.t/elastic+elasticsearch+dra-staging.yml b/.ci/jobs.t/elastic+elasticsearch+dra-staging.yml index d558872abeb9..40a759e20ba2 100644 --- a/.ci/jobs.t/elastic+elasticsearch+dra-staging.yml +++ b/.ci/jobs.t/elastic+elasticsearch+dra-staging.yml @@ -1,6 +1,6 @@ --- - job: - name: elastic+elasticsearch+%BRANCH%+dra-snapshot + name: elastic+elasticsearch+%BRANCH%+dra-staging workspace: /dev/shm/elastic+elasticsearch+%BRANCH%+dra-staging display-name: "elastic / elasticsearch # %BRANCH% - DRA staging" description: "Publishing Daily Releasable Artifacts (DRAs) of Elasticsearch %BRANCH% staging.\n" @@ -13,6 +13,12 @@ RUNTIME_JAVA_HOME=$HOME/.java/$ES_RUNTIME_JAVA - shell: | #!/usr/local/bin/runbld --redirect-stderr + + # Don't publish main branch to staging + if [ "%BRANCH%" == "main" ]; then + exit 0 + fi + WORKFLOW="staging" RM_BRANCH="%BRANCH%" && [[ "%BRANCH%" == "main" ]] && RM_BRANCH=master ES_VERSION=$(cat build-tools-internal/version.properties \ diff --git a/.ci/jobs.t/elastic+elasticsearch+intake.yml b/.ci/jobs.t/elastic+elasticsearch+intake.yml index f4aa32ec7e75..138318897b52 100644 --- a/.ci/jobs.t/elastic+elasticsearch+intake.yml +++ b/.ci/jobs.t/elastic+elasticsearch+intake.yml @@ -54,6 +54,13 @@ kill-phase-on: NEVER current-parameters: true git-revision: true + - multijob: + name: Publish snapshot artifacts + projects: + - name: elastic+elasticsearch+%BRANCH%+dra-snapshot + kill-phase-on: NEVER + current-parameters: true + git-revision: true - multijob: name: Update last good commit projects: From dcfe6a3253cf608db019f42a35d087d56e377f2d Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Tue, 22 Nov 2022 14:32:27 -0500 Subject: [PATCH 041/919] fix synthetic _source for sparse _doc_count field (#91769) If the `_doc_count` field is sparse we were using Lucene incorrectly to read it's values. This fixes how we interact with the iterator to load the values. Closes #91731 --- docs/changelog/91769.yaml | 6 ++ .../test/get/100_synthetic_source.yml | 94 ++++++++++++++++++- .../test/search/400_synthetic_source.yml | 60 ++++++++++++ .../index/mapper/DocCountFieldMapper.java | 10 +- .../mapper/DocCountFieldMapperTests.java | 31 ++++++ 5 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 docs/changelog/91769.yaml diff --git a/docs/changelog/91769.yaml b/docs/changelog/91769.yaml new file mode 100644 index 000000000000..02fb056ad2ea --- /dev/null +++ b/docs/changelog/91769.yaml @@ -0,0 +1,6 @@ +pr: 91769 +summary: Fix synthetic `_source` for sparse `_doc_count` field +area: TSDB +type: bug +issues: + - 91731 diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/get/100_synthetic_source.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/get/100_synthetic_source.yml index 566cc777faa4..cd83a7868528 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/get/100_synthetic_source.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/get/100_synthetic_source.yml @@ -600,6 +600,7 @@ _doc_count: _source: mode: synthetic + # with _doc_count - do: index: index: test @@ -608,7 +609,6 @@ _doc_count: body: _doc_count: 3 foo: bar - - do: get: index: test @@ -623,6 +623,98 @@ _doc_count: foo: bar - is_false: fields + # without _doc_count + - do: + index: + index: test + id: 2 + refresh: true + body: + foo: baz + - do: + get: + index: test + id: 2 + - match: {_index: "test"} + - match: {_id: "2"} + - match: {_version: 1} + - match: {found: true} + - match: + _source: + foo: baz + - is_false: fields + + # without immediately refreshing with _doc_count + - do: + index: + index: test + id: 3 + body: + _doc_count: 3 + foo: qux + - do: + get: + index: test + id: 3 + - match: {_index: "test"} + - match: {_id: "3"} + - match: {_version: 1} + - match: {found: true} + - match: + _source: + _doc_count: 3 + foo: qux + - is_false: fields + + # without immediately refreshing without _doc_count + - do: + index: + index: test + id: 4 + body: + foo: quux + - do: + get: + index: test + id: 4 + - match: {_index: "test"} + - match: {_id: "4"} + - match: {_version: 1} + - match: {found: true} + - match: + _source: + foo: quux + - is_false: fields + + # refresh all at once + - do: + indices.refresh: {} + - do: + get: + index: test + id: 3 + - match: {_index: "test"} + - match: {_id: "3"} + - match: {_version: 1} + - match: {found: true} + - match: + _source: + _doc_count: 3 + foo: qux + - is_false: fields + - do: + get: + index: test + id: 4 + - match: {_index: "test"} + - match: {_id: "4"} + - match: {_version: 1} + - match: {found: true} + - match: + _source: + foo: quux + - is_false: fields + --- ip with ignore_malformed: - skip: diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/400_synthetic_source.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/400_synthetic_source.yml index 902f7086e94d..e97fb7bbc992 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/400_synthetic_source.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/400_synthetic_source.yml @@ -436,3 +436,63 @@ _source filtering: - match: hits.hits.0._source: kwd: foo + +--- +_doc_count: + - skip: + version: " - 8.6.99" + reason: bug caused by many not having _doc_count fixed in 8.7.0 + + - do: + indices.create: + index: test + body: + settings: + number_of_replicas: 0 + mappings: + _source: + mode: synthetic + + - do: + index: + index: test + id: 2 + body: + foo: baz + - do: + index: + index: test + id: 3 + body: + foo: baz + - do: + index: + index: test + id: 4 + body: + foo: baz + - do: + index: + index: test + id: 1 + body: + _doc_count: 3 + foo: bar + - do: + indices.refresh: {} + + - do: + search: + index: test + body: + sort: foo.keyword + - is_false: hits.hits.0.fields + - is_false: hits.hits.1.fields + - match: + hits.hits.0._source: + _doc_count: 3 + foo: bar + - match: + hits.hits.1._source: + foo: baz + diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DocCountFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/DocCountFieldMapper.java index fa5b01913be8..946a0a1aa171 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DocCountFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DocCountFieldMapper.java @@ -155,7 +155,15 @@ public DocValuesLoader docValuesLoader(LeafReader leafReader, int[] docIdsInLeaf hasValue = false; return null; } - return docId -> hasValue = docId == postings.advance(docId); + return docId -> { + if (docId < postings.docID()) { + return hasValue = false; + } + if (docId == postings.docID()) { + return hasValue = true; + } + return hasValue = docId == postings.advance(docId); + }; } @Override diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DocCountFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DocCountFieldMapperTests.java index ba3d4d5b8f13..3a172e970222 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DocCountFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DocCountFieldMapperTests.java @@ -110,4 +110,35 @@ public void testSyntheticSourceMany() throws IOException { } }); } + + public void testSyntheticSourceManyDoNotHave() throws IOException { + MapperService mapper = createMapperService(syntheticSourceMapping(b -> b.startObject("doc").field("type", "integer").endObject())); + List counts = randomList(2, 10000, () -> randomBoolean() ? null : between(1, Integer.MAX_VALUE)); + withLuceneIndex(mapper, iw -> { + int d = 0; + for (Integer c : counts) { + int doc = d++; + iw.addDocument(mapper.documentMapper().parse(source(b -> { + b.field("doc", doc); + if (c != null) { + b.field(CONTENT_TYPE, c); + } + })).rootDoc()); + } + }, reader -> { + SourceLoader loader = mapper.mappingLookup().newSourceLoader(); + assertTrue(loader.requiredStoredFields().isEmpty()); + for (LeafReaderContext leaf : reader.leaves()) { + int[] docIds = IntStream.range(0, leaf.reader().maxDoc()).toArray(); + SourceLoader.Leaf sourceLoaderLeaf = loader.leaf(leaf.reader(), docIds); + LeafStoredFieldLoader storedFieldLoader = StoredFieldLoader.empty().getLoader(leaf, docIds); + for (int docId : docIds) { + String source = sourceLoaderLeaf.source(storedFieldLoader, docId).internalSourceRef().utf8ToString(); + int doc = (int) JsonXContent.jsonXContent.createParser(XContentParserConfiguration.EMPTY, source).map().get("doc"); + String docCountPart = counts.get(doc) == null ? "" : "\"_doc_count\":" + counts.get(doc) + ","; + assertThat("doc " + docId, source, equalTo("{" + docCountPart + "\"doc\":" + doc + "}")); + } + } + }); + } } From b9bb7252be68fa9fb5146d3b4c88fd41cfcb7a7b Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Tue, 22 Nov 2022 15:23:30 -0500 Subject: [PATCH 042/919] Docs: synthetic _source can't params._source (#91630) This documents that `params._source` isn't available for synthetic `_source` indices and suggests to instead use `doc['foo']` or `field('foo')`. --- docs/reference/mapping/fields/synthetic-source.asciidoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/reference/mapping/fields/synthetic-source.asciidoc b/docs/reference/mapping/fields/synthetic-source.asciidoc index 04f5e22e6e93..60b81c5f813a 100644 --- a/docs/reference/mapping/fields/synthetic-source.asciidoc +++ b/docs/reference/mapping/fields/synthetic-source.asciidoc @@ -32,6 +32,8 @@ space. There are a couple of restrictions to be aware of: * When you retrieve synthetic `_source` content it undergoes minor <> compared to the original JSON. +* The `params._source` is unavailable in scripts. Instead use the +{painless}/painless-field-context.html[`doc`] API or the <>. * Synthetic `_source` can be used with indices that contain only these field types: From 84b940c3f430e4592b9545be52edf47304ffeb1d Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Wed, 23 Nov 2022 09:08:40 +0100 Subject: [PATCH 043/919] Upgrade Lucene to version 9.4.2. (#91823) Most interestingly, this includes a fix for KNN vectors: apache/lucene#11905. --- build-tools-internal/version.properties | 2 +- build.gradle | 4 +- docs/Versions.asciidoc | 4 +- docs/changelog/91823.yaml | 5 + gradle/verification-metadata.xml | 120 ++++++++++++++++++ .../main/java/org/elasticsearch/Version.java | 6 +- 6 files changed, 133 insertions(+), 8 deletions(-) create mode 100644 docs/changelog/91823.yaml diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index e445200ff58d..f7f5b5c670cf 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.7.0 -lucene = 9.4.1 +lucene = 9.4.2 bundled_jdk_vendor = openjdk bundled_jdk = 19.0.1+10@afdd2e245b014143b62ccb916125e3ce diff --git a/build.gradle b/build.gradle index e1e11e60e110..ef3f945ac412 100644 --- a/build.gradle +++ b/build.gradle @@ -137,9 +137,9 @@ tasks.register("verifyVersions") { * after the backport of the backcompat code is complete. */ -boolean bwc_tests_enabled = true +boolean bwc_tests_enabled = false // place a PR link here when committing bwc changes: -String bwc_tests_disabled_issue = "" +String bwc_tests_disabled_issue = "https://github.com/elastic/elasticsearch/pull/91823" if (bwc_tests_enabled == false) { if (bwc_tests_disabled_issue.isEmpty()) { throw new GradleException("bwc_tests_disabled_issue must be set when bwc_tests_enabled == false") diff --git a/docs/Versions.asciidoc b/docs/Versions.asciidoc index ce5a7dce1531..b9bdf7334c07 100644 --- a/docs/Versions.asciidoc +++ b/docs/Versions.asciidoc @@ -1,8 +1,8 @@ include::{docs-root}/shared/versions/stack/{source_branch}.asciidoc[] -:lucene_version: 9.4.1 -:lucene_version_path: 9_4_1 +:lucene_version: 9.4.2 +:lucene_version_path: 9_4_2 :jdk: 11.0.2 :jdk_major: 11 :build_type: tar diff --git a/docs/changelog/91823.yaml b/docs/changelog/91823.yaml new file mode 100644 index 000000000000..918b73a0653a --- /dev/null +++ b/docs/changelog/91823.yaml @@ -0,0 +1,5 @@ +pr: 91823 +summary: Upgrade Lucene to version 9.4.2 +area: Engine +type: upgrade +issues: [] diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 8316f1a98e08..ab7c66f5678d 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2521,121 +2521,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/src/main/java/org/elasticsearch/Version.java b/server/src/main/java/org/elasticsearch/Version.java index e597516840e3..88e1af2717a1 100644 --- a/server/src/main/java/org/elasticsearch/Version.java +++ b/server/src/main/java/org/elasticsearch/Version.java @@ -126,9 +126,9 @@ public class Version implements Comparable, ToXContentFragment { public static final Version V_8_5_0 = new Version(8_05_00_99, org.apache.lucene.util.Version.LUCENE_9_4_1); public static final Version V_8_5_1 = new Version(8_05_01_99, org.apache.lucene.util.Version.LUCENE_9_4_1); public static final Version V_8_5_2 = new Version(8_05_02_99, org.apache.lucene.util.Version.LUCENE_9_4_1); - public static final Version V_8_5_3 = new Version(8_05_03_99, org.apache.lucene.util.Version.LUCENE_9_4_1); - public static final Version V_8_6_0 = new Version(8_06_00_99, org.apache.lucene.util.Version.LUCENE_9_4_1); - public static final Version V_8_7_0 = new Version(8_07_00_99, org.apache.lucene.util.Version.LUCENE_9_4_1); + public static final Version V_8_5_3 = new Version(8_05_03_99, org.apache.lucene.util.Version.LUCENE_9_4_2); + public static final Version V_8_6_0 = new Version(8_06_00_99, org.apache.lucene.util.Version.LUCENE_9_4_2); + public static final Version V_8_7_0 = new Version(8_07_00_99, org.apache.lucene.util.Version.LUCENE_9_4_2); public static final Version CURRENT = V_8_7_0; private static final Map idToVersion; From 691ec0400006c208721e8f1e42ad91c15a1cc4c8 Mon Sep 17 00:00:00 2001 From: Daniel Mitterdorfer Date: Wed, 23 Nov 2022 10:57:54 +0100 Subject: [PATCH 044/919] Mute CoordinatorTests#testCannotJoinClusterWithDifferentUUID (#91838) Relates #91837 --- .../org/elasticsearch/cluster/coordination/CoordinatorTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java index f1c5aa078785..5bf0b302991b 100644 --- a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java @@ -1635,6 +1635,7 @@ public void testClusterCannotFormWithFailingJoinValidation() { reason = "test includes assertions about JoinHelper logging", value = "org.elasticsearch.cluster.coordination.JoinHelper:INFO" ) + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/91837") public void testCannotJoinClusterWithDifferentUUID() throws IllegalAccessException { try (Cluster cluster1 = new Cluster(randomIntBetween(1, 3))) { cluster1.runRandomly(); From 01dcf97908de44f1fba98e820d753be1f7425cec Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Wed, 23 Nov 2022 11:01:54 +0100 Subject: [PATCH 045/919] Fix potential leak in RemoteRecoveryHandler (#91802) We have to make sure we release `request` in all cases that fail the `listener`. The current implementation would not release `request` on e.g. a rejection since we only do the request release for the nested listener but not for when that listener is never called because of another exception. --- docs/changelog/91802.yaml | 5 +++++ .../indices/recovery/RemoteRecoveryTargetHandler.java | 11 ++--------- 2 files changed, 7 insertions(+), 9 deletions(-) create mode 100644 docs/changelog/91802.yaml diff --git a/docs/changelog/91802.yaml b/docs/changelog/91802.yaml new file mode 100644 index 000000000000..9432ac4bab64 --- /dev/null +++ b/docs/changelog/91802.yaml @@ -0,0 +1,5 @@ +pr: 91802 +summary: Fix potential leak in `RemoteRecoveryHandler` +area: Recovery +type: bug +issues: [] diff --git a/server/src/main/java/org/elasticsearch/indices/recovery/RemoteRecoveryTargetHandler.java b/server/src/main/java/org/elasticsearch/indices/recovery/RemoteRecoveryTargetHandler.java index 4d6cdcaba63b..1078bfc03d27 100644 --- a/server/src/main/java/org/elasticsearch/indices/recovery/RemoteRecoveryTargetHandler.java +++ b/server/src/main/java/org/elasticsearch/indices/recovery/RemoteRecoveryTargetHandler.java @@ -294,7 +294,6 @@ public void writeFileChunk( totalTranslogOps, throttleTimeInNanos ); - final Writeable.Reader reader = in -> TransportResponse.Empty.INSTANCE; // Fork the actual sending onto a separate thread so we can send them concurrently even if CPU-bound (e.g. using compression). // The AsyncIOProcessor and MultiFileWriter both concentrate their work onto fewer threads if possible, but once we have @@ -302,14 +301,8 @@ public void writeFileChunk( threadPool.generic() .execute( ActionRunnable.wrap( - listener, - l -> executeRetryableAction( - action, - request, - fileChunkRequestOptions, - ActionListener.runBefore(l.map(r -> null), request::decRef), - reader - ) + ActionListener.runBefore(listener.map(r -> null), request::decRef), + l -> executeRetryableAction(action, request, fileChunkRequestOptions, l, in -> TransportResponse.Empty.INSTANCE) ) ); } From 0ff6e18700ca369a477f4f98fe96513f6648e4b4 Mon Sep 17 00:00:00 2001 From: Gerard Soldevila Date: Wed, 23 Nov 2022 12:03:25 +0100 Subject: [PATCH 046/919] Add elasticsearch_client stats to the monitoring index templates. (#91508) * Add elasticsearch_client stats to monitoring-kibana MB index template * Add PR to changelog * Store only most interesting stats * Update x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/monitoring/exporter/MonitoringTemplateUtils.java Co-authored-by: Carlos Crespo * Update x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/MonitoringTemplateRegistry.java Co-authored-by: Carlos Crespo Co-authored-by: Carlos Crespo --- docs/changelog/91508.yaml | 5 +++++ .../exporter/MonitoringTemplateUtils.java | 2 +- .../src/main/resources/monitoring-kibana-mb.json | 13 +++++++++++++ .../monitoring/MonitoringTemplateRegistry.java | 2 +- 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 docs/changelog/91508.yaml diff --git a/docs/changelog/91508.yaml b/docs/changelog/91508.yaml new file mode 100644 index 000000000000..33356fa3da11 --- /dev/null +++ b/docs/changelog/91508.yaml @@ -0,0 +1,5 @@ +pr: 91508 +summary: "Add kibana.stats.elasticsearch_client stats to the monitoring index templates." +area: Monitoring +type: enhancement +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/monitoring/exporter/MonitoringTemplateUtils.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/monitoring/exporter/MonitoringTemplateUtils.java index 8dca92e9746b..8d43288b8303 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/monitoring/exporter/MonitoringTemplateUtils.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/monitoring/exporter/MonitoringTemplateUtils.java @@ -19,7 +19,7 @@ public final class MonitoringTemplateUtils { *

* It may be possible for this to diverge between templates and pipelines, but for now they're the same. */ - public static final int LAST_UPDATED_VERSION = Version.V_8_1_0.id; + public static final int LAST_UPDATED_VERSION = Version.V_8_7_0.id; /** * Current version of templates used in their name to differentiate from breaking changes (separate from product version). diff --git a/x-pack/plugin/core/src/main/resources/monitoring-kibana-mb.json b/x-pack/plugin/core/src/main/resources/monitoring-kibana-mb.json index ff8abc5d8995..cf7dffbc761d 100644 --- a/x-pack/plugin/core/src/main/resources/monitoring-kibana-mb.json +++ b/x-pack/plugin/core/src/main/resources/monitoring-kibana-mb.json @@ -219,6 +219,19 @@ } } }, + "elasticsearch_client": { + "properties": { + "total_active_sockets": { + "type": "integer" + }, + "total_idle_sockets": { + "type": "integer" + }, + "total_queued_requests": { + "type": "integer" + } + } + }, "kibana": { "properties": { "status": { diff --git a/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/MonitoringTemplateRegistry.java b/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/MonitoringTemplateRegistry.java index b488f13cb4c5..2c94546bb61d 100644 --- a/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/MonitoringTemplateRegistry.java +++ b/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/MonitoringTemplateRegistry.java @@ -50,7 +50,7 @@ public class MonitoringTemplateRegistry extends IndexTemplateRegistry { * continue to use the release version number in this registry, even though this is not standard practice for template * registries. */ - public static final int REGISTRY_VERSION = Version.V_8_1_0.id; + public static final int REGISTRY_VERSION = Version.V_8_7_0.id; private static final String REGISTRY_VERSION_VARIABLE = "xpack.monitoring.template.release.version"; /** From 8b3438874ecd091d1ef15a7fd40f7a0ae4918dbd Mon Sep 17 00:00:00 2001 From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> Date: Wed, 23 Nov 2022 12:23:06 +0100 Subject: [PATCH 047/919] [Fleet] Added logs-elastic_agent* read privileges to kibana_system (#91701) * Added logs-elastic_agent* read privileges to kibana_system * Update docs/changelog/91701.yaml * added unit test * Fixed formatting * removed read cross cluster role --- docs/changelog/91701.yaml | 5 +++++ .../security/authz/store/ReservedRolesStore.java | 2 ++ .../authz/store/ReservedRolesStoreTests.java | 13 +++++++++++++ 3 files changed, 20 insertions(+) create mode 100644 docs/changelog/91701.yaml diff --git a/docs/changelog/91701.yaml b/docs/changelog/91701.yaml new file mode 100644 index 000000000000..d2e1d04db019 --- /dev/null +++ b/docs/changelog/91701.yaml @@ -0,0 +1,5 @@ +pr: 91701 +summary: "[Fleet] Added logs-elastic_agent* read privileges to `kibana_system`" +area: Authorization +type: enhancement +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java index 399639f70afb..e77d29e4e10b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java @@ -719,6 +719,8 @@ public static RoleDescriptor kibanaSystemRoleDescriptor(String name) { // Fleet Server indices. Kibana create this indice before Fleet Server use them. // Fleet Server indices. Kibana read and write to this indice to manage Elastic Agents RoleDescriptor.IndicesPrivileges.builder().indices(".fleet*").allowRestrictedIndices(true).privileges("all").build(), + // Fleet telemetry queries Agent Logs indices in kibana task runner + RoleDescriptor.IndicesPrivileges.builder().indices("logs-elastic_agent*").privileges("read").build(), // Legacy "Alerts as data" used in Security Solution. // Kibana user creates these indices; reads / writes to them. RoleDescriptor.IndicesPrivileges.builder().indices(ReservedRolesStore.ALERTS_LEGACY_INDEX).privileges("all").build(), diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java index 728ddbadf138..de368f5b7216 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java @@ -732,6 +732,19 @@ public void testKibanaSystemRole() { ".fleet-servers" ).forEach(index -> assertAllIndicesAccessAllowed(kibanaRole, index)); + // read-only indices for Fleet telemetry + Arrays.asList("logs-elastic_agent-default", "logs-elastic_agent.fleet_server-default").forEach((index) -> { + assertThat(kibanaRole.indices().allowedIndicesMatcher("indices:foo").test(mockIndexAbstraction(index)), is(false)); + assertThat(kibanaRole.indices().allowedIndicesMatcher("indices:bar").test(mockIndexAbstraction(index)), is(false)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(GetIndexAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(IndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(DeleteAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(SearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(MultiSearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(GetAction.NAME).test(mockIndexAbstraction(index)), is(true)); + }); + // read-only index for Endpoint and Osquery manager specific action responses Arrays.asList(".logs-endpoint.action.responses-" + randomAlphaOfLength(randomIntBetween(0, 13))).forEach((index) -> { final IndexAbstraction indexAbstraction = mockIndexAbstraction(index); From a7cc3d08d6661bbbcf3a06d73fcf10a2e3a421ec Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Wed, 23 Nov 2022 12:31:25 +0100 Subject: [PATCH 048/919] Build more compact RepositoryData when parsing from JSON (#91817) Low effort imporovement to #89952 mostly. We should be turning the index identifier lookup into an immutable map when parsing these right away. We do this conversion/copy for both the adding and removing of snapshots from the `IndexMetadataGenerations` later on anyways so this doesn't add any CPU cost overall. What it does however is save a massive amount of heap for single index snapshots (where the overhead of hash map over the immutable map is the greatest) when first parsing this structure from the repo and potentially having it duplicated on heap many times over due to #89952. --- .../elasticsearch/repositories/IndexMetaDataGenerations.java | 5 ++--- .../java/org/elasticsearch/repositories/RepositoryData.java | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/repositories/IndexMetaDataGenerations.java b/server/src/main/java/org/elasticsearch/repositories/IndexMetaDataGenerations.java index 0fd5bf4944a0..7f2a09637dbf 100644 --- a/server/src/main/java/org/elasticsearch/repositories/IndexMetaDataGenerations.java +++ b/server/src/main/java/org/elasticsearch/repositories/IndexMetaDataGenerations.java @@ -15,7 +15,6 @@ import org.elasticsearch.snapshots.SnapshotId; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -32,7 +31,7 @@ */ public final class IndexMetaDataGenerations { - public static final IndexMetaDataGenerations EMPTY = new IndexMetaDataGenerations(Collections.emptyMap(), Collections.emptyMap()); + public static final IndexMetaDataGenerations EMPTY = new IndexMetaDataGenerations(Map.of(), Map.of()); /** * Map of {@link SnapshotId} to a map of the indices in a snapshot mapping {@link IndexId} to metadata identifiers. @@ -94,7 +93,7 @@ public String indexMetaBlobId(SnapshotId snapshotId, IndexId indexId) { */ @Nullable public String snapshotIndexMetadataIdentifier(SnapshotId snapshotId, IndexId indexId) { - return lookup.getOrDefault(snapshotId, Collections.emptyMap()).get(indexId); + return lookup.getOrDefault(snapshotId, Map.of()).get(indexId); } /** diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoryData.java b/server/src/main/java/org/elasticsearch/repositories/RepositoryData.java index c412bf7260ec..089b5a6e639b 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoryData.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoryData.java @@ -875,7 +875,7 @@ private static IndexMetaDataGenerations buildIndexMetaGenerations( for (Map.Entry generationEntry : val.entrySet()) { forSnapshot.put(indexLookup.get(generationEntry.getKey()), generationEntry.getValue()); } - indexGenerations.put(snapshotIdMapEntry.getKey(), forSnapshot); + indexGenerations.put(snapshotIdMapEntry.getKey(), Map.copyOf(forSnapshot)); } return new IndexMetaDataGenerations(indexGenerations, indexMetaIdentifiers); } From b775a4dd11ea4a68f3c65f88eaee1be2115cd93a Mon Sep 17 00:00:00 2001 From: Philipp Kahr Date: Wed, 23 Nov 2022 12:41:41 +0100 Subject: [PATCH 049/919] Metricbeat monitoring collection suggest to enable local exporters (#77409) * Metricbeat monitoring collection suggest to enable local exporters Those steps are not needed. 1. We enable the local _legacy_ exporters 2. In the last step, we disable the local _legacy_ exporters again. * Also remove legacy collection step from Filebeat page Co-authored-by: Abdon Pijpelink --- .../monitoring/configuring-filebeat.asciidoc | 8 --- .../configuring-metricbeat.asciidoc | 56 ------------------- 2 files changed, 64 deletions(-) diff --git a/docs/reference/monitoring/configuring-filebeat.asciidoc b/docs/reference/monitoring/configuring-filebeat.asciidoc index 4288071c1ba4..196a9f080f2d 100644 --- a/docs/reference/monitoring/configuring-filebeat.asciidoc +++ b/docs/reference/monitoring/configuring-filebeat.asciidoc @@ -26,14 +26,6 @@ from impacting the performance of your production cluster. See -- -. Enable the collection of monitoring data on your cluster. -+ --- -include::configuring-metricbeat.asciidoc[tag=enable-collection] - -For more information, see <> and <>. --- - . Identify which logs you want to monitor. + -- diff --git a/docs/reference/monitoring/configuring-metricbeat.asciidoc b/docs/reference/monitoring/configuring-metricbeat.asciidoc index 70c77c80bb34..3da47f584ee9 100644 --- a/docs/reference/monitoring/configuring-metricbeat.asciidoc +++ b/docs/reference/monitoring/configuring-metricbeat.asciidoc @@ -13,38 +13,6 @@ as described in <>. image::monitoring/images/metricbeat.png[Example monitoring architecture] -. Enable the collection of monitoring data. -+ --- -// tag::enable-collection[] -Set `xpack.monitoring.collection.enabled` to `true` on the -production cluster. By default, it is disabled (`false`). - -You can use the following APIs to review and change this setting: - -[source,console] ----------------------------------- -GET _cluster/settings ----------------------------------- - -[source,console] ----------------------------------- -PUT _cluster/settings -{ - "persistent": { - "xpack.monitoring.collection.enabled": true - } -} ----------------------------------- -// TEST[warning:[xpack.monitoring.collection.enabled] setting was deprecated in Elasticsearch and will be removed in a future release.] - -If {es} {security-features} are enabled, you must have `monitor` cluster privileges to -view the cluster settings and `manage` cluster privileges to change them. -// end::enable-collection[] - -For more information, see <> and <>. --- - . {metricbeat-ref}/metricbeat-installation-configuration.html[Install {metricbeat}]. Ideally install a single {metricbeat} instance configured with `scope: cluster` and configure `hosts` to point to an endpoint (e.g. a @@ -187,28 +155,4 @@ For more information about these configuration options, see . {metricbeat-ref}/metricbeat-starting.html[Start {metricbeat}] on each node. -. Disable the default collection of {es} monitoring metrics. -+ --- -Set `xpack.monitoring.elasticsearch.collection.enabled` to `false` on the -production cluster. - -You can use the following API to change this setting: - -[source,console] ----------------------------------- -PUT _cluster/settings -{ - "persistent": { - "xpack.monitoring.elasticsearch.collection.enabled": false - } -} ----------------------------------- -// TEST[warning:[xpack.monitoring.elasticsearch.collection.enabled] setting was deprecated in Elasticsearch and will be removed in a future release.] - -If {es} {security-features} are enabled, you must have `monitor` cluster -privileges to view the cluster settings and `manage` cluster privileges -to change them. --- - . {kibana-ref}/monitoring-data.html[View the monitoring data in {kib}]. From 60d1e1feb5cedcc2b25b49c958fb9be28a78c42b Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Wed, 23 Nov 2022 13:57:19 +0200 Subject: [PATCH 050/919] Consolidate handling of "all" for index expression resolver (#91735) A refactoring such that callers of IndexNameExpressionResolver#resolveExpressions don't have to massage parameters when resolving _all and its ilk (empty, null, *). --- .../metadata/IndexNameExpressionResolver.java | 105 ++++++++---------- 1 file changed, 47 insertions(+), 58 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java index 440e2c2c7aad..16a5c2419d54 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java @@ -185,11 +185,7 @@ public List dataStreamNames(ClusterState state, IndicesOptions options, getSystemIndexAccessPredicate(), getNetNewSystemIndexPredicate() ); - if (indexExpressions == null || indexExpressions.length == 0) { - indexExpressions = new String[] { "*" }; - } - - final Collection expressions = resolveExpressions(Arrays.asList(indexExpressions), context); + final Collection expressions = resolveExpressions(context, indexExpressions); return expressions.stream() .map(x -> state.metadata().getIndicesLookup().get(x)) .filter(Objects::nonNull) @@ -219,7 +215,7 @@ public IndexAbstraction resolveWriteIndexAbstraction(ClusterState state, DocWrit getNetNewSystemIndexPredicate() ); - final Collection expressions = resolveExpressions(List.of(request.index()), context); + final Collection expressions = resolveExpressions(context, request.index()); if (expressions.size() == 1) { IndexAbstraction ia = state.metadata().getIndicesLookup().get(expressions.iterator().next()); @@ -247,8 +243,11 @@ public IndexAbstraction resolveWriteIndexAbstraction(ClusterState state, DocWrit } } - private static Collection resolveExpressions(List expressions, Context context) { - return WildcardExpressionResolver.resolve(context, DateMathExpressionResolver.resolve(context, expressions)); + private static Collection resolveExpressions(Context context, String... expressions) { + return WildcardExpressionResolver.resolve( + context, + DateMathExpressionResolver.resolve(context, expressions == null ? List.of() : List.of(expressions)) + ); } /** @@ -320,39 +319,16 @@ String[] concreteIndexNames(Context context, String... indexExpressions) { } Index[] concreteIndices(Context context, String... indexExpressions) { - IndicesOptions options = context.getOptions(); - if (indexExpressions == null || indexExpressions.length == 0) { - indexExpressions = new String[] { Metadata.ALL }; - } else { - if (options.ignoreUnavailable() == false) { - List crossClusterIndices = Arrays.stream(indexExpressions).filter(index -> index.contains(":")).toList(); - if (crossClusterIndices.size() > 0) { - throw new IllegalArgumentException( - "Cross-cluster calls are not supported in this context but remote indices " - + "were requested: " - + crossClusterIndices - ); - } - } - } - - final Collection expressions = resolveExpressions(Arrays.asList(indexExpressions), context); + ensureRemoteIndicesRequireIgnoreUnavailable(context.getOptions(), indexExpressions); + final Collection expressions = resolveExpressions(context, indexExpressions); - if (expressions.isEmpty() || (expressions.size() == 1 && expressions.iterator().next().equals(Metadata.ALL))) { - if (options.allowNoIndices() == false) { - throw notFoundException(indexExpressions); - } else { - return Index.EMPTY_ARRAY; - } - } - - final Set concreteIndices = Sets.newLinkedHashSetWithExpectedSize(expressions.size()); - final SortedMap indicesLookup = context.state.metadata().getIndicesLookup(); + final Set concreteIndicesResult = Sets.newLinkedHashSetWithExpectedSize(expressions.size()); + final Map indicesLookup = context.getState().metadata().getIndicesLookup(); for (String expression : expressions) { - if (options.ignoreUnavailable() == false) { + if (context.getOptions().ignoreUnavailable() == false) { ensureAliasOrIndexExists(context, expression); } - IndexAbstraction indexAbstraction = indicesLookup.get(expression); + final IndexAbstraction indexAbstraction = indicesLookup.get(expression); if (indexAbstraction == null) { continue; } else if (indexAbstraction.getType() == Type.ALIAS && context.getOptions().ignoreAliases()) { @@ -373,15 +349,15 @@ Index[] concreteIndices(Context context, String... indexExpressions) { ); } if (addIndex(writeIndex, null, context)) { - concreteIndices.add(writeIndex); + concreteIndicesResult.add(writeIndex); } } else if (indexAbstraction.getType() == Type.DATA_STREAM && context.isResolveToWriteIndex()) { Index writeIndex = indexAbstraction.getWriteIndex(); if (addIndex(writeIndex, null, context)) { - concreteIndices.add(writeIndex); + concreteIndicesResult.add(writeIndex); } } else { - if (indexAbstraction.getIndices().size() > 1 && options.allowAliasesToMultipleIndices() == false) { + if (indexAbstraction.getIndices().size() > 1 && context.getOptions().allowAliasesToMultipleIndices() == false) { String[] indexNames = new String[indexAbstraction.getIndices().size()]; int i = 0; for (Index indexName : indexAbstraction.getIndices()) { @@ -398,18 +374,18 @@ Index[] concreteIndices(Context context, String... indexExpressions) { } for (Index index : indexAbstraction.getIndices()) { - if (shouldTrackConcreteIndex(context, options, index)) { - concreteIndices.add(index); + if (shouldTrackConcreteIndex(context, context.getOptions(), index)) { + concreteIndicesResult.add(index); } } } } - if (options.allowNoIndices() == false && concreteIndices.isEmpty()) { + if (context.getOptions().allowNoIndices() == false && concreteIndicesResult.isEmpty()) { throw notFoundException(indexExpressions); } - checkSystemIndexAccess(context, concreteIndices); - return concreteIndices.toArray(Index.EMPTY_ARRAY); + checkSystemIndexAccess(context, concreteIndicesResult); + return concreteIndicesResult.toArray(Index.EMPTY_ARRAY); } private void checkSystemIndexAccess(Context context, Set concreteIndices) { @@ -458,14 +434,26 @@ private void checkSystemIndexAccess(Context context, Set concreteIndices) } } - private static IndexNotFoundException notFoundException(String... indexExpressions) { - IndexNotFoundException infe; - if (indexExpressions != null && indexExpressions.length == 1) { - if (Metadata.ALL.equals(indexExpressions[0])) { - infe = new IndexNotFoundException("no indices exist", indexExpressions[0]); - } else { - infe = new IndexNotFoundException(indexExpressions[0]); + private void ensureRemoteIndicesRequireIgnoreUnavailable(IndicesOptions options, String... indexExpressions) { + if (options.ignoreUnavailable() == false && indexExpressions != null) { + List crossClusterIndices = Arrays.stream(indexExpressions).filter(index -> index.contains(":")).toList(); + if (crossClusterIndices.size() > 0) { + throw new IllegalArgumentException( + "Cross-cluster calls are not supported in this context but remote indices were requested: " + crossClusterIndices + ); } + } + } + + private static IndexNotFoundException notFoundException(String... indexExpressions) { + final IndexNotFoundException infe; + if (indexExpressions == null + || indexExpressions.length == 0 + || (indexExpressions.length == 1 && Metadata.ALL.equals(indexExpressions[0]))) { + infe = new IndexNotFoundException("no indices exist", Metadata.ALL); + infe.setResources("index_or_alias", Metadata.ALL); + } else if (indexExpressions.length == 1) { + infe = new IndexNotFoundException(indexExpressions[0]); infe.setResources("index_or_alias", indexExpressions[0]); } else { infe = new IndexNotFoundException((String) null); @@ -662,7 +650,7 @@ public Set resolveExpressions(ClusterState state, String... expressions) getSystemIndexAccessPredicate(), getNetNewSystemIndexPredicate() ); - Collection resolved = resolveExpressions(Arrays.asList(expressions), context); + Collection resolved = resolveExpressions(context, expressions); if (resolved instanceof Set) { // unmodifiable without creating a new collection as it might contain many items return Collections.unmodifiableSet((Set) resolved); @@ -789,10 +777,7 @@ public Map> resolveSearchRouting(ClusterState state, @Nullab getSystemIndexAccessPredicate(), getNetNewSystemIndexPredicate() ); - final Collection resolvedExpressions = resolveExpressions( - expressions != null ? Arrays.asList(expressions) : Collections.emptyList(), - context - ); + final Collection resolvedExpressions = resolveExpressions(context, expressions); // TODO: it appears that this can never be true? if (isAllIndices(resolvedExpressions)) { @@ -1134,7 +1119,11 @@ private WildcardExpressionResolver() { public static Collection resolve(Context context, List expressions) { Objects.requireNonNull(expressions); if (context.getOptions().expandWildcardExpressions() == false) { - return expressions; + if (expressions.size() == 1 && expressions.get(0).equals(Metadata.ALL)) { + return List.of(); + } else { + return expressions; + } } else if (isEmptyOrTrivialWildcard(expressions)) { return innerResolveAll(context); } else { From 2a335389cfc4be0a692229dcc7033671b7db6c53 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 23 Nov 2022 13:10:58 +0100 Subject: [PATCH 051/919] Big arrays sliced from netty buffers (long) (#91641) Based on #90745 but for longs. This should allow aggregations down the road to read long values directly from netty buffer, rather than copying it from the netty buffer. Relates to #89437 --- .../common/bytes/BytesArray.java | 5 ++ .../common/bytes/CompositeBytesReference.java | 12 +++ .../elasticsearch/common/util/BigArrays.java | 7 ++ .../common/util/BigDoubleArray.java | 15 +--- .../common/util/BigIntArray.java | 18 +---- .../common/util/BigLongArray.java | 22 +++++ .../elasticsearch/common/util/LongArray.java | 11 ++- .../common/util/ReleasableLongArray.java | 80 +++++++++++++++++++ .../common/bytes/BytesArrayTests.java | 2 +- .../bytes/CompositeBytesReferenceTests.java | 26 ++++++ ...easableBytesReferenceStreamInputTests.java | 20 +++++ .../common/io/stream/AbstractStreamTests.java | 26 ++++++ .../common/util/MockBigArrays.java | 4 + 13 files changed, 217 insertions(+), 31 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/common/util/ReleasableLongArray.java diff --git a/server/src/main/java/org/elasticsearch/common/bytes/BytesArray.java b/server/src/main/java/org/elasticsearch/common/bytes/BytesArray.java index 4ae67e81ded9..8b48e871fec6 100644 --- a/server/src/main/java/org/elasticsearch/common/bytes/BytesArray.java +++ b/server/src/main/java/org/elasticsearch/common/bytes/BytesArray.java @@ -137,6 +137,11 @@ public int getIntLE(int index) { return ByteUtils.readIntLE(bytes, offset + index); } + @Override + public long getLongLE(int index) { + return ByteUtils.readLongLE(bytes, offset + index); + } + @Override public double getDoubleLE(int index) { return ByteUtils.readDoubleLE(bytes, offset + index); diff --git a/server/src/main/java/org/elasticsearch/common/bytes/CompositeBytesReference.java b/server/src/main/java/org/elasticsearch/common/bytes/CompositeBytesReference.java index aa4ead0b6b90..9a75ebd0e60e 100644 --- a/server/src/main/java/org/elasticsearch/common/bytes/CompositeBytesReference.java +++ b/server/src/main/java/org/elasticsearch/common/bytes/CompositeBytesReference.java @@ -238,6 +238,18 @@ public int getIntLE(int index) { return super.getIntLE(index); } + @Override + public long getLongLE(int index) { + int i = getOffsetIndex(index); + int idx = index - offsets[i]; + int end = idx + 8; + BytesReference wholeLongsLivesHere = references[i]; + if (end <= wholeLongsLivesHere.length()) { + return wholeLongsLivesHere.getLongLE(idx); + } + return super.getLongLE(index); + } + @Override public double getDoubleLE(int index) { int i = getOffsetIndex(index); diff --git a/server/src/main/java/org/elasticsearch/common/util/BigArrays.java b/server/src/main/java/org/elasticsearch/common/util/BigArrays.java index 7fc329148181..09f624282f0b 100644 --- a/server/src/main/java/org/elasticsearch/common/util/BigArrays.java +++ b/server/src/main/java/org/elasticsearch/common/util/BigArrays.java @@ -261,6 +261,13 @@ public void set(long index, byte[] buf, int offset, int len) { assert index >= 0 && index < size(); System.arraycopy(buf, offset << 3, array, (int) index << 3, len << 3); } + + @Override + public void writeTo(StreamOutput out) throws IOException { + int size = Math.toIntExact(size()) * Long.BYTES; + out.writeVInt(size); + out.write(array, 0, size); + } } private static class ByteArrayAsDoubleArrayWrapper extends AbstractArrayWrapper implements DoubleArray { diff --git a/server/src/main/java/org/elasticsearch/common/util/BigDoubleArray.java b/server/src/main/java/org/elasticsearch/common/util/BigDoubleArray.java index 5a411563660d..b2a55973cd44 100644 --- a/server/src/main/java/org/elasticsearch/common/util/BigDoubleArray.java +++ b/server/src/main/java/org/elasticsearch/common/util/BigDoubleArray.java @@ -18,6 +18,7 @@ import java.nio.ByteOrder; import java.util.Arrays; +import static org.elasticsearch.common.util.BigLongArray.writePages; import static org.elasticsearch.common.util.PageCacheRecycler.DOUBLE_PAGE_SIZE; /** @@ -128,18 +129,6 @@ public void set(long index, byte[] buf, int offset, int len) { @Override public void writeTo(StreamOutput out) throws IOException { - int size = (int) this.size; - out.writeVInt(size * Double.BYTES); - int lastPageEnd = size % DOUBLE_PAGE_SIZE; - if (lastPageEnd == 0) { - for (byte[] page : pages) { - out.write(page); - } - return; - } - for (int i = 0; i < pages.length - 1; i++) { - out.write(pages[i]); - } - out.write(pages[pages.length - 1], 0, lastPageEnd * Double.BYTES); + writePages(out, Math.toIntExact(size), pages, Double.BYTES, DOUBLE_PAGE_SIZE); } } diff --git a/server/src/main/java/org/elasticsearch/common/util/BigIntArray.java b/server/src/main/java/org/elasticsearch/common/util/BigIntArray.java index b659524a8829..e053baea9aa5 100644 --- a/server/src/main/java/org/elasticsearch/common/util/BigIntArray.java +++ b/server/src/main/java/org/elasticsearch/common/util/BigIntArray.java @@ -18,6 +18,7 @@ import java.nio.ByteOrder; import java.util.Arrays; +import static org.elasticsearch.common.util.BigLongArray.writePages; import static org.elasticsearch.common.util.PageCacheRecycler.INT_PAGE_SIZE; /** @@ -43,22 +44,7 @@ final class BigIntArray extends AbstractBigArray implements IntArray { @Override public void writeTo(StreamOutput out) throws IOException { - if (size > Integer.MAX_VALUE / Integer.BYTES) { - throw new IllegalArgumentException(); - } - int intSize = (int) size; - out.writeVInt(intSize * Integer.BYTES); - int lastPageEnd = intSize % INT_PAGE_SIZE; - if (lastPageEnd == 0) { - for (byte[] page : pages) { - out.write(page); - } - return; - } - for (int i = 0; i < pages.length - 1; i++) { - out.write(pages[i]); - } - out.write(pages[pages.length - 1], 0, lastPageEnd * Integer.BYTES); + writePages(out, (int) size, pages, Integer.BYTES, INT_PAGE_SIZE); } @Override diff --git a/server/src/main/java/org/elasticsearch/common/util/BigLongArray.java b/server/src/main/java/org/elasticsearch/common/util/BigLongArray.java index 8e7bb52f5a11..1044b5fb78ee 100644 --- a/server/src/main/java/org/elasticsearch/common/util/BigLongArray.java +++ b/server/src/main/java/org/elasticsearch/common/util/BigLongArray.java @@ -10,7 +10,9 @@ import org.apache.lucene.util.ArrayUtil; import org.apache.lucene.util.RamUsageEstimator; +import org.elasticsearch.common.io.stream.StreamOutput; +import java.io.IOException; import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; import java.nio.ByteOrder; @@ -126,4 +128,24 @@ public static long estimateRamBytes(final long size) { public void set(long index, byte[] buf, int offset, int len) { set(index, buf, offset, len, pages, 3); } + + @Override + public void writeTo(StreamOutput out) throws IOException { + writePages(out, Math.toIntExact(size), pages, Long.BYTES, LONG_PAGE_SIZE); + } + + static void writePages(StreamOutput out, int size, byte[][] pages, int bytesPerValue, int pageSize) throws IOException { + out.writeVInt(size * bytesPerValue); + int lastPageEnd = size % pageSize; + if (lastPageEnd == 0) { + for (byte[] page : pages) { + out.write(page); + } + return; + } + for (int i = 0; i < pages.length - 1; i++) { + out.write(pages[i]); + } + out.write(pages[pages.length - 1], 0, lastPageEnd * bytesPerValue); + } } diff --git a/server/src/main/java/org/elasticsearch/common/util/LongArray.java b/server/src/main/java/org/elasticsearch/common/util/LongArray.java index 984f1df48701..bd293a135640 100644 --- a/server/src/main/java/org/elasticsearch/common/util/LongArray.java +++ b/server/src/main/java/org/elasticsearch/common/util/LongArray.java @@ -8,10 +8,19 @@ package org.elasticsearch.common.util; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.Writeable; + +import java.io.IOException; + /** * Abstraction of an array of long values. */ -public interface LongArray extends BigArray { +public interface LongArray extends BigArray, Writeable { + + static LongArray readFrom(StreamInput in) throws IOException { + return new ReleasableLongArray(in); + } /** * Get an element given its index. diff --git a/server/src/main/java/org/elasticsearch/common/util/ReleasableLongArray.java b/server/src/main/java/org/elasticsearch/common/util/ReleasableLongArray.java new file mode 100644 index 000000000000..44764ea1e171 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/util/ReleasableLongArray.java @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.common.util; + +import org.apache.lucene.util.RamUsageEstimator; +import org.elasticsearch.common.bytes.ReleasableBytesReference; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +public class ReleasableLongArray implements LongArray { + + private static final long SHALLOW_SIZE = RamUsageEstimator.shallowSizeOfInstance(ReleasableLongArray.class); + + private final ReleasableBytesReference ref; + + ReleasableLongArray(StreamInput in) throws IOException { + this.ref = in.readReleasableBytesReference(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeBytesReference(ref); + } + + @Override + public long size() { + return ref.length() / Long.BYTES; + } + + @Override + public long get(long index) { + if (index > Integer.MAX_VALUE / Long.BYTES) { + // We can't serialize messages longer than 2gb anyway + throw new ArrayIndexOutOfBoundsException(); + } + return ref.getLongLE((int) index * Long.BYTES); + } + + @Override + public long set(long index, long value) { + throw new UnsupportedOperationException(); + } + + @Override + public long increment(long index, long inc) { + throw new UnsupportedOperationException(); + } + + @Override + public void fill(long fromIndex, long toIndex, long value) { + throw new UnsupportedOperationException(); + } + + @Override + public void set(long index, byte[] buf, int offset, int len) { + throw new UnsupportedOperationException(); + } + + @Override + public long ramBytesUsed() { + /* + * If we return the size of the buffer that we've sliced + * we're likely to double count things. + */ + return SHALLOW_SIZE; + } + + @Override + public void close() { + ref.decRef(); + } +} diff --git a/server/src/test/java/org/elasticsearch/common/bytes/BytesArrayTests.java b/server/src/test/java/org/elasticsearch/common/bytes/BytesArrayTests.java index 028d07cc5777..d75cd363925f 100644 --- a/server/src/test/java/org/elasticsearch/common/bytes/BytesArrayTests.java +++ b/server/src/test/java/org/elasticsearch/common/bytes/BytesArrayTests.java @@ -83,7 +83,7 @@ public void testGetLongLE() { assertThat(ref.getLongLE(0), equalTo(888L)); assertThat(ref.getLongLE(8), equalTo(Long.MAX_VALUE)); Exception e = expectThrows(ArrayIndexOutOfBoundsException.class, () -> ref.getLongLE(9)); - assertThat(e.getMessage(), equalTo("Index 16 out of bounds for length 16")); + assertThat(e.getMessage(), equalTo("Index 9 out of bounds for length 9")); } public void testGetDoubleLE() { diff --git a/server/src/test/java/org/elasticsearch/common/bytes/CompositeBytesReferenceTests.java b/server/src/test/java/org/elasticsearch/common/bytes/CompositeBytesReferenceTests.java index e6043837ccd7..99de6ed47114 100644 --- a/server/src/test/java/org/elasticsearch/common/bytes/CompositeBytesReferenceTests.java +++ b/server/src/test/java/org/elasticsearch/common/bytes/CompositeBytesReferenceTests.java @@ -13,6 +13,7 @@ import org.elasticsearch.common.Randomness; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.ReleasableBytesStreamOutput; +import org.elasticsearch.common.util.ByteUtils; import org.hamcrest.Matchers; import java.io.IOException; @@ -190,4 +191,29 @@ public void testGetDoubleLE() { // We can assert the exception message if -XX:-OmitStackTraceInFastThrow is set in gradle test task. expectThrows(IndexOutOfBoundsException.class, () -> comp.getDoubleLE(17)); } + + public void testGetLongLE() { + // first long = 2, second long = 44, third long = 512 + // tag::noformat + byte[] data = new byte[] { + 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x2C, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}; + // end::noformat + + byte[] d = new byte[8]; + ByteUtils.writeLongLE(2, d, 0); + + List refs = new ArrayList<>(); + int bytesPerChunk = randomFrom(4, 16); + for (int offset = 0; offset < data.length; offset += bytesPerChunk) { + int length = Math.min(bytesPerChunk, data.length - offset); + refs.add(new BytesArray(data, offset, length)); + } + BytesReference comp = CompositeBytesReference.of(refs.toArray(BytesReference[]::new)); + assertThat(comp.getLongLE(0), equalTo(2L)); + assertThat(comp.getLongLE(8), equalTo(44L)); + assertThat(comp.getLongLE(16), equalTo(512L)); + expectThrows(IndexOutOfBoundsException.class, () -> comp.getLongLE(17)); + } } diff --git a/server/src/test/java/org/elasticsearch/common/bytes/ReleasableBytesReferenceStreamInputTests.java b/server/src/test/java/org/elasticsearch/common/bytes/ReleasableBytesReferenceStreamInputTests.java index 5103a685beb5..267b5e4a0314 100644 --- a/server/src/test/java/org/elasticsearch/common/bytes/ReleasableBytesReferenceStreamInputTests.java +++ b/server/src/test/java/org/elasticsearch/common/bytes/ReleasableBytesReferenceStreamInputTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.DoubleArray; import org.elasticsearch.common.util.IntArray; +import org.elasticsearch.common.util.LongArray; import org.junit.After; import java.io.IOException; @@ -100,4 +101,23 @@ public void testBigDoubleArrayLivesAfterReleasableIsDecremented() throws IOExcep assertThat(ref.hasReferences(), equalTo(false)); } + public void testBigLongArrayLivesAfterReleasableIsDecremented() throws IOException { + LongArray testData = BigArrays.NON_RECYCLING_INSTANCE.newLongArray(1, false); + testData.set(0, 1); + + BytesStreamOutput out = new BytesStreamOutput(); + testData.writeTo(out); + + ReleasableBytesReference ref = ReleasableBytesReference.wrap(out.bytes()); + + try (LongArray in = LongArray.readFrom(ref.streamInput())) { + ref.decRef(); + assertThat(ref.hasReferences(), equalTo(true)); + + assertThat(in.size(), equalTo(testData.size())); + assertThat(in.get(0), equalTo(1L)); + } + assertThat(ref.hasReferences(), equalTo(false)); + } + } diff --git a/server/src/test/java/org/elasticsearch/common/io/stream/AbstractStreamTests.java b/server/src/test/java/org/elasticsearch/common/io/stream/AbstractStreamTests.java index 04918f5ba845..d673572d25a1 100644 --- a/server/src/test/java/org/elasticsearch/common/io/stream/AbstractStreamTests.java +++ b/server/src/test/java/org/elasticsearch/common/io/stream/AbstractStreamTests.java @@ -16,6 +16,7 @@ import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.DoubleArray; import org.elasticsearch.common.util.IntArray; +import org.elasticsearch.common.util.LongArray; import org.elasticsearch.common.util.PageCacheRecycler; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.core.CheckedConsumer; @@ -271,6 +272,31 @@ private void assertBigDoubleArray(int size) throws IOException { } } + public void testSmallBigLongArray() throws IOException { + assertBigLongArray(between(0, PageCacheRecycler.DOUBLE_PAGE_SIZE)); + } + + public void testLargeBigLongArray() throws IOException { + assertBigLongArray(between(PageCacheRecycler.DOUBLE_PAGE_SIZE, 10000)); + } + + private void assertBigLongArray(int size) throws IOException { + LongArray testData = BigArrays.NON_RECYCLING_INSTANCE.newLongArray(size, false); + for (int i = 0; i < size; i++) { + testData.set(i, randomLong()); + } + + BytesStreamOutput out = new BytesStreamOutput(); + testData.writeTo(out); + + try (LongArray in = LongArray.readFrom(getStreamInput(out.bytes()))) { + assertThat(in.size(), equalTo(testData.size())); + for (int i = 0; i < size; i++) { + assertThat(in.get(i), equalTo(testData.get(i))); + } + } + } + public void testCollection() throws IOException { class FooBar implements Writeable { diff --git a/test/framework/src/main/java/org/elasticsearch/common/util/MockBigArrays.java b/test/framework/src/main/java/org/elasticsearch/common/util/MockBigArrays.java index e1d106f6fff6..1aa956d4644c 100644 --- a/test/framework/src/main/java/org/elasticsearch/common/util/MockBigArrays.java +++ b/test/framework/src/main/java/org/elasticsearch/common/util/MockBigArrays.java @@ -513,6 +513,10 @@ public Collection getChildResources() { return Collections.singleton(Accountables.namedAccountable("delegate", in)); } + @Override + public void writeTo(StreamOutput out) throws IOException { + in.writeTo(out); + } } private class FloatArrayWrapper extends AbstractArrayWrapper implements FloatArray { From 72119f00af5eb5336b12ee9c870c015df9a28ef5 Mon Sep 17 00:00:00 2001 From: Przemyslaw Gomulka Date: Wed, 23 Nov 2022 13:45:12 +0100 Subject: [PATCH 052/919] Improve InnocuousThread permission checks handling (#91704) on shutdown, the jdk's InnocuousThread can try to change a thread name. This requires "java.lang.RuntimePermission" "modifyThread" permission. However InnocuousThread doe not inherit any Access Control Context and therefore have no permissions. This results in AccessControlException. This commit fixes this by skipping a check for modify thread permission if a thread is innocuous. relates #91658 and #91650 When previously described AccessControlException is thrown, it is not being catched anywhere in the Elasticsearch, hence it ends up being handled by ElasticsearchUncaughtExceptionHandler#onNonFatalUncaught This is being again being run by the thread [process reaper] which is an innocuous thread (jdk specific) and has no permissions. onNonFatalUncaught is trying to log a message, but this in turn requires java.lang.RuntimePermission" "getenv.*" permission. which is does not have. This again results in AccessControlException java.lang.RuntimePermission" "getenv.*" We can fix this by executing with doPrivileged in ElasticsearchUncaughtExceptionHandler#onNonFatalUncaught and this will stop the Security Manager's walk and will have ES's global grant permissions. closes #91650 --- docs/changelog/91704.yaml | 7 +++++++ .../org/elasticsearch/secure_sm/SecureSM.java | 10 +++++++--- .../packaging/test/DockerTests.java | 17 +++++++++++++++++ .../ElasticsearchUncaughtExceptionHandler.java | 11 +++++++++-- 4 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 docs/changelog/91704.yaml diff --git a/docs/changelog/91704.yaml b/docs/changelog/91704.yaml new file mode 100644 index 000000000000..66d368236d99 --- /dev/null +++ b/docs/changelog/91704.yaml @@ -0,0 +1,7 @@ +pr: 91704 +summary: '`DoPrivileged` in `ElasticsearchEncaughtExceptionHandler` and check modify + thread' +area: Infra/Core +type: bug +issues: + - 91650 diff --git a/libs/secure-sm/src/main/java/org/elasticsearch/secure_sm/SecureSM.java b/libs/secure-sm/src/main/java/org/elasticsearch/secure_sm/SecureSM.java index fa98bc1bacc6..68d2f45386c5 100644 --- a/libs/secure-sm/src/main/java/org/elasticsearch/secure_sm/SecureSM.java +++ b/libs/secure-sm/src/main/java/org/elasticsearch/secure_sm/SecureSM.java @@ -162,8 +162,12 @@ private static boolean isInnocuousThread(Thread t) { protected void checkThreadAccess(Thread t) { Objects.requireNonNull(t); - // first, check if we can modify threads at all. - checkPermission(MODIFY_THREAD_PERMISSION); + boolean targetThreadIsInnocuous = isInnocuousThread(t); + // we don't need to check if innocuous thread is modifying itself (like changes its name) + if (Thread.currentThread() != t || targetThreadIsInnocuous == false) { + // first, check if we can modify threads at all. + checkPermission(MODIFY_THREAD_PERMISSION); + } // check the threadgroup, if its our thread group or an ancestor, its fine. final ThreadGroup source = Thread.currentThread().getThreadGroup(); @@ -171,7 +175,7 @@ protected void checkThreadAccess(Thread t) { if (target == null) { return; // its a dead thread, do nothing. - } else if (source.parentOf(target) == false && isInnocuousThread(t) == false) { + } else if (source.parentOf(target) == false && targetThreadIsInnocuous == false) { checkPermission(MODIFY_ARBITRARY_THREAD_PERMISSION); } } diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java index 5703b1c5e2e6..b83348455f8a 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java @@ -1221,4 +1221,21 @@ public void test500Readiness() throws Exception { waitForElasticsearch(installation); assertTrue(readinessProbe(9399)); } + + public void test600Interrupt() { + waitForElasticsearch(installation, "elastic", PASSWORD); + final Result containerLogs = getContainerLogs(); + + assertThat("Container logs should contain starting ...", containerLogs.stdout(), containsString("starting ...")); + + final List infos = ProcessInfo.getProcessInfo(sh, "java"); + final int maxPid = infos.stream().map(i -> i.pid()).max(Integer::compareTo).get(); + + sh.run("kill -int " + maxPid); // send ctrl+c to all java processes + final Result containerLogsAfter = getContainerLogs(); + + assertThat("Container logs should contain stopping ...", containerLogsAfter.stdout(), containsString("stopping ...")); + assertThat("No errors stdout", containerLogsAfter.stdout(), not(containsString("java.security.AccessControlException:"))); + assertThat("No errors stderr", containerLogsAfter.stderr(), not(containsString("java.security.AccessControlException:"))); + } } diff --git a/server/src/main/java/org/elasticsearch/bootstrap/ElasticsearchUncaughtExceptionHandler.java b/server/src/main/java/org/elasticsearch/bootstrap/ElasticsearchUncaughtExceptionHandler.java index a8c732b6fa02..cefcd4bfb511 100644 --- a/server/src/main/java/org/elasticsearch/bootstrap/ElasticsearchUncaughtExceptionHandler.java +++ b/server/src/main/java/org/elasticsearch/bootstrap/ElasticsearchUncaughtExceptionHandler.java @@ -52,12 +52,19 @@ static boolean isFatalUncaught(Throwable e) { void onFatalUncaught(final String threadName, final Throwable t) { final String message = "fatal error in thread [" + threadName + "], exiting"; - logger.error(message, t); + logErrorMessage(t, message); } void onNonFatalUncaught(final String threadName, final Throwable t) { final String message = "uncaught exception in thread [" + threadName + "]"; - logger.error(message, t); + logErrorMessage(t, message); + } + + private void logErrorMessage(Throwable t, String message) { + AccessController.doPrivileged((PrivilegedAction) () -> { + logger.error(message, t); + return null; + }); } void halt(int status) { From 751dc244f19c88074b01572b19f73a5a2dffca66 Mon Sep 17 00:00:00 2001 From: David Kyle Date: Wed, 23 Nov 2022 13:03:43 +0000 Subject: [PATCH 053/919] [ML] Rename semantic search input parameter (#91787) Formerly `query_string` now `model_text` --- .../reference/search/semantic-search.asciidoc | 14 ++++---- .../core/ml/action/SemanticSearchAction.java | 34 +++++++++---------- .../SemanticSearchActionRequestTests.java | 2 +- .../integration/PyTorchModelRestTestCase.java | 10 +++--- .../ml/integration/SemanticSearchIT.java | 4 +-- .../test/semantic_search/10_basic.yml | 2 +- .../action/TransportSemanticSearchAction.java | 2 +- .../rest-api-spec/test/ml/semantic_search.yml | 14 ++++---- 8 files changed, 41 insertions(+), 41 deletions(-) diff --git a/docs/reference/search/semantic-search.asciidoc b/docs/reference/search/semantic-search.asciidoc index 95a142d6d2a6..b066da3065d3 100644 --- a/docs/reference/search/semantic-search.asciidoc +++ b/docs/reference/search/semantic-search.asciidoc @@ -14,7 +14,7 @@ by the model. ---- GET my-index/_semantic_search { - "query_string": "A picture of a snow capped mountain", + "model_text": "A picture of a snow capped mountain", "model_id": "my-text-embedding-model", "knn": { "field": "text_embedding", @@ -69,8 +69,8 @@ include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=routing] (Required, string) include::{es-repo-dir}/ml/ml-shared.asciidoc[tag=model-id] -`query_string`:: -(Required, string) The input text to embed. +`model_text`:: +(Required, string) The input to the text embedding model. `knn`:: (Required, object) @@ -80,7 +80,7 @@ include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=knn] [%collapsible%open] ==== `field`:: -(Required, string) +(Required, string) include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=knn-field] `filter`:: @@ -88,11 +88,11 @@ include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=knn-field] include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=knn-filter] `k`:: -(Required, integer) +(Required, integer) include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=knn-k] `num_candidates`:: -(Required, integer) +(Required, integer) include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=knn-num-candidates] ==== @@ -101,7 +101,7 @@ include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=knn-num-candidates] <>. `text_embedding_config`:: -(Object, optional) Override certain setting of the text embedding model's +(Object, optional) Override certain setting of the text embedding model's configuration. + .Properties of text_embedding inference diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/SemanticSearchAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/SemanticSearchAction.java index a2fb072199c2..e57e94db692e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/SemanticSearchAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/SemanticSearchAction.java @@ -56,14 +56,14 @@ private SemanticSearchAction(String name) { public static class Request extends ActionRequest implements IndicesRequest.Replaceable { - public static final ParseField QUERY_STRING = new ParseField("query_string"); // TODO a better name and update docs when changed + public static final ParseField MODEL_TEXT = new ParseField("model_text"); // TODO a better name and update docs when changed public static final ParseField TEXT_EMBEDDING_CONFIG = new ParseField("text_embedding_config"); static final ObjectParser PARSER = new ObjectParser<>(NAME); static { PARSER.declareString(Request.Builder::setModelId, InferModelAction.Request.MODEL_ID); - PARSER.declareString(Request.Builder::setQueryString, QUERY_STRING); + PARSER.declareString(Request.Builder::setModelText, MODEL_TEXT); PARSER.declareString(Request.Builder::setTimeout, SearchSourceBuilder.TIMEOUT_FIELD); PARSER.declareObject( Request.Builder::setUpdate, @@ -119,7 +119,7 @@ public static Request parseRestRequest(RestRequest restRequest) throws IOExcepti private String[] indices; private final String routing; - private final String queryString; + private final String modelText; private final String modelId; private final TimeValue inferenceTimeout; private final QueryBuilder query; @@ -135,7 +135,7 @@ public Request(StreamInput in) throws IOException { super(in); indices = in.readStringArray(); routing = in.readOptionalString(); - queryString = in.readString(); + modelText = in.readString(); modelId = in.readString(); inferenceTimeout = in.readOptionalTimeValue(); query = in.readOptionalNamedWriteable(QueryBuilder.class); @@ -151,7 +151,7 @@ public Request(StreamInput in) throws IOException { Request( String[] indices, String routing, - String queryString, + String modelText, String modelId, QueryBuilder query, KnnQueryOptions knnQueryOptions, @@ -165,7 +165,7 @@ public Request(StreamInput in) throws IOException { ) { this.indices = Objects.requireNonNull(indices, "[indices] must not be null"); this.routing = routing; - this.queryString = queryString; + this.modelText = modelText; this.modelId = modelId; this.query = query; this.knnQueryOptions = knnQueryOptions; @@ -183,7 +183,7 @@ public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); out.writeStringArray(indices); out.writeOptionalString(routing); - out.writeString(queryString); + out.writeString(modelText); out.writeString(modelId); out.writeOptionalTimeValue(inferenceTimeout); out.writeOptionalNamedWriteable(query); @@ -220,8 +220,8 @@ public String getRouting() { return routing; } - public String getQueryString() { - return queryString; + public String getModelText() { + return modelText; } public String getModelId() { @@ -271,7 +271,7 @@ public boolean equals(Object o) { Request request = (Request) o; return Arrays.equals(indices, request.indices) && Objects.equals(routing, request.routing) - && Objects.equals(queryString, request.queryString) + && Objects.equals(modelText, request.modelText) && Objects.equals(modelId, request.modelId) && Objects.equals(inferenceTimeout, request.inferenceTimeout) && Objects.equals(query, request.query) @@ -288,7 +288,7 @@ public boolean equals(Object o) { public int hashCode() { int result = Objects.hash( routing, - queryString, + modelText, modelId, inferenceTimeout, query, @@ -307,8 +307,8 @@ public int hashCode() { @Override public ActionRequestValidationException validate() { ActionRequestValidationException error = new ActionRequestValidationException(); - if (queryString == null) { - error.addValidationError("query_string cannot be null"); + if (modelText == null) { + error.addValidationError("model_text cannot be null"); } if (modelId == null) { error.addValidationError("model_id cannot be null"); @@ -325,7 +325,7 @@ public static class Builder { private final String[] indices; private String routing; private String modelId; - private String queryString; + private String modelText; private TimeValue timeout; private TextEmbeddingConfigUpdate update; private QueryBuilder queryBuilder; @@ -348,8 +348,8 @@ void setModelId(String modelId) { this.modelId = modelId; } - void setQueryString(String queryString) { - this.queryString = queryString; + void setModelText(String modelText) { + this.modelText = modelText; } void setTimeout(TimeValue timeout) { @@ -396,7 +396,7 @@ Request build() { return new Request( indices, routing, - queryString, + modelText, modelId, queryBuilder, knnSearchBuilder, diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/SemanticSearchActionRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/SemanticSearchActionRequestTests.java index c74ac85f550e..5ab09988e229 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/SemanticSearchActionRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/SemanticSearchActionRequestTests.java @@ -101,7 +101,7 @@ public void testValidate() { var validation = action.validate(); assertNotNull(validation); assertThat(validation.validationErrors(), hasSize(3)); - assertThat(validation.validationErrors().get(0), containsString("query_string cannot be null")); + assertThat(validation.validationErrors().get(0), containsString("model_text cannot be null")); assertThat(validation.validationErrors().get(1), containsString("model_id cannot be null")); assertThat(validation.validationErrors().get(2), containsString("knn cannot be null")); } diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/PyTorchModelRestTestCase.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/PyTorchModelRestTestCase.java index 53bdff094e5a..fcc1143365e4 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/PyTorchModelRestTestCase.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/PyTorchModelRestTestCase.java @@ -272,19 +272,19 @@ protected Response infer(String input, String modelId, String resultsField) thro return client().performRequest(request); } - protected Response semanticSearch(String index, String queryText, String modelId, String denseVectorFieldName) throws IOException { + protected Response semanticSearch(String index, String modelText, String modelId, String denseVectorFieldName) throws IOException { Request request = new Request("GET", index + "/_semantic_search?error_trace=true"); request.setJsonEntity(String.format(Locale.ROOT, """ { "model_id": "%s", - "query_string": "%s", + "model_text": "%s", "knn": { "field": "%s", "k": 5, "num_candidates": 10 } - }""", modelId, queryText, denseVectorFieldName)); + }""", modelId, modelText, denseVectorFieldName)); return client().performRequest(request); } @@ -304,7 +304,7 @@ protected Response semanticSearchWithTermsFilter( request.setJsonEntity(String.format(Locale.ROOT, """ { "model_id": "%s", - "query_string": "%s", + "model_text": "%s", "knn": { "field": "%s", "k": 5, @@ -322,7 +322,7 @@ protected Response semanticSearchWithQuery(String index, String queryText, Strin request.setJsonEntity(String.format(Locale.ROOT, """ { "model_id": "%s", - "query_string": "%s", + "model_text": "%s", "knn": { "field": "%s", "k": 5, diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/SemanticSearchIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/SemanticSearchIT.java index 57ebbcb8daf6..fab5f45bdc68 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/SemanticSearchIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/SemanticSearchIT.java @@ -226,7 +226,7 @@ public void testHybridSearch() throws IOException { request.setJsonEntity(String.format(Locale.ROOT, """ { "model_id": "%s", - "query_string": "my words", + "model_text": "my words", "knn": { "field": "embedding", "k": 3, @@ -251,7 +251,7 @@ public void testHybridSearch() throws IOException { request.setJsonEntity(String.format(Locale.ROOT, """ { "model_id": "%s", - "query_string": "my words", + "model_text": "my words", "knn": { "field": "embedding", "k": 3, diff --git a/x-pack/plugin/ml/qa/semantic-search-tests/src/yamlRestTest/resources/rest-api-spec/test/semantic_search/10_basic.yml b/x-pack/plugin/ml/qa/semantic-search-tests/src/yamlRestTest/resources/rest-api-spec/test/semantic_search/10_basic.yml index 4affee774c14..7ad05a55a63e 100644 --- a/x-pack/plugin/ml/qa/semantic-search-tests/src/yamlRestTest/resources/rest-api-spec/test/semantic_search/10_basic.yml +++ b/x-pack/plugin/ml/qa/semantic-search-tests/src/yamlRestTest/resources/rest-api-spec/test/semantic_search/10_basic.yml @@ -117,7 +117,7 @@ setup: index: embedded_text body: model_id: text_embedding_model - query_string: "the octopus comforter smells" + model_text: "the octopus comforter smells" knn: field: embedding k: 3 diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportSemanticSearchAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportSemanticSearchAction.java index 8525d8275797..48f34ffc9606 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportSemanticSearchAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportSemanticSearchAction.java @@ -130,7 +130,7 @@ private InferTrainedModelDeploymentAction.Request toInferenceRequest(SemanticSea var inferenceRequest = new InferTrainedModelDeploymentAction.Request( request.getModelId(), request.getEmbeddingConfig(), - request.getQueryString(), + request.getModelText(), request.getInferenceTimeout() ); inferenceRequest.setParentTask(parentTask); diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/semantic_search.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/semantic_search.yml index f4f9cf18f3d7..863722c30b8f 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/semantic_search.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/semantic_search.yml @@ -135,7 +135,7 @@ setup: index: embedded_text body: model_id: text_embedding_model - query_string: "the octopus comforter smells" + model_text: "the octopus comforter smells" knn: field: embedding k: 3 @@ -150,7 +150,7 @@ setup: index: embedded_text body: model_id: text_embedding_model - query_string: "the octopus comforter smells" + model_text: "the octopus comforter smells" text_embedding_config: tokenization: bert: @@ -178,7 +178,7 @@ setup: index: embedded_text body: model_id: text_embedding_model - query_string: "kids pyjamas with picture of a tractor" + model_text: "kids pyjamas with picture of a tractor" knn: field: source_text k: 2 @@ -198,7 +198,7 @@ setup: index: embedded_text_10_dims body: model_id: text_embedding_model - query_string: "kids pyjamas with picture of a tractor" + model_text: "kids pyjamas with picture of a tractor" knn: field: embedding k: 2 @@ -212,7 +212,7 @@ setup: index: embedded_text body: model_id: text_embedding_model - query_string: "kids pyjamas with picture of a tractor" + model_text: "kids pyjamas with picture of a tractor" knn: field: embedding k: 2 @@ -226,7 +226,7 @@ setup: index: embedded_text body: model_id: missing_model - query_string: "kids pyjamas with picture of a tractor" + model_text: "kids pyjamas with picture of a tractor" knn: field: embedding k: 2 @@ -253,4 +253,4 @@ setup: index: embedded_text body: model_id: text_embedding_model - query_string: "kids pyjamas with picture of a tractor" + model_text: "kids pyjamas with picture of a tractor" From 05d2bc29de31459ce990496b013e9ce2cd5675f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Fern=C3=A1ndez=20Casta=C3=B1o?= Date: Wed, 23 Nov 2022 14:29:32 +0100 Subject: [PATCH 054/919] Fix DataStreamIT#testWriteLoadAndAvgShardSizeIsStoredInABestEffort (#91655) Closes #91384 --- .../datastreams/DataStreamIT.java | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/DataStreamIT.java b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/DataStreamIT.java index de30aeaa641e..62d96e680621 100644 --- a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/DataStreamIT.java +++ b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/DataStreamIT.java @@ -100,6 +100,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -2024,24 +2025,7 @@ public void testWriteIndexWriteLoadAndAvgShardSizeIsStoredAfterRollover() throws final var request = new CreateDataStreamAction.Request(dataStreamName); assertAcked(client().execute(CreateDataStreamAction.INSTANCE, request).actionGet()); - assertBusy(() -> { - for (int i = 0; i < 10; i++) { - indexDocs(dataStreamName, randomIntBetween(100, 200)); - } - - final ClusterState clusterState = internalCluster().getCurrentMasterNodeInstance(ClusterService.class).state(); - final DataStream dataStream = clusterState.getMetadata().dataStreams().get(dataStreamName); - final String writeIndex = dataStream.getWriteIndex().getName(); - final IndicesStatsResponse indicesStatsResponse = client().admin().indices().prepareStats(writeIndex).get(); - for (IndexShardStats indexShardStats : indicesStatsResponse.getIndex(writeIndex).getIndexShards().values()) { - for (ShardStats shard : indexShardStats.getShards()) { - final IndexingStats.Stats shardIndexingStats = shard.getStats().getIndexing().getTotal(); - // Ensure that we have enough clock granularity before rolling over to ensure that we capture _some_ write load - assertThat(shardIndexingStats.getTotalActiveTimeInMillis(), is(greaterThan(0L))); - assertThat(shardIndexingStats.getWriteLoad(), is(greaterThan(0.0))); - } - } - }); + indexDocsAndEnsureThereIsCapturedWriteLoad(dataStreamName); assertAcked(client().admin().indices().rolloverIndex(new RolloverRequest(dataStreamName, null)).actionGet()); final ClusterState clusterState = internalCluster().getCurrentMasterNodeInstance(ClusterService.class).state(); @@ -2078,6 +2062,7 @@ public void testWriteLoadAndAvgShardSizeIsStoredInABestEffort() throws Exception // - We want to simulate two possible cases here: // - All the assigned nodes for shard 0 will fail to respond to the IndicesStatsRequest // - Only the shard 1 replica will respond successfully to the IndicesStatsRequest ensuring that we fall back in that case + // (only if it's not co-located with some other shard copies) final List dataOnlyNodes = internalCluster().startDataOnlyNodes(4); final String dataStreamName = "logs-es"; @@ -2091,20 +2076,20 @@ public void testWriteLoadAndAvgShardSizeIsStoredInABestEffort() throws Exception final var createDataStreamRequest = new CreateDataStreamAction.Request(dataStreamName); assertAcked(client().execute(CreateDataStreamAction.INSTANCE, createDataStreamRequest).actionGet()); - for (int i = 0; i < 10; i++) { - indexDocs(dataStreamName, randomIntBetween(100, 200)); - } + indexDocsAndEnsureThereIsCapturedWriteLoad(dataStreamName); final ClusterState clusterStateBeforeRollover = internalCluster().getCurrentMasterNodeInstance(ClusterService.class).state(); final DataStream dataStreamBeforeRollover = clusterStateBeforeRollover.getMetadata().dataStreams().get(dataStreamName); final IndexRoutingTable currentDataStreamWriteIndexRoutingTable = clusterStateBeforeRollover.routingTable() .index(dataStreamBeforeRollover.getWriteIndex()); - final List failingIndicesStatsNodeIds = new ArrayList<>(); + final Set failingIndicesStatsNodeIds = new HashSet<>(); for (ShardRouting shardRouting : currentDataStreamWriteIndexRoutingTable.shard(0).assignedShards()) { failingIndicesStatsNodeIds.add(shardRouting.currentNodeId()); } failingIndicesStatsNodeIds.add(currentDataStreamWriteIndexRoutingTable.shard(1).primaryShard().currentNodeId()); + final String shard1ReplicaNodeId = currentDataStreamWriteIndexRoutingTable.shard(1).replicaShards().get(0).currentNodeId(); + final boolean shard1ReplicaIsAllocatedInAReachableNode = failingIndicesStatsNodeIds.contains(shard1ReplicaNodeId) == false; for (String nodeId : failingIndicesStatsNodeIds) { String nodeName = clusterStateBeforeRollover.nodes().resolveNode(nodeId).getName(); @@ -2114,7 +2099,6 @@ public void testWriteLoadAndAvgShardSizeIsStoredInABestEffort() throws Exception (handler, request, channel, task) -> channel.sendResponse(new RuntimeException("Unable to get stats")) ); } - assertThat(failingIndicesStatsNodeIds.size(), is(equalTo(3))); assertAcked(client().admin().indices().rolloverIndex(new RolloverRequest(dataStreamName, null)).actionGet()); final ClusterState clusterState = internalCluster().getCurrentMasterNodeInstance(ClusterService.class).state(); @@ -2124,7 +2108,8 @@ public void testWriteLoadAndAvgShardSizeIsStoredInABestEffort() throws Exception final IndexMetadata indexMetadata = clusterState.metadata().index(index); final IndexMetadataStats metadataStats = indexMetadata.getStats(); - if (index.equals(dataStream.getWriteIndex()) == false) { + // If all the shards are co-located within the failing nodes, no stats will be stored during rollover + if (index.equals(dataStream.getWriteIndex()) == false && shard1ReplicaIsAllocatedInAReachableNode) { assertThat(metadataStats, is(notNullValue())); final IndexWriteLoad indexWriteLoad = metadataStats.writeLoad(); @@ -2247,6 +2232,27 @@ public void testShardSizeIsForecastedDuringRollover() throws Exception { assertThat(forecastedShardSizeInBytes.getAsLong(), is(equalTo(expectedTotalSizeInBytes / shardCount))); } + private void indexDocsAndEnsureThereIsCapturedWriteLoad(String dataStreamName) throws Exception { + assertBusy(() -> { + for (int i = 0; i < 10; i++) { + indexDocs(dataStreamName, randomIntBetween(100, 200)); + } + + final ClusterState clusterState = internalCluster().getCurrentMasterNodeInstance(ClusterService.class).state(); + final DataStream dataStream = clusterState.getMetadata().dataStreams().get(dataStreamName); + final String writeIndex = dataStream.getWriteIndex().getName(); + final IndicesStatsResponse indicesStatsResponse = client().admin().indices().prepareStats(writeIndex).get(); + for (IndexShardStats indexShardStats : indicesStatsResponse.getIndex(writeIndex).getIndexShards().values()) { + for (ShardStats shard : indexShardStats.getShards()) { + final IndexingStats.Stats shardIndexingStats = shard.getStats().getIndexing().getTotal(); + // Ensure that we have enough clock granularity before rolling over to ensure that we capture _some_ write load + assertThat(shardIndexingStats.getTotalActiveTimeInMillis(), is(greaterThan(0L))); + assertThat(shardIndexingStats.getWriteLoad(), is(greaterThan(0.0))); + } + } + }); + } + static void putComposableIndexTemplate( String id, @Nullable String mappings, From 454b3e610a13c809add90712a6a1f60a6e2b5377 Mon Sep 17 00:00:00 2001 From: Pooya Salehi Date: Wed, 23 Nov 2022 14:37:02 +0100 Subject: [PATCH 055/919] Wait for task on master in testGetMappingsCancellation (#91709) --- .../RestClusterInfoActionCancellationIT.java | 19 ++++++++++++++----- .../service/ClusterApplierService.java | 5 +++++ .../elasticsearch/test/TaskAssertions.java | 13 +++++++++++-- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/RestClusterInfoActionCancellationIT.java b/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/RestClusterInfoActionCancellationIT.java index 0594191ed3c8..43d7630199bb 100644 --- a/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/RestClusterInfoActionCancellationIT.java +++ b/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/RestClusterInfoActionCancellationIT.java @@ -26,7 +26,7 @@ import org.elasticsearch.core.TimeValue; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.test.ESIntegTestCase; -import org.elasticsearch.test.junit.annotations.TestLogging; +import org.hamcrest.Matchers; import java.util.EnumSet; import java.util.concurrent.CancellationException; @@ -35,11 +35,10 @@ import static org.elasticsearch.action.support.ActionTestUtils.wrapAsRestResponseListener; import static org.elasticsearch.test.TaskAssertions.assertAllCancellableTasksAreCancelled; import static org.elasticsearch.test.TaskAssertions.assertAllTasksHaveFinished; -import static org.elasticsearch.test.TaskAssertions.awaitTaskWithPrefix; +import static org.elasticsearch.test.TaskAssertions.awaitTaskWithPrefixOnMaster; import static org.hamcrest.core.IsEqual.equalTo; @ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 0, numClientNodes = 0) -@TestLogging(value = "org.elasticsearch.tasks.TaskManager:TRACE,org.elasticsearch.test.TaskAssertions:TRACE", reason = "debugging") public class RestClusterInfoActionCancellationIT extends HttpSmokeTestCase { public void testGetMappingsCancellation() throws Exception { @@ -77,8 +76,18 @@ private void runTest(String actionName, String endpoint) throws Exception { final Cancellable cancellable = getRestClient().performRequestAsync(request, wrapAsRestResponseListener(future)); assertThat(future.isDone(), equalTo(false)); - awaitTaskWithPrefix(actionName); - + awaitTaskWithPrefixOnMaster(actionName); + // To ensure that the task is executing on master, we wait until the first blocked execution of the task registers its cluster state + // observer for further retries. This ensures that a task is not cancelled before we have started its execution, which could result + // in the task being unregistered and the test not being able to find any cancelled tasks. + assertBusy( + () -> assertThat( + internalCluster().getCurrentMasterNodeInstance(ClusterService.class) + .getClusterApplierService() + .getTimeoutClusterStateListenersSize(), + Matchers.greaterThan(0) + ) + ); cancellable.cancel(); assertAllCancellableTasksAreCancelled(actionName); diff --git a/server/src/main/java/org/elasticsearch/cluster/service/ClusterApplierService.java b/server/src/main/java/org/elasticsearch/cluster/service/ClusterApplierService.java index 49935668ed47..dedb7e2ee452 100644 --- a/server/src/main/java/org/elasticsearch/cluster/service/ClusterApplierService.java +++ b/server/src/main/java/org/elasticsearch/cluster/service/ClusterApplierService.java @@ -654,4 +654,9 @@ protected boolean applicationMayFail() { public ClusterApplierRecordingService.Stats getStats() { return recordingService.getStats(); } + + // Exposed only for testing + public int getTimeoutClusterStateListenersSize() { + return timeoutClusterStateListeners.size(); + } } diff --git a/test/framework/src/main/java/org/elasticsearch/test/TaskAssertions.java b/test/framework/src/main/java/org/elasticsearch/test/TaskAssertions.java index b811b33cff53..b4ecc36fc5b9 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/TaskAssertions.java +++ b/test/framework/src/main/java/org/elasticsearch/test/TaskAssertions.java @@ -32,10 +32,18 @@ public class TaskAssertions { private TaskAssertions() {} public static void awaitTaskWithPrefix(String actionPrefix) throws Exception { + awaitTaskWithPrefix(actionPrefix, internalCluster().getInstances(TransportService.class)); + } + + public static void awaitTaskWithPrefixOnMaster(String actionPrefix) throws Exception { + awaitTaskWithPrefix(actionPrefix, List.of(internalCluster().getCurrentMasterNodeInstance(TransportService.class))); + } + + private static void awaitTaskWithPrefix(String actionPrefix, Iterable transportServiceInstances) throws Exception { logger.info("--> waiting for task with prefix [{}] to start", actionPrefix); assertBusy(() -> { - for (TransportService transportService : internalCluster().getInstances(TransportService.class)) { + for (TransportService transportService : transportServiceInstances) { List matchingTasks = transportService.getTaskManager() .getTasks() .values() @@ -61,12 +69,13 @@ public static void assertAllCancellableTasksAreCancelled(String actionPrefix) th assertTrue(taskManager.assertCancellableTaskConsistency()); for (CancellableTask cancellableTask : taskManager.getCancellableTasks().values()) { if (cancellableTask.getAction().startsWith(actionPrefix)) { - logger.trace("--> found task with prefix [{}] marked as cancelled: [{}]", actionPrefix, cancellableTask); + logger.trace("--> found task with prefix [{}]: [{}]", actionPrefix, cancellableTask); foundTask = true; assertTrue( "task " + cancellableTask.getId() + "/" + cancellableTask.getAction() + " not cancelled", cancellableTask.isCancelled() ); + logger.trace("--> Task with prefix [{}] is marked as cancelled: [{}]", actionPrefix, cancellableTask); } } } From 7607526cb77cc15a47455ee1819ec4b0c6ed437b Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Wed, 23 Nov 2022 16:26:25 +0200 Subject: [PATCH 056/919] [ML] Increase timeout for _infer in PyTorchModelIT (#91853) Inference requests running on slow workers in CI may exceed the default 10s timeout. This commit increases the timeout so those tests become more robust. --- .../xpack/ml/integration/PyTorchModelRestTestCase.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/PyTorchModelRestTestCase.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/PyTorchModelRestTestCase.java index fcc1143365e4..bd64a888ab9c 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/PyTorchModelRestTestCase.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/PyTorchModelRestTestCase.java @@ -251,7 +251,7 @@ protected Response infer(String input, String modelId, TimeValue timeout) throws } protected Response infer(String input, String modelId) throws IOException { - Request request = new Request("POST", "/_ml/trained_models/" + modelId + "/_infer"); + Request request = new Request("POST", "/_ml/trained_models/" + modelId + "/_infer?timeout=30s"); request.setJsonEntity(String.format(Locale.ROOT, """ { "docs": [{"input":"%s"}] } """, input)); @@ -259,7 +259,7 @@ protected Response infer(String input, String modelId) throws IOException { } protected Response infer(String input, String modelId, String resultsField) throws IOException { - Request request = new Request("POST", "/_ml/trained_models/" + modelId + "/_infer"); + Request request = new Request("POST", "/_ml/trained_models/" + modelId + "/_infer?timeout=30s"); request.setJsonEntity(String.format(Locale.ROOT, """ { "docs": [ { "input": "%s" } ], From 3ad3ad4a73011bf3be50d8b637528f31d3d8dadd Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Wed, 23 Nov 2022 15:45:44 +0100 Subject: [PATCH 057/919] Make wiping all snapshots in REST tests more efficient (#91828) The logic here was a left-over from a time when we couldn't bulk delete snapshots. We can simply run a wildcard delete per fs-repo now and it will transparently take care of in-progress snapshots etc. No need for any of the logic retrying on existing in-progress snapshots and such. --- .../test/rest/ESRestTestCase.java | 55 ++----------------- .../actions/SearchableSnapshotActionIT.java | 5 -- .../xpack/slm/SnapshotLifecycleRestIT.java | 9 +-- 3 files changed, 5 insertions(+), 64 deletions(-) diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java index 297ea9beafff..8ef88d81f4e3 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java @@ -20,7 +20,6 @@ import org.apache.http.ssl.SSLContextBuilder; import org.apache.http.ssl.SSLContexts; import org.apache.http.util.EntityUtils; -import org.apache.lucene.util.SetOnce; import org.elasticsearch.Version; import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksAction; import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryRequest; @@ -57,7 +56,6 @@ import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.seqno.ReplicationTracker; import org.elasticsearch.rest.RestStatus; -import org.elasticsearch.snapshots.SnapshotState; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xcontent.DeprecationHandler; import org.elasticsearch.xcontent.NamedXContentRegistry; @@ -98,7 +96,6 @@ import java.util.Set; import java.util.TreeSet; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -109,7 +106,6 @@ import static java.util.Collections.sort; import static java.util.Collections.unmodifiableList; import static org.elasticsearch.core.Strings.format; -import static org.hamcrest.Matchers.anEmptyMap; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; @@ -638,14 +634,6 @@ protected boolean preserveSearchableSnapshotsIndicesUponCompletion() { return false; } - /** - * Returns whether to wait to make absolutely certain that all snapshots - * have been deleted. - */ - protected boolean waitForAllSnapshotsWiped() { - return false; - } - private void wipeCluster() throws Exception { // Cleanup rollup before deleting indices. A rollup job might have bulks in-flight, @@ -666,24 +654,7 @@ private void wipeCluster() throws Exception { wipeSearchableSnapshotsIndices(); } - SetOnce>>> inProgressSnapshots = new SetOnce<>(); - if (waitForAllSnapshotsWiped()) { - AtomicReference>>> snapshots = new AtomicReference<>(); - try { - // Repeatedly delete the snapshots until there aren't any - assertBusy(() -> { - snapshots.set(wipeSnapshots()); - assertThat(snapshots.get(), anEmptyMap()); - }, 2, TimeUnit.MINUTES); - // At this point there should be no snaphots - inProgressSnapshots.set(snapshots.get()); - } catch (AssertionError e) { - // This will cause an error at the end of this method, but do the rest of the cleanup first - inProgressSnapshots.set(snapshots.get()); - } - } else { - inProgressSnapshots.set(wipeSnapshots()); - } + wipeSnapshots(); // wipe data streams before indices so that the backing indices for data streams are handled properly if (preserveDataStreamsUponCompletion() == false) { @@ -816,8 +787,6 @@ private void wipeCluster() throws Exception { } deleteAllNodeShutdownMetadata(); - - assertThat("Found in progress snapshots [" + inProgressSnapshots.get() + "].", inProgressSnapshots.get(), anEmptyMap()); } /** @@ -1007,37 +976,21 @@ protected void wipeSearchableSnapshotsIndices() throws IOException { /** * Wipe fs snapshots we created one by one and all repositories so that the next test can create the repositories fresh and they'll - * start empty. There isn't an API to delete all snapshots. There is an API to delete all snapshot repositories but that leaves all of - * the snapshots intact in the repository. - * @return Map of repository name to list of snapshots found in unfinished state + * start empty. */ - protected Map>> wipeSnapshots() throws IOException { - final Map>> inProgressSnapshots = new HashMap<>(); + protected void wipeSnapshots() throws IOException { for (Map.Entry repo : entityAsMap(adminClient.performRequest(new Request("GET", "/_snapshot/_all"))).entrySet()) { String repoName = repo.getKey(); Map repoSpec = (Map) repo.getValue(); String repoType = (String) repoSpec.get("type"); if (false == preserveSnapshotsUponCompletion() && repoType.equals("fs")) { // All other repo types we really don't have a chance of being able to iterate properly, sadly. - Request listRequest = new Request("GET", "/_snapshot/" + repoName + "/_all"); - listRequest.addParameter("ignore_unavailable", "true"); - - List snapshots = (List) entityAsMap(adminClient.performRequest(listRequest)).get("snapshots"); - for (Object snapshot : snapshots) { - Map snapshotInfo = (Map) snapshot; - String name = (String) snapshotInfo.get("snapshot"); - if (SnapshotState.valueOf((String) snapshotInfo.get("state")).completed() == false) { - inProgressSnapshots.computeIfAbsent(repoName, key -> new ArrayList<>()).add(snapshotInfo); - } - logger.debug("wiping snapshot [{}/{}]", repoName, name); - adminClient().performRequest(new Request("DELETE", "/_snapshot/" + repoName + "/" + name)); - } + adminClient().performRequest(new Request("DELETE", "/_snapshot/" + repoName + "/*")); } if (preserveReposUponCompletion() == false) { deleteRepository(repoName); } } - return inProgressSnapshots; } protected void deleteRepository(String repoName) throws IOException { diff --git a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/SearchableSnapshotActionIT.java b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/SearchableSnapshotActionIT.java index 630157989484..cbacffef5567 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/SearchableSnapshotActionIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/SearchableSnapshotActionIT.java @@ -83,11 +83,6 @@ public void refreshIndex() { ); } - @Override - protected boolean waitForAllSnapshotsWiped() { - return true; - } - public void testSearchableSnapshotAction() throws Exception { createSnapshotRepo(client(), snapshotRepo, randomBoolean()); createNewSingletonPolicy(client(), policy, "cold", new SearchableSnapshotAction(snapshotRepo, true)); diff --git a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/slm/SnapshotLifecycleRestIT.java b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/slm/SnapshotLifecycleRestIT.java index 02bdf446e6b3..80a877c7f4b0 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/slm/SnapshotLifecycleRestIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/slm/SnapshotLifecycleRestIT.java @@ -67,11 +67,6 @@ public class SnapshotLifecycleRestIT extends ESRestTestCase { private static final String NEVER_EXECUTE_CRON_SCHEDULE = "* * * 31 FEB ? *"; - @Override - protected boolean waitForAllSnapshotsWiped() { - return true; - } - // as we are testing the SLM history entries we'll preserve the "slm-history-ilm-policy" policy as it'll be associated with the // .slm-history-* indices and we won't be able to delete it when we wipe out the cluster @Override @@ -365,9 +360,7 @@ public void testStartStopStatus() throws Exception { assertBusy(() -> { try { - Map>> snaps = wipeSnapshots(); - logger.info("--> checking for wiped snapshots: {}", snaps); - assertThat(snaps.size(), equalTo(0)); + wipeSnapshots(); } catch (ResponseException e) { logger.error("got exception wiping snapshots", e); fail("got exception: " + EntityUtils.toString(e.getResponse().getEntity())); From 1c6d9a90be5ebea22d89d847e5c927d6d87a4310 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Wed, 23 Nov 2022 16:02:19 +0100 Subject: [PATCH 058/919] Fix CacheServiceTests.testProcessShardEviction (#91855) Slightly adjust the mechanics of the map of futures so that `waitForCacheFilesEvictionIfNeeded` and a future obtained from `pendingShardsEvictions` will always be consistent since both the future and the map removal happen under the same mutex. The existing approach of using the future from the executor submit and removing it from the map during the execution of its task would lead to a race where the future is always removed from the map before it completes, so `waitForCacheFilesEvictionIfNeeded` would briefly not see the future in the map and return while the future has not yet been completed. closes #91814 --- .../cache/full/CacheService.java | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/cache/full/CacheService.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/cache/full/CacheService.java index dfbf658add23..b177e6eb165b 100644 --- a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/cache/full/CacheService.java +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/cache/full/CacheService.java @@ -9,6 +9,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.lucene.store.AlreadyClosedException; +import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.cache.Cache; @@ -140,7 +141,7 @@ public class CacheService extends AbstractLifecycleComponent { private final Cache cache; private final ByteSizeValue rangeSize; private final ByteSizeValue recoveryRangeSize; - private final Map> pendingShardsEvictions; + private final Map> pendingShardsEvictions; private final ReadWriteLock shardsEvictionsLock; private final Object shardsEvictionsMutex; @@ -345,18 +346,22 @@ public void markShardAsEvictedInCache(String snapshotUUID, String snapshotIndexN synchronized (shardsEvictionsMutex) { if (allowShardsEvictions) { final ShardEviction shardEviction = new ShardEviction(snapshotUUID, snapshotIndexName, shardId); - pendingShardsEvictions.computeIfAbsent(shardEviction, shard -> threadPool.generic().submit(new AbstractRunnable() { - @Override - protected void doRun() { - processShardEviction(shardEviction); - } + pendingShardsEvictions.computeIfAbsent(shardEviction, shard -> { + final PlainActionFuture future = PlainActionFuture.newFuture(); + threadPool.generic().execute(new AbstractRunnable() { + @Override + protected void doRun() { + processShardEviction(shardEviction); + } - @Override - public void onFailure(Exception e) { - logger.warn(() -> format("failed to evict cache files associated with shard %s", shardEviction), e); - assert false : e; - } - })); + @Override + public void onFailure(Exception e) { + logger.warn(() -> format("failed to evict cache files associated with shard %s", shardEviction), e); + assert false : e; + } + }); + return future; + }); } } } @@ -423,8 +428,8 @@ private void processShardEviction(ShardEviction shardEviction) { } } finally { synchronized (shardsEvictionsMutex) { - final Future removedFuture = pendingShardsEvictions.remove(shardEviction); - assert removedFuture != null; + final PlainActionFuture removedFuture = pendingShardsEvictions.remove(shardEviction); + removedFuture.onResponse(null); } } } finally { From f62db9f8bb1e7ae832bc6e025b19d5f3ed58e1df Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Wed, 23 Nov 2022 15:41:11 +0000 Subject: [PATCH 059/919] Add trace.id to request trace logs (#91772) This adds trace.id, extracted from traceparent header, so its present in the request trace log. It is already included in response trace logs via the threadcontext, but that is not set at request log time. --- docs/changelog/91772.yaml | 5 ++ .../org/elasticsearch/http/HttpTracer.java | 14 ++- .../elasticsearch/rest/RestController.java | 6 +- .../org/elasticsearch/rest/RestUtils.java | 15 +++- .../elasticsearch/http/HttpTracerTests.java | 86 +++++++++++++++++++ 5 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 docs/changelog/91772.yaml create mode 100644 server/src/test/java/org/elasticsearch/http/HttpTracerTests.java diff --git a/docs/changelog/91772.yaml b/docs/changelog/91772.yaml new file mode 100644 index 000000000000..0fbe8e9dc32f --- /dev/null +++ b/docs/changelog/91772.yaml @@ -0,0 +1,5 @@ +pr: 91772 +summary: Add `trace.id` to request trace logs +area: Infra/Core +type: bug +issues: [88174] diff --git a/server/src/main/java/org/elasticsearch/http/HttpTracer.java b/server/src/main/java/org/elasticsearch/http/HttpTracer.java index 2978f00d7a16..9e879c44aea6 100644 --- a/server/src/main/java/org/elasticsearch/http/HttpTracer.java +++ b/server/src/main/java/org/elasticsearch/http/HttpTracer.java @@ -16,6 +16,7 @@ import org.elasticsearch.core.Nullable; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestUtils; import org.elasticsearch.tasks.Task; import org.elasticsearch.transport.TransportService; @@ -33,6 +34,11 @@ class HttpTracer { private volatile String[] tracerLogInclude; private volatile String[] tracerLogExclude; + // for testing + HttpTracer() { + tracerLogInclude = tracerLogExclude = new String[0]; + } + HttpTracer(Settings settings, ClusterSettings clusterSettings) { setTracerLogInclude(HttpTransportSettings.SETTING_HTTP_TRACE_LOG_INCLUDE.get(settings)); @@ -55,14 +61,17 @@ class HttpTracer { @Nullable HttpTracer maybeLogRequest(RestRequest restRequest, @Nullable Exception e) { if (logger.isTraceEnabled() && TransportService.shouldTraceAction(restRequest.uri(), tracerLogInclude, tracerLogExclude)) { + // trace.id in the response log is included from threadcontext, which isn't set at request log time + // so include it here as part of the message logger.trace( () -> format( - "[%s][%s][%s][%s] received request from [%s]", + "[%s][%s][%s][%s] received request from [%s]%s", restRequest.getRequestId(), restRequest.header(Task.X_OPAQUE_ID_HTTP_HEADER), restRequest.method(), restRequest.uri(), - restRequest.getHttpChannel() + restRequest.getHttpChannel(), + RestUtils.extractTraceId(restRequest.header(Task.TRACE_PARENT_HTTP_HEADER)).map(t -> " trace.id: " + t).orElse("") ), e ); @@ -89,6 +98,7 @@ void logResponse( long requestId, boolean success ) { + // trace id is included in the ThreadContext for the response logger.trace( () -> format( "[%s][%s][%s][%s][%s] sent response to [%s] success [%s]", diff --git a/server/src/main/java/org/elasticsearch/rest/RestController.java b/server/src/main/java/org/elasticsearch/rest/RestController.java index 81825c790cc1..c314c4dbc82d 100644 --- a/server/src/main/java/org/elasticsearch/rest/RestController.java +++ b/server/src/main/java/org/elasticsearch/rest/RestController.java @@ -44,6 +44,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; @@ -561,8 +562,9 @@ private void copyRestHeaders(RestRequest request, ThreadContext threadContext) { throw new IllegalArgumentException("multiple values for single-valued header [" + name + "]."); } else if (name.equals(Task.TRACE_PARENT_HTTP_HEADER)) { String traceparent = distinctHeaderValues.get(0); - if (traceparent.length() >= 55) { - threadContext.putHeader(Task.TRACE_ID, traceparent.substring(3, 35)); + Optional traceId = RestUtils.extractTraceId(traceparent); + if (traceId.isPresent()) { + threadContext.putHeader(Task.TRACE_ID, traceId.get()); threadContext.putTransient("parent_" + Task.TRACE_PARENT_HTTP_HEADER, traceparent); } } else if (name.equals(Task.TRACE_STATE)) { diff --git a/server/src/main/java/org/elasticsearch/rest/RestUtils.java b/server/src/main/java/org/elasticsearch/rest/RestUtils.java index 80d68161243d..39ef8200424b 100644 --- a/server/src/main/java/org/elasticsearch/rest/RestUtils.java +++ b/server/src/main/java/org/elasticsearch/rest/RestUtils.java @@ -16,6 +16,7 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Map; +import java.util.Optional; import java.util.regex.Pattern; public class RestUtils { @@ -236,6 +237,18 @@ public static String[] corsSettingAsArray(String corsSetting) { if (Strings.isNullOrEmpty(corsSetting)) { return new String[0]; } - return Arrays.asList(corsSetting.split(",")).stream().map(String::trim).toArray(size -> new String[size]); + return Arrays.stream(corsSetting.split(",")).map(String::trim).toArray(String[]::new); } + + /** + * Extract the trace id from the specified traceparent string. + * @see W3 traceparent spec + * + * @param traceparent The value from the {@code traceparent} HTTP header + * @return The trace id from the traceparent string, or {@code Optional.empty()} if it is not present. + */ + public static Optional extractTraceId(String traceparent) { + return traceparent != null && traceparent.length() >= 55 ? Optional.of(traceparent.substring(3, 35)) : Optional.empty(); + } + } diff --git a/server/src/test/java/org/elasticsearch/http/HttpTracerTests.java b/server/src/test/java/org/elasticsearch/http/HttpTracerTests.java new file mode 100644 index 000000000000..aac41e8349f2 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/http/HttpTracerTests.java @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.http; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.MockLogAppender; +import org.elasticsearch.test.junit.annotations.TestLogging; +import org.elasticsearch.test.rest.FakeRestRequest; +import org.elasticsearch.xcontent.NamedXContentRegistry; + +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Map; + +public class HttpTracerTests extends ESTestCase { + + @TestLogging(reason = "Get HttpTracer to output trace logs", value = "org.elasticsearch.http.HttpTracer:TRACE") + public void testLogging() { + String httpTracerLog = HttpTracer.class.getName(); + + MockLogAppender appender = new MockLogAppender(); + try { + appender.start(); + Loggers.addAppender(LogManager.getLogger(httpTracerLog), appender); + + appender.addExpectation( + new MockLogAppender.PatternSeenEventExpectation( + "request log", + httpTracerLog, + Level.TRACE, + "\\[1]\\[idHeader]\\[GET]\\[uri] received request from \\[.*] trace.id: 4bf92f3577b34da6a3ce929d0e0e4736" + ) + ); + appender.addExpectation( + new MockLogAppender.PatternSeenEventExpectation( + "response log", + httpTracerLog, + Level.TRACE, + "\\[1]\\[idHeader]\\[ACCEPTED]\\[text/plain; charset=UTF-8]\\[length] sent response to \\[.*] success \\[true]" + ) + ); + + RestRequest request = new FakeRestRequest.Builder(new NamedXContentRegistry(List.of())).withMethod(RestRequest.Method.GET) + .withPath("uri") + .withHeaders( + Map.of( + Task.X_OPAQUE_ID_HTTP_HEADER, + List.of("idHeader"), + "traceparent", + List.of("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01") + ) + ) + .build(); + + HttpTracer tracer = new HttpTracer().maybeLogRequest(request, null); + assertNotNull(tracer); + + tracer.logResponse( + new RestResponse(RestStatus.ACCEPTED, ""), + new FakeRestRequest.FakeHttpChannel(InetSocketAddress.createUnresolved("127.0.0.1", 9200)), + "length", + "idHeader", + 1L, + true + ); + + appender.assertAllExpectationsMatched(); + } finally { + Loggers.removeAppender(LogManager.getLogger(httpTracerLog), appender); + appender.stop(); + } + } +} From 261f184c22e42728ef7810c59edacbfeba827e4a Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Wed, 23 Nov 2022 16:54:54 +0000 Subject: [PATCH 060/919] [DOCS] disable the ILM history store on full cluster restore (#88515) --- .../snapshot-restore/restore-snapshot.asciidoc | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/reference/snapshot-restore/restore-snapshot.asciidoc b/docs/reference/snapshot-restore/restore-snapshot.asciidoc index 3aeac1c8556c..4fce5e99f449 100644 --- a/docs/reference/snapshot-restore/restore-snapshot.asciidoc +++ b/docs/reference/snapshot-restore/restore-snapshot.asciidoc @@ -289,14 +289,15 @@ the cluster. . Temporarily stop indexing and turn off the following features: + -- -* GeoIP database downloader +* GeoIP database downloader and ILM history store + [source,console] ---- PUT _cluster/settings { "persistent": { - "ingest.geoip.downloader.enabled": false + "ingest.geoip.downloader.enabled": false, + "indices.lifecycle.history_index_enabled": false } } ---- @@ -429,14 +430,15 @@ POST _snapshot/my_repository/my_snapshot_2099.05.06/_restore features you stopped: + -- -* GeoIP database downloader +* GeoIP database downloader and ILM history store + [source,console] ---- PUT _cluster/settings { "persistent": { - "ingest.geoip.downloader.enabled": true + "ingest.geoip.downloader.enabled": true, + "indices.lifecycle.history_index_enabled": true } } ---- From 88e44a9cd32399110431e46ea97929520f0d53d7 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Wed, 23 Nov 2022 17:55:15 +0100 Subject: [PATCH 061/919] Simplify and optimize deduplication of RepositoryData for a non-caching repository instance (#91851) This makes use of the new deduplicator infrastructure to move to more efficient deduplication mechanics. The existing solution hardly ever deduplicated because it would only deduplicate after the repository entered a consistent state. The adjusted solution is much simpler, in that it simply deduplicates such that only a single loading of `RepositoryData` will ever happen at a time, fixing memory issues from massively concurrent loading of the repo data as described in #89952. closes #89952 --- docs/changelog/91851.yaml | 7 + .../blobstore/BlobStoreRepository.java | 156 ++++++++---------- .../blobstore/BlobStoreTestUtil.java | 1 + 3 files changed, 77 insertions(+), 87 deletions(-) create mode 100644 docs/changelog/91851.yaml diff --git a/docs/changelog/91851.yaml b/docs/changelog/91851.yaml new file mode 100644 index 000000000000..36a09f478202 --- /dev/null +++ b/docs/changelog/91851.yaml @@ -0,0 +1,7 @@ +pr: 91851 +summary: Simplify and optimize deduplication of `RepositoryData` for a non-caching + repository instance +area: Snapshot/Restore +type: bug +issues: + - 89952 diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java index d7df332673be..0dc2422f9c53 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -25,7 +25,7 @@ import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRunnable; -import org.elasticsearch.action.ResultDeduplicator; +import org.elasticsearch.action.SingleResultDeduplicator; import org.elasticsearch.action.StepListener; import org.elasticsearch.action.support.GroupedActionListener; import org.elasticsearch.action.support.ListenableActionFuture; @@ -413,7 +413,11 @@ protected BlobStoreRepository( this.namedXContentRegistry = namedXContentRegistry; this.basePath = basePath; this.maxSnapshotCount = MAX_SNAPSHOTS_SETTING.get(metadata.settings()); - this.repoDataDeduplicator = new ResultDeduplicator<>(threadPool.getThreadContext()); + this.repoDataLoadDeduplicator = new SingleResultDeduplicator<>( + threadPool.getThreadContext(), + listener -> threadPool.executor(ThreadPool.Names.SNAPSHOT_META) + .execute(ActionRunnable.wrap(listener, this::doGetRepositoryData)) + ); shardSnapshotTaskRunner = new ShardSnapshotTaskRunner( threadPool.info(ThreadPool.Names.SNAPSHOT).getMax(), threadPool.executor(ThreadPool.Names.SNAPSHOT), @@ -1787,19 +1791,7 @@ public void getRepositoryData(ActionListener listener) { metadata.name(), latestKnownRepoGen ); - // Don't deduplicate repo data loading if we don't have strong consistency guarantees between the repo and the cluster state - // Also, if we are not caching repository data (for tests) we assume that the contents of the repository data at a given - // generation may change - final Executor executor = threadPool.executor(ThreadPool.Names.SNAPSHOT_META); - if (bestEffortConsistency || cacheRepositoryData == false) { - executor.execute(ActionRunnable.wrap(listener, this::doGetRepositoryData)); - } else { - repoDataDeduplicator.executeOnce( - metadata, - listener, - (metadata, l) -> executor.execute(ActionRunnable.wrap(l, this::doGetRepositoryData)) - ); - } + repoDataLoadDeduplicator.execute(listener); } } @@ -1843,78 +1835,70 @@ private void initializeRepoGenerationTracking(ActionListener lis } existingListener.onFailure(e); }; - threadPool.generic() - .execute( - ActionRunnable.wrap( - ActionListener.wrap( - repoData -> submitUnbatchedTask( - "set initial safe repository generation [" + metadata.name() + "][" + repoData.getGenId() + "]", - new ClusterStateUpdateTask() { - @Override - public ClusterState execute(ClusterState currentState) { - RepositoryMetadata metadata = getRepoMetadata(currentState); - // No update to the repository generation should have occurred concurrently in general except - // for - // extreme corner cases like failing over to an older version master node and back to the - // current - // node concurrently - if (metadata.generation() != RepositoryData.UNKNOWN_REPO_GEN) { - throw new RepositoryException( - metadata.name(), - "Found unexpected initialized repo metadata [" + metadata + "]" - ); - } - return ClusterState.builder(currentState) - .metadata( - Metadata.builder(currentState.getMetadata()) - .putCustom( - RepositoriesMetadata.TYPE, - currentState.metadata() - .custom(RepositoriesMetadata.TYPE) - .withUpdatedGeneration( - metadata.name(), - repoData.getGenId(), - repoData.getGenId() - ) - ) + repoDataLoadDeduplicator.execute( + ActionListener.wrap( + repoData -> submitUnbatchedTask( + "set initial safe repository generation [" + metadata.name() + "][" + repoData.getGenId() + "]", + new ClusterStateUpdateTask() { + @Override + public ClusterState execute(ClusterState currentState) { + RepositoryMetadata metadata = getRepoMetadata(currentState); + // No update to the repository generation should have occurred concurrently in general except + // for + // extreme corner cases like failing over to an older version master node and back to the + // current + // node concurrently + if (metadata.generation() != RepositoryData.UNKNOWN_REPO_GEN) { + throw new RepositoryException( + metadata.name(), + "Found unexpected initialized repo metadata [" + metadata + "]" + ); + } + return ClusterState.builder(currentState) + .metadata( + Metadata.builder(currentState.getMetadata()) + .putCustom( + RepositoriesMetadata.TYPE, + currentState.metadata() + .custom(RepositoriesMetadata.TYPE) + .withUpdatedGeneration(metadata.name(), repoData.getGenId(), repoData.getGenId()) ) - .build(); - } + ) + .build(); + } - @Override - public void onFailure(Exception e) { - onFailure.accept(e); - } + @Override + public void onFailure(Exception e) { + onFailure.accept(e); + } - @Override - public void clusterStateProcessed(ClusterState oldState, ClusterState newState) { - logger.trace( - "[{}] initialized repository generation in cluster state to [{}]", - metadata.name(), - repoData.getGenId() - ); - // Resolve listeners on generic pool since some callbacks for repository data do additional IO - threadPool.generic().execute(() -> { - final ActionListener existingListener; - synchronized (BlobStoreRepository.this) { - existingListener = repoDataInitialized; - repoDataInitialized = null; - } - existingListener.onResponse(repoData); - logger.trace( - "[{}] called listeners after initializing repository to generation [{}]", - metadata.name(), - repoData.getGenId() - ); - }); + @Override + public void clusterStateProcessed(ClusterState oldState, ClusterState newState) { + logger.trace( + "[{}] initialized repository generation in cluster state to [{}]", + metadata.name(), + repoData.getGenId() + ); + // Resolve listeners on generic pool since some callbacks for repository data do additional IO + threadPool.generic().execute(() -> { + final ActionListener existingListener; + synchronized (BlobStoreRepository.this) { + existingListener = repoDataInitialized; + repoDataInitialized = null; } - } - ), - onFailure - ), - this::doGetRepositoryData - ) - ); + existingListener.onResponse(repoData); + logger.trace( + "[{}] called listeners after initializing repository to generation [{}]", + metadata.name(), + repoData.getGenId() + ); + }); + } + } + ), + onFailure + ) + ); } else { logger.trace( "[{}] waiting for existing initialization of repository metadata generation in cluster state", @@ -1926,11 +1910,9 @@ public void clusterStateProcessed(ClusterState oldState, ClusterState newState) } /** - * {@link RepositoryData} loading deduplicator. This may only be used with consistent generation repositories, meaning - * {@link #bestEffortConsistency} must be {@code false}, in which case we can assume that the {@link RepositoryData} loaded is - * unique for a given value of {@link #metadata} at any point in time. + * Deduplicator that deduplicates the physical loading of {@link RepositoryData} from the repositories' underlying storage. */ - private final ResultDeduplicator repoDataDeduplicator; + private final SingleResultDeduplicator repoDataLoadDeduplicator; private void doGetRepositoryData(ActionListener listener) { // Retry loading RepositoryData in a loop in case we run into concurrent modifications of the repository. diff --git a/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreTestUtil.java b/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreTestUtil.java index 8304901864ba..52e4cf7b7ea8 100644 --- a/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreTestUtil.java +++ b/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreTestUtil.java @@ -423,6 +423,7 @@ private static ClusterService mockClusterService(ClusterState initialState) { final ThreadPool threadPool = mock(ThreadPool.class); when(threadPool.getThreadContext()).thenReturn(threadContext); when(threadPool.executor(ThreadPool.Names.SNAPSHOT)).thenReturn(new SameThreadExecutorService()); + when(threadPool.executor(ThreadPool.Names.SNAPSHOT_META)).thenReturn(new SameThreadExecutorService()); when(threadPool.generic()).thenReturn(new SameThreadExecutorService()); when(threadPool.info(ThreadPool.Names.SNAPSHOT)).thenReturn( new ThreadPool.Info(ThreadPool.Names.SNAPSHOT, ThreadPool.ThreadPoolType.FIXED, randomIntBetween(1, 10)) From 0367e07ef16946c29dd196bb729ca1cb350efcd8 Mon Sep 17 00:00:00 2001 From: William Brafford Date: Wed, 23 Nov 2022 13:21:40 -0500 Subject: [PATCH 062/919] Check stable plugin version at install and load time (#91780) For a node to install a stable plugin, the node's Elasticsearch version must have the same major version as the Elasticsearch libraries against which the stable plugin was built. Additionally, a node will reject stable plugins that have been built with a future version of Elasticsearch. * Add version checks for stable plugins * Update docs/changelog/91780.yaml --- docs/changelog/91780.yaml | 5 ++ .../elasticsearch/plugins/PluginsUtils.java | 25 ++++++- .../plugins/PluginsUtilsTests.java | 69 +++++++++++-------- 3 files changed, 70 insertions(+), 29 deletions(-) create mode 100644 docs/changelog/91780.yaml diff --git a/docs/changelog/91780.yaml b/docs/changelog/91780.yaml new file mode 100644 index 000000000000..1cab6b9c2960 --- /dev/null +++ b/docs/changelog/91780.yaml @@ -0,0 +1,5 @@ +pr: 91780 +summary: Check stable plugin version at install and load time +area: Infra/Plugins +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/plugins/PluginsUtils.java b/server/src/main/java/org/elasticsearch/plugins/PluginsUtils.java index afe5f0b40e3e..e08331ee8fb9 100644 --- a/server/src/main/java/org/elasticsearch/plugins/PluginsUtils.java +++ b/server/src/main/java/org/elasticsearch/plugins/PluginsUtils.java @@ -77,7 +77,30 @@ public static List findPluginDirs(final Path rootPath) throws IOException * Verify the given plugin is compatible with the current Elasticsearch installation. */ public static void verifyCompatibility(PluginDescriptor info) { - if (info.getElasticsearchVersion().equals(Version.CURRENT) == false) { + if (info.isStable()) { + if (info.getElasticsearchVersion().major != Version.CURRENT.major) { + throw new IllegalArgumentException( + "Stable Plugin [" + + info.getName() + + "] was built for Elasticsearch major version " + + info.getElasticsearchVersion().major + + " but version " + + Version.CURRENT + + " is running" + ); + } + if (info.getElasticsearchVersion().after(Version.CURRENT)) { + throw new IllegalArgumentException( + "Stable Plugin [" + + info.getName() + + "] was built for Elasticsearch version " + + info.getElasticsearchVersion() + + " but earlier version " + + Version.CURRENT + + " is running" + ); + } + } else if (info.getElasticsearchVersion().equals(Version.CURRENT) == false) { throw new IllegalArgumentException( "Plugin [" + info.getName() diff --git a/server/src/test/java/org/elasticsearch/plugins/PluginsUtilsTests.java b/server/src/test/java/org/elasticsearch/plugins/PluginsUtilsTests.java index 72dc0806e7ba..755f8dcf482b 100644 --- a/server/src/test/java/org/elasticsearch/plugins/PluginsUtilsTests.java +++ b/server/src/test/java/org/elasticsearch/plugins/PluginsUtilsTests.java @@ -368,40 +368,35 @@ public void testJarHellSpiConflict() throws Exception { assertThat(e.getCause().getMessage(), containsString("DummyClass1")); } - public void testIncompatibleElasticsearchVersion() throws Exception { - PluginDescriptor info = new PluginDescriptor( - "my_plugin", - "desc", - "1.0", - Version.fromId(6000099), - "1.8", - "FakePlugin", - null, - Collections.emptyList(), - false, - false, - false, - false + public void testStableEarlierElasticsearchVersion() throws Exception { + PluginDescriptor info = getPluginDescriptorForVersion(Version.fromId(Version.CURRENT.id + 1), "1.8", true); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> PluginsUtils.verifyCompatibility(info)); + assertThat( + e.getMessage(), + containsString( + "was built for Elasticsearch version " + + Version.fromId(Version.CURRENT.id + 1) + + " but earlier version " + + Version.CURRENT + + " is running" + ) ); + } + + public void testStableIncompatibleElasticsearchVersion() throws Exception { + PluginDescriptor info = getPluginDescriptorForVersion(Version.fromId(6000099), "1.8", true); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> PluginsUtils.verifyCompatibility(info)); + assertThat(e.getMessage(), containsString("was built for Elasticsearch major version 6")); + } + + public void testIncompatibleElasticsearchVersion() throws Exception { + PluginDescriptor info = getPluginDescriptorForVersion(Version.fromId(6000099), "1.8", false); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> PluginsUtils.verifyCompatibility(info)); assertThat(e.getMessage(), containsString("was built for Elasticsearch version 6.0.0")); } public void testIncompatibleJavaVersion() throws Exception { - PluginDescriptor info = new PluginDescriptor( - "my_plugin", - "desc", - "1.0", - Version.CURRENT, - "1000", - "FakePlugin", - null, - Collections.emptyList(), - false, - false, - false, - false - ); + PluginDescriptor info = getPluginDescriptorForVersion(Version.CURRENT, "1000", false); IllegalStateException e = expectThrows(IllegalStateException.class, () -> PluginsUtils.verifyCompatibility(info)); assertThat(e.getMessage(), containsString("my_plugin requires Java")); } @@ -434,4 +429,22 @@ public void testFindPluginDirs() throws IOException { assertThat(PluginsUtils.findPluginDirs(plugins), containsInAnyOrder(fake)); } + private static PluginDescriptor getPluginDescriptorForVersion(Version id, String javaVersion, boolean isStable) { + PluginDescriptor info = new PluginDescriptor( + "my_plugin", + "desc", + "1.0", + id, + javaVersion, + "FakePlugin", + null, + Collections.emptyList(), + false, + false, + false, + isStable + ); + return info; + } + } From e064b0d330a397330d22dff82d2216ab79137e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Slobodan=20Adamovi=C4=87?= Date: Wed, 23 Nov 2022 19:36:46 +0100 Subject: [PATCH 063/919] Add ability to resolve the cluster alias of remote cluster connections (#91724) This commit adds the ability to resolve a cluster alias of a `Transport.Connection` to `RemoteConnectionManager` class. --- .../transport/RemoteConnectionManager.java | 109 +++++++++++++++++- .../RemoteConnectionManagerTests.java | 28 ++++- 2 files changed, 129 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionManager.java b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionManager.java index 16296ac85f1e..25313970e3be 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionManager.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionManager.java @@ -17,6 +17,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; @@ -87,7 +89,7 @@ public void openConnection(DiscoveryNode node, ConnectionProfile profile, Action @Override public Transport.Connection getConnection(DiscoveryNode node) { try { - return delegate.getConnection(node); + return getConnectionInternal(node); } catch (NodeNotConnectedException e) { return new ProxyConnection(getAnyRemoteConnection(), node); } @@ -116,7 +118,7 @@ public Transport.Connection getAnyRemoteConnection() { if (localConnectedNodes.isEmpty() == false) { DiscoveryNode nextNode = localConnectedNodes.get(Math.floorMod(curr, localConnectedNodes.size())); try { - return delegate.getConnection(nextNode); + return getConnectionInternal(nextNode); } catch (NodeNotConnectedException e) { // Ignore. We will manually create an iterator of open nodes } @@ -124,7 +126,7 @@ public Transport.Connection getAnyRemoteConnection() { Set allConnectionNodes = getAllConnectedNodes(); for (DiscoveryNode connectedNode : allConnectionNodes) { try { - return delegate.getConnection(connectedNode); + return getConnectionInternal(connectedNode); } catch (NodeNotConnectedException e) { // Ignore. We will try the next one until all are exhausted. } @@ -152,6 +154,26 @@ public void closeNoBlock() { delegate.closeNoBlock(); } + /** + * This method returns a remote cluster alias for the given transport connection if it targets a node in the remote cluster. + * This method will return an optional empty in case the connection targets the local node or the node in the local cluster. + * + * @param connection the transport connection for which to resolve a remote cluster alias + * @return a cluster alias if the connection target a node in the remote cluster, otherwise an empty result + */ + public static Optional resolveRemoteClusterAlias(Transport.Connection connection) { + Transport.Connection unwrapped = TransportService.unwrapConnection(connection); + if (unwrapped instanceof InternalRemoteConnection remoteConnection) { + return Optional.of(remoteConnection.getClusterAlias()); + } + return Optional.empty(); + } + + private Transport.Connection getConnectionInternal(DiscoveryNode node) throws NodeNotConnectedException { + Transport.Connection connection = delegate.getConnection(node); + return new InternalRemoteConnection(connection, clusterAlias); + } + private synchronized void addConnectedNode(DiscoveryNode addedNode) { this.connectedNodes = CollectionUtils.appendToCopy(this.connectedNodes, addedNode); } @@ -249,4 +271,85 @@ public boolean hasReferences() { @Override public void onRemoved() {} } + + private static final class InternalRemoteConnection implements Transport.Connection { + + private final Transport.Connection connection; + private final String clusterAlias; + + InternalRemoteConnection(Transport.Connection connection, String clusterAlias) { + this.clusterAlias = Objects.requireNonNull(clusterAlias); + this.connection = Objects.requireNonNull(connection); + } + + public String getClusterAlias() { + return clusterAlias; + } + + @Override + public DiscoveryNode getNode() { + return connection.getNode(); + } + + @Override + public void sendRequest(long requestId, String action, TransportRequest request, TransportRequestOptions options) + throws IOException, TransportException { + connection.sendRequest(requestId, action, request, options); + } + + @Override + public void addCloseListener(ActionListener listener) { + connection.addCloseListener(listener); + } + + @Override + public boolean isClosed() { + return connection.isClosed(); + } + + @Override + public Version getVersion() { + return connection.getVersion(); + } + + @Override + public Object getCacheKey() { + return connection.getCacheKey(); + } + + @Override + public void close() { + connection.close(); + } + + @Override + public void onRemoved() { + connection.onRemoved(); + } + + @Override + public void addRemovedListener(ActionListener listener) { + connection.addRemovedListener(listener); + } + + @Override + public void incRef() { + connection.incRef(); + } + + @Override + public boolean tryIncRef() { + return connection.tryIncRef(); + } + + @Override + public boolean decRef() { + return connection.decRef(); + } + + @Override + public boolean hasReferences() { + return connection.hasReferences(); + } + } } diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteConnectionManagerTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteConnectionManagerTests.java index 031bafbaf78f..3e8b36455b9d 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteConnectionManagerTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteConnectionManagerTests.java @@ -20,6 +20,7 @@ import java.util.HashSet; import java.util.Set; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.mockito.ArgumentMatchers.any; @@ -31,7 +32,9 @@ public class RemoteConnectionManagerTests extends ESTestCase { private Transport transport; private RemoteConnectionManager remoteConnectionManager; private ConnectionManager.ConnectionValidator validator = (connection, profile, listener) -> listener.onResponse(null); + private TransportAddress address = new TransportAddress(InetAddress.getLoopbackAddress(), 1000); + @SuppressWarnings("unchecked") @Override public void setUp() throws Exception { super.setUp(); @@ -40,18 +43,15 @@ public void setUp() throws Exception { "remote-cluster", new ClusterConnectionManager(Settings.EMPTY, transport, new ThreadContext(Settings.EMPTY)) ); - } - - @SuppressWarnings("unchecked") - public void testGetConnection() { - TransportAddress address = new TransportAddress(InetAddress.getLoopbackAddress(), 1000); doAnswer(invocationOnMock -> { ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; listener.onResponse(new TestRemoteConnection((DiscoveryNode) invocationOnMock.getArguments()[0])); return null; }).when(transport).openConnection(any(DiscoveryNode.class), any(ConnectionProfile.class), any(ActionListener.class)); + } + public void testGetConnection() { DiscoveryNode node1 = new DiscoveryNode("node-1", address, Version.CURRENT); PlainActionFuture future1 = PlainActionFuture.newFuture(); remoteConnectionManager.connectToRemoteClusterNode(node1, validator, future1); @@ -89,6 +89,24 @@ public void testGetConnection() { assertEquals(1, versions.size()); } + public void testResolveRemoteClusterAlias() { + DiscoveryNode remoteNode1 = new DiscoveryNode("remote-node-1", address, Version.CURRENT); + PlainActionFuture future = PlainActionFuture.newFuture(); + remoteConnectionManager.connectToRemoteClusterNode(remoteNode1, validator, future); + assertTrue(future.isDone()); + + Transport.Connection remoteConnection = remoteConnectionManager.getConnection(remoteNode1); + assertThat(RemoteConnectionManager.resolveRemoteClusterAlias(remoteConnection).get(), equalTo("remote-cluster")); + + Transport.Connection localConnection = mock(Transport.Connection.class); + assertThat(RemoteConnectionManager.resolveRemoteClusterAlias(localConnection).isPresent(), equalTo(false)); + + DiscoveryNode remoteNode2 = new DiscoveryNode("remote-node-2", address, Version.CURRENT); + Transport.Connection proxyConnection = remoteConnectionManager.getConnection(remoteNode2); + assertThat(proxyConnection, instanceOf(RemoteConnectionManager.ProxyConnection.class)); + assertThat(RemoteConnectionManager.resolveRemoteClusterAlias(proxyConnection).get(), equalTo("remote-cluster")); + } + private static class TestRemoteConnection extends CloseableConnection { private final DiscoveryNode node; From 53014c36a092c5a5afd98c19c4651213e6ac9aad Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Thu, 24 Nov 2022 08:41:18 +0100 Subject: [PATCH 064/919] Pre-size hash maps in BalancedShardsAllocator (#91554) --- .../allocator/BalancedShardsAllocator.java | 6 ++++-- .../allocation/AwarenessAllocationTests.java | 16 ++++++++++------ .../decider/DiskThresholdDeciderTests.java | 8 ++++---- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocator.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocator.java index aaaa4cd26b04..3dfd098bd3b9 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocator.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocator.java @@ -38,6 +38,7 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.Maps; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.core.Tuple; import org.elasticsearch.gateway.PriorityComparator; @@ -931,7 +932,7 @@ private Decision decideCanForceAllocateForVacate(ShardRouting shardRouting, Rout * process. In short, this method recreates the status-quo in the cluster. */ private Map buildModelFromAssigned() { - Map nodes = new HashMap<>(); + Map nodes = Maps.newMapWithExpectedSize(routingNodes.size()); for (RoutingNode rn : routingNodes) { ModelNode node = new ModelNode(writeLoadForecaster, metadata, rn); nodes.put(rn.nodeId(), node); @@ -1220,18 +1221,19 @@ private boolean tryRelocateShard(ModelNode minNode, ModelNode maxNode, String id } static class ModelNode implements Iterable { - private final Map indices = new HashMap<>(); private int numShards = 0; private double writeLoad = 0.0; private double diskUsageInBytes = 0.0; private final WriteLoadForecaster writeLoadForecaster; private final Metadata metadata; private final RoutingNode routingNode; + private final Map indices; ModelNode(WriteLoadForecaster writeLoadForecaster, Metadata metadata, RoutingNode routingNode) { this.writeLoadForecaster = writeLoadForecaster; this.metadata = metadata; this.routingNode = routingNode; + this.indices = Maps.newMapWithExpectedSize(routingNode.size() + 10);// some extra to account for shard movements } public ModelIndex getIndex(String indexName) { diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/AwarenessAllocationTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/AwarenessAllocationTests.java index 2f2129ca9e6a..d786c583aa22 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/AwarenessAllocationTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/AwarenessAllocationTests.java @@ -930,17 +930,21 @@ public void testUnassignedShardsWithUnbalancedZones() { assertThat(shardsWithState(clusterState.getRoutingNodes(), INITIALIZING).size(), equalTo(3)); assertThat(shardsWithState(clusterState.getRoutingNodes(), UNASSIGNED).size(), equalTo(1)); // Unassigned shard is expected. - // Cancel all initializing shards and move started primary to another node. AllocationCommands commands = new AllocationCommands(); - String primaryNode = null; + // Cancel all initializing shards for (ShardRouting routing : clusterState.routingTable().allShards()) { - if (routing.primary()) { - primaryNode = routing.currentNodeId(); - } else if (routing.initializing()) { + if (routing.initializing()) { commands.add(new CancelAllocationCommand(routing.shardId().getIndexName(), routing.id(), routing.currentNodeId(), false)); } } - commands.add(new MoveAllocationCommand("test", 0, primaryNode, "A-4")); + // Move started primary to another node. + for (ShardRouting routing : clusterState.routingTable().allShards()) { + if (routing.primary()) { + var currentNodeId = routing.currentNodeId(); + var otherNodeId = randomValueOtherThan(currentNodeId, clusterState.nodes().getNodes().keySet().iterator()::next); + commands.add(new MoveAllocationCommand("test", 0, currentNodeId, otherNodeId)); + } + } clusterState = strategy.reroute(clusterState, commands, false, false, false, ActionListener.noop()).clusterState(); diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderTests.java index d063e4975f60..a81ea558a49a 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderTests.java @@ -521,10 +521,10 @@ public void testDiskThresholdWithAbsoluteSizes() { clusterState = startInitializingShardsAndReroute(strategy, clusterState); logShardStates(clusterState); - // primary shard already has been relocated away - assertThat(clusterState.getRoutingNodes().node(nodeWithPrimary).size(), equalTo(0)); - // node with increased space still has its shard - assertThat(clusterState.getRoutingNodes().node(nodeWithoutPrimary).size(), equalTo(1)); + assertThat( + clusterState.getRoutingNodes().node(nodeWithPrimary).size() + clusterState.getRoutingNodes().node(nodeWithoutPrimary).size(), + equalTo(1) + ); assertThat(clusterState.getRoutingNodes().node("node3").size(), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node4").size(), equalTo(1)); From 908e9515de1749022616af2f5d9d47b5730ffc52 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 24 Nov 2022 10:03:04 +0000 Subject: [PATCH 065/919] Expose params to toXContentChunked as well as per-chunk (#91771) Some responses change shape depending on the supplied `params`, and/or parse certain details out of `params`. By passing the `params` to `toXContentChunked` we can adjust the shape of the returned iterator and/or avoid duplicate parsing effort in ways that are not possible today where `params` is only made available to each leaf `ToXContent` object. --- .../discovery/StableMasterDisruptionIT.java | 2 +- .../snapshots/get/GetSnapshotsResponse.java | 2 +- .../snapshots/status/SnapshotStatus.java | 2 +- .../status/SnapshotsStatusResponse.java | 6 ++++-- .../mapping/get/GetMappingsResponse.java | 2 +- .../indices/recovery/RecoveryResponse.java | 2 +- .../segments/IndicesSegmentResponse.java | 2 +- .../settings/get/GetSettingsResponse.java | 2 +- .../fieldcaps/FieldCapabilitiesResponse.java | 2 +- .../nodes/BaseNodesXContentResponse.java | 2 +- .../common/xcontent/ChunkedToXContent.java | 4 ++-- .../org/elasticsearch/health/Diagnosis.java | 11 ++++++++--- .../elasticsearch/health/GetHealthAction.java | 4 ++-- .../health/HealthIndicatorResult.java | 9 +++++++-- .../rest/ChunkedRestResponseBody.java | 2 +- .../get/GetSnapshotsResponseTests.java | 3 ++- .../status/SnapshotsStatusResponseTests.java | 4 +++- .../mapping/get/GetMappingsResponseTests.java | 4 +++- .../recovery/RecoveryResponseTests.java | 3 ++- .../segments/IndicesSegmentResponseTests.java | 5 +++-- .../settings/get/GetSettingsResponseTests.java | 4 +++- .../MergedFieldCapabilitiesResponseTests.java | 6 ++++-- .../health/GetHealthResponseTests.java | 6 +++--- .../health/HealthIndicatorResultTests.java | 8 ++++---- .../rest/ChunkedRestResponseBodyTests.java | 18 ++++++++++-------- .../test/AbstractXContentTestCase.java | 2 +- 26 files changed, 71 insertions(+), 46 deletions(-) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/discovery/StableMasterDisruptionIT.java b/server/src/internalClusterTest/java/org/elasticsearch/discovery/StableMasterDisruptionIT.java index cf8c44119b22..9508d550404b 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/discovery/StableMasterDisruptionIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/discovery/StableMasterDisruptionIT.java @@ -144,7 +144,7 @@ private void assertMasterStability(Client client, HealthStatus expectedStatus, M private String xContentToString(ChunkedToXContent xContent) throws IOException { XContentBuilder builder = JsonXContent.contentBuilder(); - xContent.toXContentChunked().forEachRemaining(xcontent -> { + xContent.toXContentChunked(ToXContent.EMPTY_PARAMS).forEachRemaining(xcontent -> { try { xcontent.toXContent(builder, ToXContent.EMPTY_PARAMS); } catch (IOException e) { diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponse.java index 026435f3a9b2..f80ace56b62c 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponse.java @@ -165,7 +165,7 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public Iterator toXContentChunked() { + public Iterator toXContentChunked(ToXContent.Params params) { return Iterators.concat(Iterators.single((b, p) -> { b.startObject(); b.startArray("snapshots"); diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotStatus.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotStatus.java index d9b47f7ecc23..fa635094a1ba 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotStatus.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotStatus.java @@ -190,7 +190,7 @@ public SnapshotStats getStats() { private static final String INCLUDE_GLOBAL_STATE = "include_global_state"; @Override - public Iterator toXContentChunked() { + public Iterator toXContentChunked(ToXContent.Params params) { return Iterators.concat(Iterators.single((ToXContent) (b, p) -> { b.startObject() .field(SNAPSHOT, snapshot.getSnapshotId().getName()) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponse.java index 13069119d3b9..3ac90f882ab2 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponse.java @@ -89,11 +89,13 @@ public int hashCode() { } @Override - public Iterator toXContentChunked() { + public Iterator toXContentChunked(ToXContent.Params params) { return Iterators.concat( Iterators.single((ToXContent) (b, p) -> b.startObject().startArray("snapshots")), snapshots.stream() - .flatMap(s -> StreamSupport.stream(Spliterators.spliteratorUnknownSize(s.toXContentChunked(), Spliterator.ORDERED), false)) + .flatMap( + s -> StreamSupport.stream(Spliterators.spliteratorUnknownSize(s.toXContentChunked(params), Spliterator.ORDERED), false) + ) .iterator(), Iterators.single((b, p) -> b.endArray().endObject()) ); diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponse.java b/server/src/main/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponse.java index b2360de13238..e1db53e5e3e5 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponse.java @@ -67,7 +67,7 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public Iterator toXContentChunked() { + public Iterator toXContentChunked(ToXContent.Params outerParams) { return Iterators.concat( Iterators.single((b, p) -> b.startObject()), getMappings().entrySet().stream().map(indexEntry -> (ToXContent) (builder, params) -> { diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/recovery/RecoveryResponse.java b/server/src/main/java/org/elasticsearch/action/admin/indices/recovery/RecoveryResponse.java index e53f17b68559..1ec8f4ea90df 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/recovery/RecoveryResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/recovery/RecoveryResponse.java @@ -65,7 +65,7 @@ public Map> shardRecoveryStates() { } @Override - public Iterator toXContentChunked() { + public Iterator toXContentChunked(ToXContent.Params params) { return Iterators.concat( Iterators.single((b, p) -> b.startObject()), shardRecoveryStates.entrySet() diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/segments/IndicesSegmentResponse.java b/server/src/main/java/org/elasticsearch/action/admin/indices/segments/IndicesSegmentResponse.java index 0c5abb578880..c0a7db846043 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/segments/IndicesSegmentResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/segments/IndicesSegmentResponse.java @@ -79,7 +79,7 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public Iterator toXContentChunked() { + public Iterator toXContentChunked(ToXContent.Params outerParams) { return Iterators.concat(Iterators.single(((builder, params) -> { builder.startObject(); RestActions.buildBroadcastShardsHeader(builder, params, this); diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/settings/get/GetSettingsResponse.java b/server/src/main/java/org/elasticsearch/action/admin/indices/settings/get/GetSettingsResponse.java index ad4c49c29a0e..80c00a322452 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/settings/get/GetSettingsResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/settings/get/GetSettingsResponse.java @@ -154,7 +154,7 @@ public String toString() { } @Override - public Iterator toXContentChunked() { + public Iterator toXContentChunked(ToXContent.Params params) { final boolean omitEmptySettings = indexToDefaultSettings.isEmpty(); return toXContentChunked(omitEmptySettings); } diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java index f1bbc144cb4a..7cd8f8adbdd2 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java @@ -152,7 +152,7 @@ private static void writeField(StreamOutput out, Map } @Override - public Iterator toXContentChunked() { + public Iterator toXContentChunked(ToXContent.Params params) { if (indexResponses.size() > 0) { throw new IllegalStateException("cannot serialize non-merged response"); } diff --git a/server/src/main/java/org/elasticsearch/action/support/nodes/BaseNodesXContentResponse.java b/server/src/main/java/org/elasticsearch/action/support/nodes/BaseNodesXContentResponse.java index 2922d33a85d4..4113788aafca 100644 --- a/server/src/main/java/org/elasticsearch/action/support/nodes/BaseNodesXContentResponse.java +++ b/server/src/main/java/org/elasticsearch/action/support/nodes/BaseNodesXContentResponse.java @@ -33,7 +33,7 @@ protected BaseNodesXContentResponse(StreamInput in) throws IOException { } @Override - public final Iterator toXContentChunked() { + public final Iterator toXContentChunked(ToXContent.Params params) { return Iterators.concat(Iterators.single((b, p) -> { b.startObject(); RestActions.buildNodesHeader(b, p, this); diff --git a/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContent.java b/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContent.java index f565df41a7c0..9fe5eefd4588 100644 --- a/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContent.java +++ b/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContent.java @@ -26,7 +26,7 @@ public interface ChunkedToXContent { * {@link ToXContent.Params} for each call until it is fully drained. * @return iterator over chunks of {@link ToXContent} */ - Iterator toXContentChunked(); + Iterator toXContentChunked(ToXContent.Params params); /** * Wraps the given instance in a {@link ToXContentObject} that will fully serialize the instance when serialized. @@ -35,7 +35,7 @@ public interface ChunkedToXContent { */ static ToXContentObject wrapAsXContentObject(ChunkedToXContent chunkedToXContent) { return (builder, params) -> { - Iterator serialization = chunkedToXContent.toXContentChunked(); + Iterator serialization = chunkedToXContent.toXContentChunked(params); while (serialization.hasNext()) { serialization.next().toXContent(builder, params); } diff --git a/server/src/main/java/org/elasticsearch/health/Diagnosis.java b/server/src/main/java/org/elasticsearch/health/Diagnosis.java index 2f3d44b1f699..b3adddd1b6cc 100644 --- a/server/src/main/java/org/elasticsearch/health/Diagnosis.java +++ b/server/src/main/java/org/elasticsearch/health/Diagnosis.java @@ -77,7 +77,7 @@ public Resource(Collection nodes) { } @Override - public Iterator toXContentChunked() { + public Iterator toXContentChunked(ToXContent.Params outerParams) { Iterator valuesIterator; if (nodes != null) { valuesIterator = nodes.stream().map(node -> (ToXContent) (builder, params) -> { @@ -147,11 +147,16 @@ public String getUniqueId() { } @Override - public Iterator toXContentChunked() { + public Iterator toXContentChunked(ToXContent.Params outerParams) { Iterator resourcesIterator = Collections.emptyIterator(); if (affectedResources != null && affectedResources.size() > 0) { resourcesIterator = affectedResources.stream() - .flatMap(s -> StreamSupport.stream(Spliterators.spliteratorUnknownSize(s.toXContentChunked(), Spliterator.ORDERED), false)) + .flatMap( + s -> StreamSupport.stream( + Spliterators.spliteratorUnknownSize(s.toXContentChunked(outerParams), Spliterator.ORDERED), + false + ) + ) .iterator(); } return Iterators.concat(Iterators.single((ToXContent) (builder, params) -> { diff --git a/server/src/main/java/org/elasticsearch/health/GetHealthAction.java b/server/src/main/java/org/elasticsearch/health/GetHealthAction.java index e8a8a823308e..2029717b3545 100644 --- a/server/src/main/java/org/elasticsearch/health/GetHealthAction.java +++ b/server/src/main/java/org/elasticsearch/health/GetHealthAction.java @@ -93,7 +93,7 @@ public void writeTo(StreamOutput out) throws IOException { @Override @SuppressWarnings("unchecked") - public Iterator toXContentChunked() { + public Iterator toXContentChunked(ToXContent.Params outerParams) { return Iterators.concat(Iterators.single((ToXContent) (builder, params) -> { builder.startObject(); if (status != null) { @@ -111,7 +111,7 @@ public Iterator toXContentChunked() { // indicators however the affected resources which are the O(indices) fields are // flat mapped over all diagnoses within the indicator Iterators.single((ToXContent) (builder, params) -> builder.field(indicator.name())), - indicator.toXContentChunked() + indicator.toXContentChunked(outerParams) ) ) .toArray(Iterator[]::new) diff --git a/server/src/main/java/org/elasticsearch/health/HealthIndicatorResult.java b/server/src/main/java/org/elasticsearch/health/HealthIndicatorResult.java index 46ecabca1590..8ce2818c371c 100644 --- a/server/src/main/java/org/elasticsearch/health/HealthIndicatorResult.java +++ b/server/src/main/java/org/elasticsearch/health/HealthIndicatorResult.java @@ -28,11 +28,16 @@ public record HealthIndicatorResult( List diagnosisList ) implements ChunkedToXContent { @Override - public Iterator toXContentChunked() { + public Iterator toXContentChunked(ToXContent.Params outerParams) { Iterator diagnosisIterator = Collections.emptyIterator(); if (diagnosisList != null && diagnosisList.isEmpty() == false) { diagnosisIterator = diagnosisList.stream() - .flatMap(s -> StreamSupport.stream(Spliterators.spliteratorUnknownSize(s.toXContentChunked(), Spliterator.ORDERED), false)) + .flatMap( + s -> StreamSupport.stream( + Spliterators.spliteratorUnknownSize(s.toXContentChunked(outerParams), Spliterator.ORDERED), + false + ) + ) .iterator(); } return Iterators.concat(Iterators.single((ToXContent) (builder, params) -> { diff --git a/server/src/main/java/org/elasticsearch/rest/ChunkedRestResponseBody.java b/server/src/main/java/org/elasticsearch/rest/ChunkedRestResponseBody.java index 746727e29555..f9c5aadc1f56 100644 --- a/server/src/main/java/org/elasticsearch/rest/ChunkedRestResponseBody.java +++ b/server/src/main/java/org/elasticsearch/rest/ChunkedRestResponseBody.java @@ -81,7 +81,7 @@ public void write(byte[] b, int off, int len) throws IOException { Streams.noCloseStream(out) ); - private final Iterator serialization = chunkedToXContent.toXContentChunked(); + private final Iterator serialization = chunkedToXContent.toXContentChunked(params); private BytesStream target; diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponseTests.java index c2a3a265a33c..bd7e416409ef 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponseTests.java @@ -43,6 +43,7 @@ import static org.elasticsearch.snapshots.SnapshotInfo.INDEX_DETAILS_XCONTENT_PARAM; import static org.elasticsearch.test.AbstractXContentTestCase.chunkedXContentTester; +import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; import static org.hamcrest.CoreMatchers.containsString; public class GetSnapshotsResponseTests extends ESTestCase { @@ -179,7 +180,7 @@ public void testFromXContent() throws IOException { public void testToChunkedXContent() { final GetSnapshotsResponse response = createTestInstance(); - final Iterator serialization = response.toXContentChunked(); + final Iterator serialization = response.toXContentChunked(EMPTY_PARAMS); int chunks = 0; while (serialization.hasNext()) { serialization.next(); diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponseTests.java index df4fe643ab25..26baa1a337ef 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponseTests.java @@ -17,6 +17,8 @@ import java.util.List; import java.util.function.Predicate; +import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; + public class SnapshotsStatusResponseTests extends AbstractChunkedSerializingTestCase { @Override @@ -58,7 +60,7 @@ public void testChunkCount() { // open and close chunk + one chunk per index chunksExpected += 2 + snapshot.getIndices().size(); } - final var iterator = instance.toXContentChunked(); + final var iterator = instance.toXContentChunked(EMPTY_PARAMS); int chunksSeen = 0; while (iterator.hasNext()) { iterator.next(); diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponseTests.java index 520737cc07c3..0c58846483f5 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponseTests.java @@ -22,6 +22,8 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; + public class GetMappingsResponseTests extends AbstractWireSerializingTestCase { public void testCheckEqualsAndHashCode() { @@ -73,7 +75,7 @@ public void testChunkedXContentUsesChunkPerIndex() { .mapToObj(i -> "index-" + i) .collect(Collectors.toUnmodifiableMap(Function.identity(), k -> createMappingsForIndex())) ); - final var chunks = response.toXContentChunked(); + final var chunks = response.toXContentChunked(EMPTY_PARAMS); int chunkCount = 0; while (chunks.hasNext()) { chunks.next(); diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/recovery/RecoveryResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/recovery/RecoveryResponseTests.java index 6cf36a8c12d2..53a2201ec5f5 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/recovery/RecoveryResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/recovery/RecoveryResponseTests.java @@ -23,6 +23,7 @@ import static java.util.Collections.emptyMap; import static java.util.Collections.emptySet; +import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; public class RecoveryResponseTests extends ESTestCase { @@ -57,7 +58,7 @@ public void testChunkedToXContent() { ), List.of() ); - final var iterator = recoveryResponse.toXContentChunked(); + final var iterator = recoveryResponse.toXContentChunked(EMPTY_PARAMS); int chunks = 0; while (iterator.hasNext()) { iterator.next(); diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/segments/IndicesSegmentResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/segments/IndicesSegmentResponseTests.java index efc68d2bbf97..2c211a66c7b2 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/segments/IndicesSegmentResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/segments/IndicesSegmentResponseTests.java @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.List; +import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; public class IndicesSegmentResponseTests extends ESTestCase { @@ -42,7 +43,7 @@ public void testToXContentSerialiationWithSortedFields() throws Exception { 0, Collections.emptyList() ); - var serialization = response.toXContentChunked(); + var serialization = response.toXContentChunked(EMPTY_PARAMS); try (XContentBuilder builder = jsonBuilder()) { while (serialization.hasNext()) { serialization.next().toXContent(builder, ToXContent.EMPTY_PARAMS); @@ -68,7 +69,7 @@ public void testSerializesOneChunkPerIndex() { Collections.emptyList() ); int chunks = 0; - final var iterator = response.toXContentChunked(); + final var iterator = response.toXContentChunked(EMPTY_PARAMS); while (iterator.hasNext()) { iterator.next(); chunks++; diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/settings/get/GetSettingsResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/settings/get/GetSettingsResponseTests.java index 8e007bfcac39..1fb3590c6a47 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/settings/get/GetSettingsResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/settings/get/GetSettingsResponseTests.java @@ -21,6 +21,8 @@ import java.util.Set; import java.util.function.Predicate; +import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; + public class GetSettingsResponseTests extends AbstractChunkedSerializingTestCase { @Override @@ -76,7 +78,7 @@ protected Predicate getRandomFieldsExcludeFilter() { public void testOneChunkPerIndex() { final var instance = createTestInstance(); - final var iterator = instance.toXContentChunked(); + final var iterator = instance.toXContentChunked(EMPTY_PARAMS); int chunks = 0; while (iterator.hasNext()) { chunks++; diff --git a/server/src/test/java/org/elasticsearch/action/fieldcaps/MergedFieldCapabilitiesResponseTests.java b/server/src/test/java/org/elasticsearch/action/fieldcaps/MergedFieldCapabilitiesResponseTests.java index 94c339e417c0..7954543c1794 100644 --- a/server/src/test/java/org/elasticsearch/action/fieldcaps/MergedFieldCapabilitiesResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/fieldcaps/MergedFieldCapabilitiesResponseTests.java @@ -21,6 +21,8 @@ import java.util.Map; import java.util.function.Predicate; +import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; + public class MergedFieldCapabilitiesResponseTests extends AbstractChunkedSerializingTestCase { @Override @@ -207,7 +209,7 @@ private static FieldCapabilitiesResponse createSimpleResponse() { public void testExpectedChunkSizes() { { final FieldCapabilitiesResponse instance = FieldCapabilitiesResponseTests.createResponseWithFailures(); - final var iterator = instance.toXContentChunked(); + final var iterator = instance.toXContentChunked(EMPTY_PARAMS); int chunks = 0; while (iterator.hasNext()) { iterator.next(); @@ -221,7 +223,7 @@ public void testExpectedChunkSizes() { } { final FieldCapabilitiesResponse instance = createTestInstance(); - final var iterator = instance.toXContentChunked(); + final var iterator = instance.toXContentChunked(EMPTY_PARAMS); int chunks = 0; while (iterator.hasNext()) { iterator.next(); diff --git a/server/src/test/java/org/elasticsearch/health/GetHealthResponseTests.java b/server/src/test/java/org/elasticsearch/health/GetHealthResponseTests.java index 454383d3ecf3..0c16f22bc765 100644 --- a/server/src/test/java/org/elasticsearch/health/GetHealthResponseTests.java +++ b/server/src/test/java/org/elasticsearch/health/GetHealthResponseTests.java @@ -13,7 +13,6 @@ import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.health.GetHealthAction.Response; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; @@ -24,6 +23,7 @@ import java.util.Locale; import java.util.Map; +import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; import static org.hamcrest.Matchers.is; public class GetHealthResponseTests extends ESTestCase { @@ -35,9 +35,9 @@ public void testToXContent() throws IOException { Response response = new Response(ClusterName.DEFAULT, indicatorResults, true); XContentBuilder builder = XContentFactory.jsonBuilder().prettyPrint(); - response.toXContentChunked().forEachRemaining(xcontent -> { + response.toXContentChunked(EMPTY_PARAMS).forEachRemaining(xcontent -> { try { - xcontent.toXContent(builder, ToXContent.EMPTY_PARAMS); + xcontent.toXContent(builder, EMPTY_PARAMS); } catch (IOException e) { logger.error(e.getMessage(), e); fail(e.getMessage()); diff --git a/server/src/test/java/org/elasticsearch/health/HealthIndicatorResultTests.java b/server/src/test/java/org/elasticsearch/health/HealthIndicatorResultTests.java index df0cf83cd8bb..8825e7d6f113 100644 --- a/server/src/test/java/org/elasticsearch/health/HealthIndicatorResultTests.java +++ b/server/src/test/java/org/elasticsearch/health/HealthIndicatorResultTests.java @@ -70,7 +70,7 @@ public void testToXContent() throws Exception { HealthIndicatorResult result = new HealthIndicatorResult(name, status, symptom, details, impacts, diagnosisList); XContentBuilder builder = XContentFactory.jsonBuilder().prettyPrint(); - result.toXContentChunked().forEachRemaining(xcontent -> { + result.toXContentChunked(ToXContent.EMPTY_PARAMS).forEachRemaining(xcontent -> { try { xcontent.toXContent(builder, ToXContent.EMPTY_PARAMS); } catch (IOException e) { @@ -106,7 +106,7 @@ public void testToXContent() throws Exception { if (diagnosis1.affectedResources() != null) { XContentBuilder diagnosisXContent = XContentFactory.jsonBuilder().prettyPrint(); - diagnosis1.toXContentChunked().forEachRemaining(xcontent -> { + diagnosis1.toXContentChunked(ToXContent.EMPTY_PARAMS).forEachRemaining(xcontent -> { try { xcontent.toXContent(diagnosisXContent, ToXContent.EMPTY_PARAMS); } catch (IOException e) { @@ -131,7 +131,7 @@ public void testToXContent() throws Exception { expectedDiagnosis2.put("help_url", diagnosis2.definition().helpURL()); if (diagnosis2.affectedResources() != null) { XContentBuilder diagnosisXContent = XContentFactory.jsonBuilder().prettyPrint(); - diagnosis2.toXContentChunked().forEachRemaining(xcontent -> { + diagnosis2.toXContentChunked(ToXContent.EMPTY_PARAMS).forEachRemaining(xcontent -> { try { xcontent.toXContent(diagnosisXContent, ToXContent.EMPTY_PARAMS); } catch (IOException e) { @@ -199,7 +199,7 @@ public void testChunkCount() { // -> each Diagnosis yields 5 chunks => 10 chunks from both diagnosis // -> HealthIndicatorResult surrounds the diagnosis list by 2 chunks int chunksExpected = 12; - var iterator = result.toXContentChunked(); + var iterator = result.toXContentChunked(ToXContent.EMPTY_PARAMS); int chunksSeen = 0; while (iterator.hasNext()) { iterator.next(); diff --git a/server/src/test/java/org/elasticsearch/rest/ChunkedRestResponseBodyTests.java b/server/src/test/java/org/elasticsearch/rest/ChunkedRestResponseBodyTests.java index 72bd2c65fc6b..de26b8fc88ae 100644 --- a/server/src/test/java/org/elasticsearch/rest/ChunkedRestResponseBodyTests.java +++ b/server/src/test/java/org/elasticsearch/rest/ChunkedRestResponseBodyTests.java @@ -30,16 +30,18 @@ public class ChunkedRestResponseBodyTests extends ESTestCase { public void testEncodesChunkedXContentCorrectly() throws IOException { - final ChunkedToXContent chunkedToXContent = () -> Iterators.forArray(new ToXContent[] { (b, p) -> b.startObject(), (b, p) -> { - if (randomBoolean()) { - b.flush(); - } - b.mapContents(Map.of("foo", "bar", "some_other_key", "some_other_value")); - return b; - }, (b, p) -> b.stringListField("list_field", List.of("string", "otherString")).endObject() }); + final ChunkedToXContent chunkedToXContent = (ToXContent.Params outerParams) -> Iterators.forArray( + new ToXContent[] { (b, p) -> b.startObject(), (b, p) -> { + if (randomBoolean()) { + b.flush(); + } + b.mapContents(Map.of("foo", "bar", "some_other_key", "some_other_value")); + return b; + }, (b, p) -> b.stringListField("list_field", List.of("string", "otherString")).endObject() } + ); final XContent randomXContent = randomFrom(XContentType.values()).xContent(); final XContentBuilder builderDirect = XContentBuilder.builder(randomXContent); - var iter = chunkedToXContent.toXContentChunked(); + var iter = chunkedToXContent.toXContentChunked(ToXContent.EMPTY_PARAMS); while (iter.hasNext()) { iter.next().toXContent(builderDirect, ToXContent.EMPTY_PARAMS); } diff --git a/test/framework/src/main/java/org/elasticsearch/test/AbstractXContentTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/AbstractXContentTestCase.java index 157276b8ef10..28010f5b2f5b 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/AbstractXContentTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractXContentTestCase.java @@ -91,7 +91,7 @@ public static XContentTester chunkedXContentTes ) { return new XContentTester<>(createParser, instanceSupplier, (testInstance, xContentType) -> { try (XContentBuilder builder = XContentBuilder.builder(xContentType.xContent())) { - var serialization = testInstance.toXContentChunked(); + var serialization = testInstance.toXContentChunked(toXContentParams); while (serialization.hasNext()) { serialization.next().toXContent(builder, toXContentParams); } From c417658e1eb89da989ab03291fecec38f8742453 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 24 Nov 2022 10:14:57 +0000 Subject: [PATCH 066/919] Fix compile error introduced in #91771 --- .../cluster/allocation/DesiredBalanceResponse.java | 12 ++++++------ .../allocation/DesiredBalanceResponseTests.java | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/allocation/DesiredBalanceResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/allocation/DesiredBalanceResponse.java index 9a0f39e8680f..7b5dc5635d69 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/allocation/DesiredBalanceResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/allocation/DesiredBalanceResponse.java @@ -60,21 +60,21 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public Iterator toXContentChunked() { - return Iterators.concat(Iterators.single((builder, params) -> { + public Iterator toXContentChunked(ToXContent.Params params) { + return Iterators.concat(Iterators.single((builder, p) -> { builder.startObject(); builder.startObject("stats"); - stats.toXContent(builder, params); + stats.toXContent(builder, p); builder.endObject(); return builder.startObject("routing_table"); - }), routingTable.entrySet().stream().map(indexEntry -> (ToXContent) (builder, params) -> { + }), routingTable.entrySet().stream().map(indexEntry -> (ToXContent) (builder, p) -> { builder.startObject(indexEntry.getKey()); for (Map.Entry shardEntry : indexEntry.getValue().entrySet()) { builder.field(String.valueOf(shardEntry.getKey())); - shardEntry.getValue().toXContent(builder, params); + shardEntry.getValue().toXContent(builder, p); } return builder.endObject(); - }).iterator(), Iterators.single((builder, params) -> builder.endObject().endObject())); + }).iterator(), Iterators.single((builder, p) -> builder.endObject().endObject())); } public DesiredBalanceStats getStats() { diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/allocation/DesiredBalanceResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/allocation/DesiredBalanceResponseTests.java index 6bc6cecbaf3d..51106c075bd5 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/allocation/DesiredBalanceResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/allocation/DesiredBalanceResponseTests.java @@ -158,7 +158,7 @@ public void testToXContent() throws IOException { public void testToChunkedXContent() { DesiredBalanceResponse response = new DesiredBalanceResponse(randomStats(), randomRoutingTable()); - var toXContentChunked = response.toXContentChunked(); + var toXContentChunked = response.toXContentChunked(ToXContent.EMPTY_PARAMS); int chunks = 0; while (toXContentChunked.hasNext()) { toXContentChunked.next(); From ab4c6edcb7101da2c4b7ca1396cf9dee218a3b4a Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 24 Nov 2022 11:19:55 +0100 Subject: [PATCH 067/919] Get remote access privileges for authenticated subject (#91734) This PR adds support for resolving a remote access role descriptors intersection for an authenticated subject, on the querying cluster. It does so by using the role info available in authorization information (for RBACEngine) and provides an interface method for custom engines to likewise implement support. The intersection represents the indices privileges a given user has towards a target remote cluster. This functionality is not hooked up to any active flows yet. It will be used in the security transport interceptor to construct the remote access header to be sent from the querying cluster to the fulfilling cluster for RCS 2.0. The POC PR highlights the tentative usage of functionality added in this PR. --- .../security/authz/AuthorizationEngine.java | 12 + .../core/security/authz/RoleDescriptor.java | 31 ++- .../authz/RoleDescriptorsIntersection.java | 3 + .../authc/AuthenticationTestHelper.java | 4 + .../security/authz/RoleDescriptorTests.java | 119 +++++++-- .../security/authz/AuthorizationService.java | 42 ++++ .../xpack/security/authz/RBACEngine.java | 115 ++++++++- .../authz/AuthorizationServiceIntegTests.java | 200 ++++++++++++++++ .../xpack/security/authz/RBACEngineTests.java | 225 ++++++++++++++++++ 9 files changed, 720 insertions(+), 31 deletions(-) create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceIntegTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index 7c69ee63d7d8..553d731c3ab2 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -236,6 +236,18 @@ void checkPrivileges( */ void getUserPrivileges(AuthorizationInfo authorizationInfo, ActionListener listener); + /** + * Retrieve remote access privileges for a given target cluster, from the provided authorization information, to be sent together + * with a cross-cluster request (e.g. CCS) from an originating cluster to the target cluster. + */ + default void getRemoteAccessRoleDescriptorsIntersection( + final String remoteClusterAlias, + final AuthorizationInfo authorizationInfo, + final ActionListener listener + ) { + throw new UnsupportedOperationException("retrieving remote access role descriptors is not supported by this authorization engine"); + } + /** * Interface for objects that contains the information needed to authorize a request */ diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java index 847b5f36747b..3be2c511b3b4 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java @@ -1034,7 +1034,7 @@ public RemoteIndicesPrivileges build() { * A class representing permissions for a group of indices mapped to * privileges, field permissions, and a query. */ - public static class IndicesPrivileges implements ToXContentObject, Writeable { + public static class IndicesPrivileges implements ToXContentObject, Writeable, Comparable { private static final IndicesPrivileges[] NONE = new IndicesPrivileges[0]; @@ -1214,6 +1214,35 @@ public static void write(StreamOutput out, IndicesPrivileges privileges) throws privileges.writeTo(out); } + @Override + public int compareTo(IndicesPrivileges o) { + if (this == o) { + return 0; + } + int cmp = Boolean.compare(allowRestrictedIndices, o.allowRestrictedIndices); + if (cmp != 0) { + return cmp; + } + cmp = Arrays.compare(indices, o.indices); + if (cmp != 0) { + return cmp; + } + cmp = Arrays.compare(privileges, o.privileges); + if (cmp != 0) { + return cmp; + } + cmp = Objects.compare(query, o.query, Comparator.nullsFirst(BytesReference::compareTo)); + if (cmp != 0) { + return cmp; + } + cmp = Arrays.compare(grantedFields, o.grantedFields); + if (cmp != 0) { + return cmp; + } + cmp = Arrays.compare(deniedFields, o.deniedFields); + return cmp; + } + public static class Builder { private IndicesPrivileges indicesPrivileges = new IndicesPrivileges(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptorsIntersection.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptorsIntersection.java index 9ba992d69f44..16bf0a074c67 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptorsIntersection.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptorsIntersection.java @@ -18,11 +18,14 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Set; public record RoleDescriptorsIntersection(Collection> roleDescriptorsList) implements ToXContentObject, Writeable { + public static RoleDescriptorsIntersection EMPTY = new RoleDescriptorsIntersection(Collections.emptyList()); + public RoleDescriptorsIntersection(StreamInput in) throws IOException { this(in.readImmutableList(inner -> inner.readSet(RoleDescriptor::new))); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTestHelper.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTestHelper.java index 3b86b6b32f46..abfb0e5e6190 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTestHelper.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTestHelper.java @@ -90,6 +90,10 @@ public static User randomUser() { ); } + public static User randomInternalUser() { + return ESTestCase.randomFrom(INTERNAL_USERS); + } + public static User userWithRandomMetadataAndDetails(final String username, final String... roles) { return new User( username, diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptorTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptorTests.java index b0002578d524..ed88c6835f7f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptorTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptorTests.java @@ -48,7 +48,9 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.emptyArray; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.Is.is; @@ -655,6 +657,79 @@ public void testParseIndicesPrivilegesFailsWhenClustersFieldPresent() { ); } + public void testIndicesPrivilegesCompareTo() { + final RoleDescriptor.IndicesPrivileges indexPrivilege = randomIndicesPrivilegesBuilder().build(); + @SuppressWarnings({ "EqualsWithItself" }) + final int actual = indexPrivilege.compareTo(indexPrivilege); + assertThat(actual, equalTo(0)); + assertThat( + indexPrivilege.compareTo( + RoleDescriptor.IndicesPrivileges.builder() + .indices(indexPrivilege.getIndices().clone()) + .privileges(indexPrivilege.getPrivileges().clone()) + // test for both cases when the query is the same instance or a copy + .query( + (indexPrivilege.getQuery() == null || randomBoolean()) + ? indexPrivilege.getQuery() + : new BytesArray(indexPrivilege.getQuery().toBytesRef()) + ) + .grantedFields(indexPrivilege.getGrantedFields() == null ? null : indexPrivilege.getGrantedFields().clone()) + .deniedFields(indexPrivilege.getDeniedFields() == null ? null : indexPrivilege.getDeniedFields().clone()) + .allowRestrictedIndices(indexPrivilege.allowRestrictedIndices()) + .build() + ), + equalTo(0) + ); + + RoleDescriptor.IndicesPrivileges first = randomIndicesPrivilegesBuilder().allowRestrictedIndices(false).build(); + RoleDescriptor.IndicesPrivileges second = randomIndicesPrivilegesBuilder().allowRestrictedIndices(true).build(); + assertThat(first.compareTo(second), lessThan(0)); + assertThat(second.compareTo(first), greaterThan(0)); + + first = randomIndicesPrivilegesBuilder().indices("a", "b").build(); + second = randomIndicesPrivilegesBuilder().indices("b", "a").allowRestrictedIndices(first.allowRestrictedIndices()).build(); + assertThat(first.compareTo(second), lessThan(0)); + assertThat(second.compareTo(first), greaterThan(0)); + + first = randomIndicesPrivilegesBuilder().privileges("read", "write").build(); + second = randomIndicesPrivilegesBuilder().allowRestrictedIndices(first.allowRestrictedIndices()) + .privileges("write", "read") + .indices(first.getIndices()) + .build(); + assertThat(first.compareTo(second), lessThan(0)); + assertThat(second.compareTo(first), greaterThan(0)); + + first = randomIndicesPrivilegesBuilder().query(randomBoolean() ? null : "{\"match\":{\"field-a\":\"a\"}}").build(); + second = randomIndicesPrivilegesBuilder().allowRestrictedIndices(first.allowRestrictedIndices()) + .query("{\"match\":{\"field-b\":\"b\"}}") + .indices(first.getIndices()) + .privileges(first.getPrivileges()) + .build(); + assertThat(first.compareTo(second), lessThan(0)); + assertThat(second.compareTo(first), greaterThan(0)); + + first = randomIndicesPrivilegesBuilder().grantedFields(randomBoolean() ? null : new String[] { "a", "b" }).build(); + second = randomIndicesPrivilegesBuilder().allowRestrictedIndices(first.allowRestrictedIndices()) + .grantedFields("b", "a") + .indices(first.getIndices()) + .privileges(first.getPrivileges()) + .query(first.getQuery()) + .build(); + assertThat(first.compareTo(second), lessThan(0)); + assertThat(second.compareTo(first), greaterThan(0)); + + first = randomIndicesPrivilegesBuilder().deniedFields(randomBoolean() ? null : new String[] { "a", "b" }).build(); + second = randomIndicesPrivilegesBuilder().allowRestrictedIndices(first.allowRestrictedIndices()) + .deniedFields("b", "a") + .indices(first.getIndices()) + .privileges(first.getPrivileges()) + .query(first.getQuery()) + .grantedFields(first.getGrantedFields()) + .build(); + assertThat(first.compareTo(second), lessThan(0)); + assertThat(second.compareTo(first), greaterThan(0)); + } + public void testGlobalPrivilegesOrdering() throws IOException { final String roleName = randomAlphaOfLengthBetween(3, 30); final String[] applicationNames = generateRandomStringArray(3, randomIntBetween(0, 3), false, true); @@ -892,30 +967,34 @@ public static RoleDescriptor randomRoleDescriptor(boolean allowReservedMetadata, ); } - private static RoleDescriptor.IndicesPrivileges[] randomIndicesPriveleges() { + public static RoleDescriptor.IndicesPrivileges[] randomIndicesPriveleges() { final RoleDescriptor.IndicesPrivileges[] indexPrivileges = new RoleDescriptor.IndicesPrivileges[randomIntBetween(0, 3)]; for (int i = 0; i < indexPrivileges.length; i++) { - final RoleDescriptor.IndicesPrivileges.Builder builder = RoleDescriptor.IndicesPrivileges.builder() - .privileges(randomSubsetOf(randomIntBetween(1, 4), IndexPrivilege.names())) - .indices(generateRandomStringArray(5, randomIntBetween(3, 9), false, false)) - .allowRestrictedIndices(randomBoolean()); - if (randomBoolean()) { - builder.query( - randomBoolean() - ? "{ \"term\": { \"" + randomAlphaOfLengthBetween(3, 24) + "\" : \"" + randomAlphaOfLengthBetween(3, 24) + "\" }" - : "{ \"match_all\": {} }" - ); - } + indexPrivileges[i] = randomIndicesPrivilegesBuilder().build(); + } + return indexPrivileges; + } + + private static RoleDescriptor.IndicesPrivileges.Builder randomIndicesPrivilegesBuilder() { + final RoleDescriptor.IndicesPrivileges.Builder builder = RoleDescriptor.IndicesPrivileges.builder() + .privileges(randomSubsetOf(randomIntBetween(1, 4), IndexPrivilege.names())) + .indices(generateRandomStringArray(5, randomIntBetween(3, 9), false, false)) + .allowRestrictedIndices(randomBoolean()); + if (randomBoolean()) { + builder.query( + randomBoolean() + ? "{ \"term\": { \"" + randomAlphaOfLengthBetween(3, 24) + "\" : \"" + randomAlphaOfLengthBetween(3, 24) + "\" }" + : "{ \"match_all\": {} }" + ); + } + if (randomBoolean()) { if (randomBoolean()) { - if (randomBoolean()) { - builder.grantedFields("*"); - builder.deniedFields(generateRandomStringArray(4, randomIntBetween(4, 9), false, false)); - } else { - builder.grantedFields(generateRandomStringArray(4, randomIntBetween(4, 9), false, false)); - } + builder.grantedFields("*"); + builder.deniedFields(generateRandomStringArray(4, randomIntBetween(4, 9), false, false)); + } else { + builder.grantedFields(generateRandomStringArray(4, randomIntBetween(4, 9), false, false)); } - indexPrivileges[i] = builder.build(); } - return indexPrivileges; + return builder; } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java index 6c2fc43d6cfa..544809a520c0 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java @@ -61,6 +61,7 @@ import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; import org.elasticsearch.xpack.core.security.authz.RestrictedIndices; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver; @@ -202,6 +203,47 @@ public void retrieveUserPrivileges( authorizationEngine.getUserPrivileges(authorizationInfo, wrapPreservingContext(listener, threadContext)); } + public void retrieveRemoteAccessRoleDescriptorsIntersection( + final String remoteClusterAlias, + final Subject subject, + final ActionListener listener + ) { + if (SystemUser.is(subject.getUser())) { + final String message = "the user [" + + subject.getUser().principal() + + "] is the system user and we should never try to retrieve its remote access roles descriptors"; + assert false : message; + listener.onFailure(new IllegalArgumentException(message)); + return; + } + + final AuthorizationEngine authorizationEngine = getAuthorizationEngineForSubject(subject); + final AuthorizationInfo authorizationInfo = threadContext.getTransient(AUTHORIZATION_INFO_KEY); + if (authorizationInfo != null) { + authorizationEngine.getRemoteAccessRoleDescriptorsIntersection( + remoteClusterAlias, + authorizationInfo, + wrapPreservingContext(listener, threadContext) + ); + } else { + assert isInternal(subject.getUser()) + : "authorization info must be available in thread context for all users other than internal users"; + authorizationEngine.resolveAuthorizationInfo( + subject, + wrapPreservingContext( + listener.delegateFailure( + (delegatedLister, resolvedAuthzInfo) -> authorizationEngine.getRemoteAccessRoleDescriptorsIntersection( + remoteClusterAlias, + resolvedAuthzInfo, + wrapPreservingContext(delegatedLister, threadContext) + ) + ), + threadContext + ) + ); + } + } + /** * Verifies that the given user can execute the given request (and action). If the user doesn't * have the appropriate privileges for this action/request, an {@link ElasticsearchSecurityException} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index d6d4b5c60d59..9ff3942d6f1f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -61,6 +61,7 @@ import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField; import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; @@ -82,6 +83,7 @@ import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -118,6 +120,8 @@ public class RBACEngine implements AuthorizationEngine { private static final String DELETE_SUB_REQUEST_REPLICA = DeleteAction.NAME + "[r]"; private static final Logger logger = LogManager.getLogger(RBACEngine.class); + // TODO move once we have a dedicated class for RCS 2.0 constants + public static final String REMOTE_USER_ROLE_NAME = "_remote_user"; private final Settings settings; private final CompositeRolesStore rolesStore; @@ -657,6 +661,94 @@ public void getUserPrivileges(AuthorizationInfo authorizationInfo, ActionListene } } + @Override + public void getRemoteAccessRoleDescriptorsIntersection( + final String remoteClusterAlias, + final AuthorizationInfo authorizationInfo, + final ActionListener listener + ) { + if (authorizationInfo instanceof RBACAuthorizationInfo == false) { + listener.onFailure( + new IllegalArgumentException("unsupported authorization info:" + authorizationInfo.getClass().getSimpleName()) + ); + return; + } + + final Role role = ((RBACAuthorizationInfo) authorizationInfo).getRole(); + final RemoteIndicesPermission remoteIndicesPermission; + try { + remoteIndicesPermission = role.remoteIndices().forCluster(remoteClusterAlias); + } catch (UnsupportedOperationException e) { + // TODO we will need to implement this to support API keys with assigned role descriptors + listener.onFailure( + new IllegalArgumentException( + "cannot retrieve remote access role descriptors for API keys with assigned role descriptors.", + e + ) + ); + return; + } + + if (remoteIndicesPermission.remoteIndicesGroups().isEmpty()) { + listener.onResponse(RoleDescriptorsIntersection.EMPTY); + return; + } + + final List indicesPrivileges = new ArrayList<>(); + for (RemoteIndicesPermission.RemoteIndicesGroup remoteIndicesGroup : remoteIndicesPermission.remoteIndicesGroups()) { + for (IndicesPermission.Group indicesGroup : remoteIndicesGroup.indicesPermissionGroups()) { + indicesPrivileges.add(toIndicesPrivileges(indicesGroup)); + } + } + + listener.onResponse( + new RoleDescriptorsIntersection( + List.of( + Set.of( + new RoleDescriptor( + REMOTE_USER_ROLE_NAME, + null, + // The role descriptors constructed here may be cached in raw byte form, using a hash of their content as a + // cache key; we therefore need deterministic order when constructing them here, to ensure cache hits for + // equivalent role descriptors + indicesPrivileges.stream().sorted().toArray(RoleDescriptor.IndicesPrivileges[]::new), + null, + null, + null, + null, + null + ) + ) + ) + ) + ); + } + + private static RoleDescriptor.IndicesPrivileges toIndicesPrivileges(final IndicesPermission.Group indicesGroup) { + final Set queries = indicesGroup.getQuery(); + final Set fieldGrantExcludeGroups = getFieldGrantExcludeGroups(indicesGroup); + assert queries == null || queries.size() <= 1 + : "translation from an indices permission group to indices privileges supports up to one DLS query but multiple queries found"; + assert fieldGrantExcludeGroups.size() <= 1 + : "translation from an indices permission group to indices privileges supports up to one FLS field-grant-exclude group" + + " but multiple groups found"; + + final BytesReference query = (queries == null || false == queries.iterator().hasNext()) ? null : queries.iterator().next(); + final RoleDescriptor.IndicesPrivileges.Builder builder = RoleDescriptor.IndicesPrivileges.builder() + // Sort because these index privileges will be part of role descriptors that may be cached in raw byte form; + // we need deterministic order to ensure cache hits for equivalent role descriptors + .indices(Arrays.stream(indicesGroup.indices()).sorted().collect(Collectors.toList())) + .privileges(indicesGroup.privilege().name().stream().sorted().collect(Collectors.toList())) + .allowRestrictedIndices(indicesGroup.allowRestrictedIndices()) + .query(query); + if (false == fieldGrantExcludeGroups.isEmpty()) { + final FieldPermissionsDefinition.FieldGrantExcludeGroup fieldGrantExcludeGroup = fieldGrantExcludeGroups.iterator().next(); + builder.grantedFields(fieldGrantExcludeGroup.getGrantedFields()).deniedFields(fieldGrantExcludeGroup.getExcludedFields()); + } + + return builder.build(); + } + static GetUserPrivilegesResponse buildUserPrivilegesResponseObject(Role userRole) { logger.trace(() -> "List privileges for role [" + arrayToCommaDelimitedString(userRole.names()) + "]"); @@ -721,24 +813,27 @@ static GetUserPrivilegesResponse buildUserPrivilegesResponseObject(Role userRole private static GetUserPrivilegesResponse.Indices toIndices(final IndicesPermission.Group group) { final Set queries = group.getQuery() == null ? Collections.emptySet() : group.getQuery(); - final Set fieldSecurity; + final Set fieldSecurity = getFieldGrantExcludeGroups(group); + return new GetUserPrivilegesResponse.Indices( + Arrays.asList(group.indices()), + group.privilege().name(), + fieldSecurity, + queries, + group.allowRestrictedIndices() + ); + } + + private static Set getFieldGrantExcludeGroups(IndicesPermission.Group group) { if (group.getFieldPermissions().hasFieldLevelSecurity()) { final List fieldPermissionsDefinitions = group.getFieldPermissions() .getFieldPermissionsDefinitions(); assert fieldPermissionsDefinitions.size() == 1 : "limited-by field must not exist since we do not support reporting user privileges for limited roles"; final FieldPermissionsDefinition definition = fieldPermissionsDefinitions.get(0); - fieldSecurity = definition.getFieldGrantExcludeGroups(); + return definition.getFieldGrantExcludeGroups(); } else { - fieldSecurity = Collections.emptySet(); + return Collections.emptySet(); } - return new GetUserPrivilegesResponse.Indices( - Arrays.asList(group.indices()), - group.privilege().name(), - fieldSecurity, - queries, - group.allowRestrictedIndices() - ); } static AuthorizedIndices resolveAuthorizedIndicesFromRole( diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceIntegTests.java new file mode 100644 index 000000000000..f4d45b6ad451 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceIntegTests.java @@ -0,0 +1,200 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authz; + +import org.elasticsearch.action.LatchedActionListener; +import org.elasticsearch.action.support.ActionTestUtils; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.test.SecurityIntegTestCase; +import org.elasticsearch.transport.TcpTransport; +import org.elasticsearch.xpack.core.security.SecurityContext; +import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction; +import org.elasticsearch.xpack.core.security.action.user.AuthenticateRequest; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection; +import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; +import org.elasticsearch.xpack.core.security.support.NativeRealmValidationUtil; +import org.elasticsearch.xpack.core.security.user.SystemUser; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.audit.AuditUtil; + +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.AUTHORIZATION_INFO_KEY; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; + +public class AuthorizationServiceIntegTests extends SecurityIntegTestCase { + + @Override + protected boolean addMockHttpTransport() { + return false; // need real http + } + + public void testRetrieveRemoteAccessRoleDescriptorsIntersectionForNonInternalUser() throws IOException, InterruptedException { + assumeTrue("untrusted remote cluster feature flag must be enabled", TcpTransport.isUntrustedRemoteClusterEnabled()); + + final String concreteClusterAlias = randomAlphaOfLength(10); + final String roleName = randomAlphaOfLength(5); + getSecurityClient().putRole( + new RoleDescriptor( + roleName, + randomSubsetOf(ClusterPrivilegeResolver.names()).toArray(String[]::new), + randomBoolean() + ? null + : new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .privileges(randomSubsetOf(randomIntBetween(1, 4), IndexPrivilege.names())) + .indices(generateRandomStringArray(5, randomIntBetween(3, 9), false, false)) + .allowRestrictedIndices(randomBoolean()) + .build() }, + null, + null, + generateRandomStringArray(5, randomIntBetween(2, 8), false, true), + null, + null, + new RoleDescriptor.RemoteIndicesPrivileges[] { + new RoleDescriptor.RemoteIndicesPrivileges( + RoleDescriptor.IndicesPrivileges.builder() + .indices(shuffledList(List.of("index1", "index2"))) + .privileges(shuffledList(List.of("read", "write"))) + .build(), + randomNonEmptySubsetOf(List.of(concreteClusterAlias, "*")).toArray(new String[0]) + ) } + ) + ); + final String nodeName = internalCluster().getRandomNodeName(); + final ThreadContext threadContext = internalCluster().getInstance(SecurityContext.class, nodeName).getThreadContext(); + final AuthorizationService authzService = internalCluster().getInstance(AuthorizationService.class, nodeName); + final Authentication authentication = Authentication.newRealmAuthentication( + new User(randomAlphaOfLengthBetween(5, 16), roleName), + new Authentication.RealmRef( + randomBoolean() ? "realm" : randomAlphaOfLengthBetween(1, 16), + randomAlphaOfLengthBetween(5, 16), + nodeName + ) + ); + final RoleDescriptorsIntersection actual = authorizeThenRetrieveRemoteAccessDescriptors( + threadContext, + authzService, + authentication, + concreteClusterAlias + ); + final String generatedRoleName = actual.roleDescriptorsList().iterator().next().iterator().next().getName(); + assertNull(NativeRealmValidationUtil.validateRoleName(generatedRoleName, false)); + assertThat(generatedRoleName, not(equalTo(roleName))); + assertThat( + actual, + equalTo( + new RoleDescriptorsIntersection( + List.of( + Set.of( + new RoleDescriptor( + generatedRoleName, + null, + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices("index1", "index2") + .privileges("read", "write") + .build() }, + null, + null, + null, + null, + null + ) + ) + ) + ) + ) + ); + } + + public void testRetrieveRemoteAccessRoleDescriptorsIntersectionForInternalUser() throws InterruptedException { + assumeTrue("untrusted remote cluster feature flag must be enabled", TcpTransport.isUntrustedRemoteClusterEnabled()); + + final String nodeName = internalCluster().getRandomNodeName(); + final ThreadContext threadContext = internalCluster().getInstance(SecurityContext.class, nodeName).getThreadContext(); + final AuthorizationService authzService = internalCluster().getInstance(AuthorizationService.class, nodeName); + final Authentication authentication = AuthenticationTestHelper.builder() + .internal(randomValueOtherThan(SystemUser.INSTANCE, AuthenticationTestHelper::randomInternalUser)) + .build(); + final String concreteClusterAlias = randomAlphaOfLength(10); + + // For internal users, we support the situation where there is no authorization information populated in thread context + // We test both scenarios, one where we don't authorize and don't have authorization info in thread context, and one where we do + if (randomBoolean()) { + assertThat(threadContext.getTransient(AUTHORIZATION_INFO_KEY), nullValue()); + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference actual = new AtomicReference<>(); + authzService.retrieveRemoteAccessRoleDescriptorsIntersection( + concreteClusterAlias, + authentication.getEffectiveSubject(), + new LatchedActionListener<>(ActionTestUtils.assertNoFailureListener(newValue -> { + assertThat(threadContext.getTransient(AUTHORIZATION_INFO_KEY), nullValue()); + actual.set(newValue); + }), latch) + ); + latch.await(); + assertThat(actual.get(), equalTo(RoleDescriptorsIntersection.EMPTY)); + // Validate original authz info is restored to null after call complete + assertThat(threadContext.getTransient(AUTHORIZATION_INFO_KEY), nullValue()); + } else { + assertThat( + authorizeThenRetrieveRemoteAccessDescriptors(threadContext, authzService, authentication, concreteClusterAlias), + equalTo(RoleDescriptorsIntersection.EMPTY) + ); + } + } + + private RoleDescriptorsIntersection authorizeThenRetrieveRemoteAccessDescriptors( + final ThreadContext threadContext, + final AuthorizationService authzService, + final Authentication authentication, + final String concreteClusterAlias + ) throws InterruptedException { + try (var ignored = threadContext.stashContext()) { + assertThat(threadContext.getTransient(AUTHORIZATION_INFO_KEY), nullValue()); + final AtomicReference actual = new AtomicReference<>(); + final CountDownLatch latch = new CountDownLatch(1); + // A request ID is set during authentication and is required for authorization; since we are not authenticating, set it + // explicitly + AuditUtil.generateRequestId(threadContext); + // Authorize to populate thread context with authz info + // Note that if the outer listener throws, we will not count down on the latch, however, we also won't get to the await call + // since the exception will be thrown before -- so no deadlock + authzService.authorize( + authentication, + AuthenticateAction.INSTANCE.name(), + AuthenticateRequest.INSTANCE, + ActionTestUtils.assertNoFailureListener(nothing -> { + authzService.retrieveRemoteAccessRoleDescriptorsIntersection( + concreteClusterAlias, + authentication.getEffectiveSubject(), + new LatchedActionListener<>(ActionTestUtils.assertNoFailureListener(newValue -> { + assertThat(threadContext.getTransient(AUTHORIZATION_INFO_KEY), not(nullValue())); + actual.set(newValue); + }), latch) + ); + }) + ); + latch.await(); + // Validate original authz info is restored after call complete + assertThat(threadContext.getTransient(AUTHORIZATION_INFO_KEY), nullValue()); + return actual.get(); + } + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index bec673e26ac9..2b99d6918549 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -25,12 +25,14 @@ import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.core.Tuple; import org.elasticsearch.license.GetLicenseAction; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.TcpTransport; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.core.XPackPlugin; @@ -60,8 +62,10 @@ import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizedIndices; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.PrivilegesCheckResult; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.PrivilegesToCheck; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.ApplicationResourcePrivileges; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection; import org.elasticsearch.xpack.core.security.authz.permission.ApplicationPermission; import org.elasticsearch.xpack.core.security.authz.permission.ClusterPermission; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; @@ -88,13 +92,16 @@ import org.mockito.Mockito; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static java.util.Collections.emptyMap; @@ -123,6 +130,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -1576,6 +1584,201 @@ public void testGetUserPrivilegesThrowsIaeForUnsupportedOperation() { assertThat(e.getCause(), sameInstance(unsupportedOperationException)); } + public void testGetRemoteAccessRoleDescriptorsIntersection() throws ExecutionException, InterruptedException { + assumeTrue("untrusted remote cluster feature flag must be enabled", TcpTransport.isUntrustedRemoteClusterEnabled()); + + final RemoteIndicesPermission.Builder remoteIndicesBuilder = RemoteIndicesPermission.builder(); + final String concreteClusterAlias = randomAlphaOfLength(10); + final int numGroups = randomIntBetween(1, 3); + final List expectedIndicesPrivileges = new ArrayList<>(); + for (int i = 0; i < numGroups; i++) { + final String[] indexNames = Objects.requireNonNull(generateRandomStringArray(3, 10, false, false)); + final boolean allowRestrictedIndices = randomBoolean(); + final boolean hasFls = randomBoolean(); + final FieldPermissionsDefinition.FieldGrantExcludeGroup group = hasFls + ? randomFieldGrantExcludeGroup() + : new FieldPermissionsDefinition.FieldGrantExcludeGroup(null, null); + final BytesReference query = randomBoolean() ? randomDlsQuery() : null; + final IndicesPrivileges.Builder builder = IndicesPrivileges.builder() + .indices(Arrays.stream(indexNames).sorted().collect(Collectors.toList())) + .privileges("read") + .allowRestrictedIndices(allowRestrictedIndices) + .query(query); + if (hasFls) { + builder.grantedFields(group.getGrantedFields()); + builder.deniedFields(group.getExcludedFields()); + } + expectedIndicesPrivileges.add(builder.build()); + remoteIndicesBuilder.addGroup( + Set.of(randomFrom(concreteClusterAlias, "*")), + IndexPrivilege.READ, + new FieldPermissions(new FieldPermissionsDefinition(Set.of(group))), + query == null ? null : Set.of(query), + allowRestrictedIndices, + indexNames + ); + } + final String mismatchedConcreteClusterAlias = randomValueOtherThan(concreteClusterAlias, () -> randomAlphaOfLength(10)); + // Add some groups that don't match the alias + final int numMismatchedGroups = randomIntBetween(0, 3); + for (int i = 0; i < numMismatchedGroups; i++) { + remoteIndicesBuilder.addGroup( + Set.of(mismatchedConcreteClusterAlias), + IndexPrivilege.READ, + new FieldPermissions( + new FieldPermissionsDefinition(Set.of(new FieldPermissionsDefinition.FieldGrantExcludeGroup(null, null))) + ), + null, + randomBoolean(), + generateRandomStringArray(3, 10, false, false) + ); + } + + final Role role = mockRoleWithRemoteIndices(remoteIndicesBuilder.build()); + final RBACAuthorizationInfo authorizationInfo = mock(RBACAuthorizationInfo.class); + when(authorizationInfo.getRole()).thenReturn(role); + + final PlainActionFuture future = new PlainActionFuture<>(); + engine.getRemoteAccessRoleDescriptorsIntersection(concreteClusterAlias, authorizationInfo, future); + final RoleDescriptorsIntersection actual = future.get(); + + assertThat( + actual, + equalTo( + new RoleDescriptorsIntersection( + List.of( + Set.of( + new RoleDescriptor( + RBACEngine.REMOTE_USER_ROLE_NAME, + null, + expectedIndicesPrivileges.stream().sorted().toArray(RoleDescriptor.IndicesPrivileges[]::new), + null, + null, + null, + null, + null + ) + ) + ) + ) + ) + ); + verify(role, times(1)).remoteIndices(); + verifyNoMoreInteractions(role); + } + + public void testGetRemoteAccessRoleDescriptorsIntersectionHasDeterministicOrderForIndicesPrivileges() throws ExecutionException, + InterruptedException { + assumeTrue("untrusted remote cluster feature flag must be enabled", TcpTransport.isUntrustedRemoteClusterEnabled()); + + final RemoteIndicesPermission.Builder remoteIndicesBuilder = RemoteIndicesPermission.builder(); + final String concreteClusterAlias = randomAlphaOfLength(10); + final int numGroups = randomIntBetween(2, 5); + for (int i = 0; i < numGroups; i++) { + remoteIndicesBuilder.addGroup( + Set.copyOf(randomNonEmptySubsetOf(List.of(concreteClusterAlias, "*"))), + IndexPrivilege.get(Set.copyOf(randomSubsetOf(randomIntBetween(1, 4), IndexPrivilege.names()))), + new FieldPermissions( + new FieldPermissionsDefinition( + Set.of( + randomBoolean() + ? randomFieldGrantExcludeGroup() + : new FieldPermissionsDefinition.FieldGrantExcludeGroup(null, null) + ) + ) + ), + randomBoolean() ? Set.of(randomDlsQuery()) : null, + randomBoolean(), + generateRandomStringArray(3, 10, false, false) + ); + } + final RemoteIndicesPermission permissions = remoteIndicesBuilder.build(); + List remoteIndicesGroups = permissions.remoteIndicesGroups(); + final Role role1 = mockRoleWithRemoteIndices(permissions); + final RBACAuthorizationInfo authorizationInfo1 = mock(RBACAuthorizationInfo.class); + when(authorizationInfo1.getRole()).thenReturn(role1); + final PlainActionFuture future1 = new PlainActionFuture<>(); + engine.getRemoteAccessRoleDescriptorsIntersection(concreteClusterAlias, authorizationInfo1, future1); + final RoleDescriptorsIntersection actual1 = future1.get(); + verify(role1, times(1)).remoteIndices(); + verifyNoMoreInteractions(role1); + + // Randomize the order of both remote indices groups and each of the indices permissions groups each group holds + final RemoteIndicesPermission shuffledPermissions = new RemoteIndicesPermission( + shuffledList( + remoteIndicesGroups.stream() + .map( + group -> new RemoteIndicesPermission.RemoteIndicesGroup( + group.remoteClusterAliases(), + shuffledList(group.indicesPermissionGroups()) + ) + ) + .toList() + ) + ); + final Role role2 = mockRoleWithRemoteIndices(shuffledPermissions); + final RBACAuthorizationInfo authorizationInfo2 = mock(RBACAuthorizationInfo.class); + when(authorizationInfo2.getRole()).thenReturn(role2); + final PlainActionFuture future2 = new PlainActionFuture<>(); + engine.getRemoteAccessRoleDescriptorsIntersection(concreteClusterAlias, authorizationInfo2, future2); + final RoleDescriptorsIntersection actual2 = future2.get(); + + verify(role2, times(1)).remoteIndices(); + verifyNoMoreInteractions(role2); + assertThat(actual1, equalTo(actual2)); + assertThat(actual1.roleDescriptorsList().iterator().next().iterator().next().getIndicesPrivileges().length, equalTo(numGroups)); + } + + public void testGetRemoteAccessRoleDescriptorsIntersectionWithoutMatchingGroups() throws ExecutionException, InterruptedException { + assumeTrue("untrusted remote cluster feature flag must be enabled", TcpTransport.isUntrustedRemoteClusterEnabled()); + + final String concreteClusterAlias = randomAlphaOfLength(10); + final Role role = mockRoleWithRemoteIndices( + RemoteIndicesPermission.builder() + .addGroup( + Set.of(concreteClusterAlias), + IndexPrivilege.READ, + new FieldPermissions(new FieldPermissionsDefinition(null, null)), + null, + randomBoolean(), + generateRandomStringArray(3, 10, false, false) + ) + .build() + ); + final RBACAuthorizationInfo authorizationInfo = mock(RBACAuthorizationInfo.class); + when(authorizationInfo.getRole()).thenReturn(role); + + final PlainActionFuture future = new PlainActionFuture<>(); + engine.getRemoteAccessRoleDescriptorsIntersection( + randomValueOtherThan(concreteClusterAlias, () -> randomAlphaOfLength(10)), + authorizationInfo, + future + ); + final RoleDescriptorsIntersection actual = future.get(); + assertThat(actual, equalTo(RoleDescriptorsIntersection.EMPTY)); + verify(role, times(1)).remoteIndices(); + verifyNoMoreInteractions(role); + } + + public void testGetRemoteAccessRoleDescriptorsIntersectionWithoutRemoteIndicesPermissions() throws ExecutionException, + InterruptedException { + assumeTrue("untrusted remote cluster feature flag must be enabled", TcpTransport.isUntrustedRemoteClusterEnabled()); + + final String concreteClusterAlias = randomAlphaOfLength(10); + final Role role = mockRoleWithRemoteIndices(RemoteIndicesPermission.NONE); + final RBACAuthorizationInfo authorizationInfo = mock(RBACAuthorizationInfo.class); + when(authorizationInfo.getRole()).thenReturn(role); + + final PlainActionFuture future = new PlainActionFuture<>(); + engine.getRemoteAccessRoleDescriptorsIntersection( + randomValueOtherThan(concreteClusterAlias, () -> randomAlphaOfLength(10)), + authorizationInfo, + future + ); + final RoleDescriptorsIntersection actual = future.get(); + assertThat(actual, equalTo(RoleDescriptorsIntersection.EMPTY)); + } + private GetUserPrivilegesResponse.Indices findIndexPrivilege(Set indices, String name) { return indices.stream().filter(i -> i.getIndices().contains(name)).findFirst().get(); } @@ -1667,4 +1870,26 @@ private PrivilegesCheckResult hasPrivileges( private static MapBuilder mapBuilder() { return MapBuilder.newMapBuilder(); } + + private BytesArray randomDlsQuery() { + return new BytesArray( + "{ \"term\": { \"" + randomAlphaOfLengthBetween(3, 24) + "\" : \"" + randomAlphaOfLengthBetween(3, 24) + "\" }" + ); + } + + private FieldPermissionsDefinition.FieldGrantExcludeGroup randomFieldGrantExcludeGroup() { + return new FieldPermissionsDefinition.FieldGrantExcludeGroup(generateRandomStringArray(3, 10, false, false), new String[] {}); + } + + private Role mockRoleWithRemoteIndices(final RemoteIndicesPermission remoteIndicesPermission) { + final Role role = mock(Role.class); + final String[] roleNames = generateRandomStringArray(3, 10, false, false); + when(role.names()).thenReturn(roleNames); + when(role.cluster()).thenReturn(ClusterPermission.NONE); + when(role.indices()).thenReturn(IndicesPermission.NONE); + when(role.application()).thenReturn(ApplicationPermission.NONE); + when(role.runAs()).thenReturn(RunAsPermission.NONE); + when(role.remoteIndices()).thenReturn(remoteIndicesPermission); + return role; + } } From b456653fb96ffbafe5b134f2233c2c0f20f992ae Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Thu, 24 Nov 2022 14:24:08 +0200 Subject: [PATCH 068/919] [ML] Load pytorch models from a thread of the inference pool (#91882) In #91661 we fixed a bug where we could leak a thread of the inference thread pool after loading the model through the callbacks. This could cause exhaustion of that pool and thus a deployment could hang whilst waiting for a thread in vain. The fix was to do the loading on a thread of the utility thread pool. However, for debugging purposes it is nicer to do the loading on a thread of the inference thread pool as it would make it easier to understand that such a thread belongs to a pytorch model being loaded. This commit changes model loading to occur back on a thread of the inference thread pool but this time we take care that in the callback we return on a thread of the utility thread pool. --- .../deployment/DeploymentManager.java | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/deployment/DeploymentManager.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/deployment/DeploymentManager.java index a25222c4c34d..c095b863800e 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/deployment/DeploymentManager.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/deployment/DeploymentManager.java @@ -64,6 +64,7 @@ import static org.elasticsearch.core.Strings.format; import static org.elasticsearch.xpack.core.ClientHelper.ML_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin; +import static org.elasticsearch.xpack.ml.MachineLearning.UTILITY_THREAD_POOL_NAME; public class DeploymentManager { @@ -242,6 +243,8 @@ private void startAndLoad(ProcessContext processContext, TrainedModelLocation mo try { processContext.startProcess(); processContext.loadModel(modelLocation, ActionListener.wrap(success -> { + assert Thread.currentThread().getName().contains(UTILITY_THREAD_POOL_NAME) + : format("Must execute from [%s] but thread is [%s]", UTILITY_THREAD_POOL_NAME, Thread.currentThread().getName()); processContext.startPriorityProcessWorker(); loadedListener.onResponse(success); }, loadedListener::onFailure)); @@ -401,9 +404,12 @@ class ProcessContext { this.numThreadsPerAllocation = threadSettings.numThreadsPerAllocation(); this.numAllocations = threadSettings.numAllocations(); }); - // We want to use the utility thread pool to load the model and not one of the process - // threads that are dedicated to processing done throughout the lifetime of the process. - this.stateStreamer = new PyTorchStateStreamer(client, executorServiceForDeployment, xContentRegistry); + // We want to use the inference thread pool to load the model as it is a possibly long operation + // and knowing it is an inference thread would enable better understanding during debugging. + // Even though we account for 3 threads per process in the thread pool, loading the model + // happens before we start input/output so it should be ok to use a thread from that pool for loading + // the model. + this.stateStreamer = new PyTorchStateStreamer(client, executorServiceForProcess, xContentRegistry); this.priorityProcessWorker = new PriorityProcessWorkerExecutorService( threadPool.getThreadContext(), "inference process", @@ -458,7 +464,18 @@ private Consumer onProcessCrash() { void loadModel(TrainedModelLocation modelLocation, ActionListener listener) { if (modelLocation instanceof IndexLocation indexLocation) { - process.get().loadModel(task.getModelId(), indexLocation.getIndexName(), stateStreamer, listener); + // Loading the model happens on the inference thread pool but when we get the callback + // we need to return to the utility thread pool to avoid leaking the thread we used. + process.get() + .loadModel( + task.getModelId(), + indexLocation.getIndexName(), + stateStreamer, + ActionListener.wrap( + r -> executorServiceForDeployment.submit(() -> listener.onResponse(r)), + e -> executorServiceForDeployment.submit(() -> listener.onFailure(e)) + ) + ); } else { listener.onFailure( new IllegalStateException("unsupported trained model location [" + modelLocation.getClass().getSimpleName() + "]") From 9063ae0720648b78d237c31a98c62b6d4cbb37b0 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 24 Nov 2022 13:24:38 +0100 Subject: [PATCH 069/919] Mute o.e.p.t.DockerTests.test600Interrupt (#91888) Relates: #91874 --- .../test/java/org/elasticsearch/packaging/test/DockerTests.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java index b83348455f8a..6e3d8baff3fe 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java @@ -13,6 +13,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.http.client.fluent.Request; +import org.apache.lucene.tests.util.LuceneTestCase; import org.elasticsearch.packaging.util.Installation; import org.elasticsearch.packaging.util.Platforms; import org.elasticsearch.packaging.util.ProcessInfo; @@ -1222,6 +1223,7 @@ public void test500Readiness() throws Exception { assertTrue(readinessProbe(9399)); } + @LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/91874") public void test600Interrupt() { waitForElasticsearch(installation, "elastic", PASSWORD); final Result containerLogs = getContainerLogs(); From 29bbf1ade3834eb8369ba02750c862bd65060fbe Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Thu, 24 Nov 2022 12:48:19 +0000 Subject: [PATCH 070/919] Fix HttpTracerTests failure (#91880) Request id can be any number --- .../src/test/java/org/elasticsearch/http/HttpTracerTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/http/HttpTracerTests.java b/server/src/test/java/org/elasticsearch/http/HttpTracerTests.java index aac41e8349f2..d754c3ab0a07 100644 --- a/server/src/test/java/org/elasticsearch/http/HttpTracerTests.java +++ b/server/src/test/java/org/elasticsearch/http/HttpTracerTests.java @@ -41,7 +41,7 @@ public void testLogging() { "request log", httpTracerLog, Level.TRACE, - "\\[1]\\[idHeader]\\[GET]\\[uri] received request from \\[.*] trace.id: 4bf92f3577b34da6a3ce929d0e0e4736" + "\\[\\d+]\\[idHeader]\\[GET]\\[uri] received request from \\[.*] trace.id: 4bf92f3577b34da6a3ce929d0e0e4736" ) ); appender.addExpectation( @@ -49,7 +49,7 @@ public void testLogging() { "response log", httpTracerLog, Level.TRACE, - "\\[1]\\[idHeader]\\[ACCEPTED]\\[text/plain; charset=UTF-8]\\[length] sent response to \\[.*] success \\[true]" + "\\[\\d+]\\[idHeader]\\[ACCEPTED]\\[text/plain; charset=UTF-8]\\[length] sent response to \\[.*] success \\[true]" ) ); From f24b9da9020e7beaf25cd97d3879ae3c5e16555a Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 24 Nov 2022 14:14:22 +0100 Subject: [PATCH 071/919] Revert "Mute o.e.p.t.DockerTests.test600Interrupt (#91888)" (#91892) The previous commit introduced a compilation error that did not show up in CI (because of the test-mute label, I'm assuming). Unblocking main with a revert for now and will open a separate PR to deal with the correct AwaitsFix annotation. Reverts #91888 --- .../test/java/org/elasticsearch/packaging/test/DockerTests.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java index 6e3d8baff3fe..b83348455f8a 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java @@ -13,7 +13,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.http.client.fluent.Request; -import org.apache.lucene.tests.util.LuceneTestCase; import org.elasticsearch.packaging.util.Installation; import org.elasticsearch.packaging.util.Platforms; import org.elasticsearch.packaging.util.ProcessInfo; @@ -1223,7 +1222,6 @@ public void test500Readiness() throws Exception { assertTrue(readinessProbe(9399)); } - @LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/91874") public void test600Interrupt() { waitForElasticsearch(installation, "elastic", PASSWORD); final Result containerLogs = getContainerLogs(); From 5e816686c5d2659a4ebf779dc69e0c5b1f91800b Mon Sep 17 00:00:00 2001 From: Abdon Pijpelink Date: Thu, 24 Nov 2022 14:40:43 +0100 Subject: [PATCH 072/919] Fix typo (#91894) --- docs/reference/features/apis/reset-features-api.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/features/apis/reset-features-api.asciidoc b/docs/reference/features/apis/reset-features-api.asciidoc index 300ce166eaa1..d8ba0832cc2a 100644 --- a/docs/reference/features/apis/reset-features-api.asciidoc +++ b/docs/reference/features/apis/reset-features-api.asciidoc @@ -6,7 +6,7 @@ experimental::[] -Clears all of the the state information stored in system indices by {es} features, including the security and machine learning indices. +Clears all of the state information stored in system indices by {es} features, including the security and machine learning indices. WARNING: Intended for development and testing use only. Do not reset features on a production cluster. From 96a09262abca97c592fc6a07b0a1f4819725f914 Mon Sep 17 00:00:00 2001 From: Przemyslaw Gomulka Date: Thu, 24 Nov 2022 16:03:23 +0100 Subject: [PATCH 073/919] Fix DockerTests for dockerUbi (#91901) kill command was not available. bash -c with kill should be used relates #91704 closes #91874 --- .../test/java/org/elasticsearch/packaging/test/DockerTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java index b83348455f8a..a1b71e217594 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java @@ -1231,7 +1231,7 @@ public void test600Interrupt() { final List infos = ProcessInfo.getProcessInfo(sh, "java"); final int maxPid = infos.stream().map(i -> i.pid()).max(Integer::compareTo).get(); - sh.run("kill -int " + maxPid); // send ctrl+c to all java processes + sh.run("bash -c 'kill -int " + maxPid + "'"); // send ctrl+c to all java processes final Result containerLogsAfter = getContainerLogs(); assertThat("Container logs should contain stopping ...", containerLogsAfter.stdout(), containsString("stopping ...")); From 8ecabb1e0e2def274a89b4f329efcda6431bc7e8 Mon Sep 17 00:00:00 2001 From: amyjtechwriter <61687663+amyjtechwriter@users.noreply.github.com> Date: Thu, 24 Nov 2022 15:33:18 +0000 Subject: [PATCH 074/919] fixed typo (#91909) --- .../authentication/remote-clusters-privileges.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/docs/en/security/authentication/remote-clusters-privileges.asciidoc b/x-pack/docs/en/security/authentication/remote-clusters-privileges.asciidoc index d827ccd89bda..d0899494140a 100644 --- a/x-pack/docs/en/security/authentication/remote-clusters-privileges.asciidoc +++ b/x-pack/docs/en/security/authentication/remote-clusters-privileges.asciidoc @@ -34,7 +34,7 @@ the `read_ccr` cluster privilege, and `monitor` and `read` privileges on the leader index. NOTE: If requests will be issued <>, -then the the authenticating user must have the `run_as` privilege on the remote +then the authenticating user must have the `run_as` privilege on the remote cluster. The following request creates a `remote-replication` role on the remote cluster: @@ -134,7 +134,7 @@ On the remote cluster, the {ccs} role requires the `read` and `read_cross_cluster` privileges for the target indices. NOTE: If requests will be issued <>, -then the the authenticating user must have the `run_as` privilege on the remote +then the authenticating user must have the `run_as` privilege on the remote cluster. The following request creates a `remote-search` role on the remote cluster: From 6a8e433f634162a9d5fc21cb7b28494670fffbd6 Mon Sep 17 00:00:00 2001 From: Artem Prigoda Date: Thu, 24 Nov 2022 16:35:11 +0100 Subject: [PATCH 075/919] Increase timeout for FrozenExistenceDeciderIT#testZeroToOne (#91830) We increased the timeout from 10s to 30s in #90108 it was not enough. There were 5 failures over the 8100 latest builds. I suspect we do need 30 seconds to allow for ILM to finish. --- .../xpack/autoscaling/existence/FrozenExistenceDeciderIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/autoscaling/src/internalClusterTest/java/org/elasticsearch/xpack/autoscaling/existence/FrozenExistenceDeciderIT.java b/x-pack/plugin/autoscaling/src/internalClusterTest/java/org/elasticsearch/xpack/autoscaling/existence/FrozenExistenceDeciderIT.java index b657876323e2..24cf90dcfaa9 100644 --- a/x-pack/plugin/autoscaling/src/internalClusterTest/java/org/elasticsearch/xpack/autoscaling/existence/FrozenExistenceDeciderIT.java +++ b/x-pack/plugin/autoscaling/src/internalClusterTest/java/org/elasticsearch/xpack/autoscaling/existence/FrozenExistenceDeciderIT.java @@ -131,7 +131,7 @@ public void testZeroToOne() throws Exception { String[] indices = indices(); assertThat(indices, arrayContaining(PARTIAL_INDEX_NAME)); assertThat(indices, not(arrayContaining(INDEX_NAME))); - }, 30, TimeUnit.SECONDS); + }, 60, TimeUnit.SECONDS); ensureGreen(); } From 84326b1b82ed2e03c0e3812436a3fbf9b6bdd571 Mon Sep 17 00:00:00 2001 From: Abdon Pijpelink Date: Thu, 24 Nov 2022 16:35:44 +0100 Subject: [PATCH 076/919] Fixes typo in knn search page (#91898) --- docs/reference/search/search-your-data/knn-search.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/search/search-your-data/knn-search.asciidoc b/docs/reference/search/search-your-data/knn-search.asciidoc index 6789ac4b9954..cf6264d9b9ac 100644 --- a/docs/reference/search/search-your-data/knn-search.asciidoc +++ b/docs/reference/search/search-your-data/knn-search.asciidoc @@ -415,7 +415,7 @@ segment as an https://arxiv.org/abs/1603.09320[HNSW graph]. Indexing vectors for approximate kNN search can take substantial time because of how expensive it is to build these graphs. You may need to increase the client request timeout for index and bulk requests. The <> -contains important guidance around indexing performance, and how the the index +contains important guidance around indexing performance, and how the index configuration can affect search performance. In addition to its search-time tuning parameters, the HNSW algorithm has From 9e045401df0faf01db90f3e2bb6f8525e535f40b Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Thu, 24 Nov 2022 17:42:53 +0200 Subject: [PATCH 077/919] mute snapshot based recovery (#91847) --- .../java/org/elasticsearch/upgrades/SnapshotBasedRecoveryIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/SnapshotBasedRecoveryIT.java b/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/SnapshotBasedRecoveryIT.java index 5997220f10b8..c6437b505e08 100644 --- a/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/SnapshotBasedRecoveryIT.java +++ b/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/SnapshotBasedRecoveryIT.java @@ -40,6 +40,7 @@ import static org.hamcrest.Matchers.notNullValue; public class SnapshotBasedRecoveryIT extends AbstractRollingTestCase { + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/91383") public void testSnapshotBasedRecovery() throws Exception { final String indexName = "snapshot_based_recovery"; final String repositoryName = "snapshot_based_recovery_repo"; From 23eafaa111632cdab3b1c831a6260931dbbb47df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Witek?= Date: Thu, 24 Nov 2022 17:02:52 +0100 Subject: [PATCH 078/919] [ML] Skip remote clusters when performing up front privileges validation (#91895) --- docs/changelog/91895.yaml | 6 + .../build.gradle | 63 ++++++++++ .../MultiClusterYamlTestSuiteIT.java | 46 +++++++ .../test/multi_cluster/30_jobs.yml | 118 ++++++++++++++++++ .../test/remote_cluster/30_jobs.yml | 97 ++++++++++++++ .../TransportPutDataFrameAnalyticsAction.java | 25 +++- .../xpack/ml/datafeed/DatafeedManager.java | 16 ++- 7 files changed, 363 insertions(+), 8 deletions(-) create mode 100644 docs/changelog/91895.yaml create mode 100644 x-pack/plugin/ml/qa/multi-cluster-tests-with-security/build.gradle create mode 100644 x-pack/plugin/ml/qa/multi-cluster-tests-with-security/src/test/java/org/elasticsearch/multi_cluster/MultiClusterYamlTestSuiteIT.java create mode 100644 x-pack/plugin/ml/qa/multi-cluster-tests-with-security/src/test/resources/rest-api-spec/test/multi_cluster/30_jobs.yml create mode 100644 x-pack/plugin/ml/qa/multi-cluster-tests-with-security/src/test/resources/rest-api-spec/test/remote_cluster/30_jobs.yml diff --git a/docs/changelog/91895.yaml b/docs/changelog/91895.yaml new file mode 100644 index 000000000000..66f876ee52f7 --- /dev/null +++ b/docs/changelog/91895.yaml @@ -0,0 +1,6 @@ +pr: 91895 +summary: Skip remote clusters when performing up front privileges validation for datafeeds +area: Machine Learning +type: bug +issues: + - 87832 diff --git a/x-pack/plugin/ml/qa/multi-cluster-tests-with-security/build.gradle b/x-pack/plugin/ml/qa/multi-cluster-tests-with-security/build.gradle new file mode 100644 index 000000000000..368aab4615fc --- /dev/null +++ b/x-pack/plugin/ml/qa/multi-cluster-tests-with-security/build.gradle @@ -0,0 +1,63 @@ +import org.elasticsearch.gradle.internal.test.RestIntegTestTask +import org.elasticsearch.gradle.Version +import org.elasticsearch.gradle.VersionProperties + +apply plugin: 'elasticsearch.internal-testclusters' +apply plugin: 'elasticsearch.standalone-rest-test' +apply plugin: 'elasticsearch.rest-resources' + +dependencies { + testImplementation project(':x-pack:qa') +} + +Version ccsCompatVersion = new Version(VersionProperties.getElasticsearchVersion().getMajor(), VersionProperties.getElasticsearchVersion().getMinor() - 1, 0) + +restResources { + restApi { + include '_common', 'bulk', 'indices', 'cluster', 'search', 'security', 'ml' + } +} + +def remoteCluster = testClusters.register('remote-cluster') { + testDistribution = 'DEFAULT' + versions = [ccsCompatVersion.toString(), project.version] + numberOfNodes = 2 + setting 'node.roles', '[data,ingest,master]' + setting 'xpack.security.enabled', 'true' + setting 'xpack.watcher.enabled', 'false' + setting 'xpack.license.self_generated.type', 'trial' + + user username: "test_user", password: "x-pack-test-password" +} + +testClusters.register('mixed-cluster') { + testDistribution = 'DEFAULT' + numberOfNodes = 2 + setting 'node.roles', '[data,ingest,master]' + setting 'xpack.security.enabled', 'true' + setting 'xpack.watcher.enabled', 'false' + setting 'xpack.license.self_generated.type', 'trial' + setting 'cluster.remote.my_remote_cluster.seeds', { + remoteCluster.get().getAllTransportPortURI().collect { "\"$it\"" }.toString() + } + setting 'cluster.remote.connections_per_cluster', "1" + + user username: "test_user", password: "x-pack-test-password" +} + +tasks.register('remote-cluster', RestIntegTestTask) { + mustRunAfter("precommit") + systemProperty 'tests.rest.suite', 'remote_cluster' +} + +tasks.register('mixed-cluster', RestIntegTestTask) { + dependsOn 'remote-cluster' + useCluster remoteCluster + systemProperty 'tests.rest.suite', 'multi_cluster' +} + +tasks.register("integTest") { + dependsOn 'mixed-cluster' +} + +tasks.named("check").configure { dependsOn("integTest") } diff --git a/x-pack/plugin/ml/qa/multi-cluster-tests-with-security/src/test/java/org/elasticsearch/multi_cluster/MultiClusterYamlTestSuiteIT.java b/x-pack/plugin/ml/qa/multi-cluster-tests-with-security/src/test/java/org/elasticsearch/multi_cluster/MultiClusterYamlTestSuiteIT.java new file mode 100644 index 000000000000..0fbd3d463f7f --- /dev/null +++ b/x-pack/plugin/ml/qa/multi-cluster-tests-with-security/src/test/java/org/elasticsearch/multi_cluster/MultiClusterYamlTestSuiteIT.java @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.multi_cluster; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; +import com.carrotsearch.randomizedtesting.annotations.TimeoutSuite; + +import org.apache.lucene.tests.util.TimeUnits; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate; +import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase; + +@TimeoutSuite(millis = 5 * TimeUnits.MINUTE) // to account for slow as hell VMs +public class MultiClusterYamlTestSuiteIT extends ESClientYamlSuiteTestCase { + + private static final String USER = "test_user"; + private static final String PASS = "x-pack-test-password"; + + @Override + protected boolean preserveIndicesUponCompletion() { + return true; + } + + public MultiClusterYamlTestSuiteIT(@Name("yaml") ClientYamlTestCandidate testCandidate) { + super(testCandidate); + } + + @ParametersFactory + public static Iterable parameters() throws Exception { + return createParameters(); + } + + @Override + protected Settings restClientSettings() { + String token = basicAuthHeaderValue(USER, new SecureString(PASS.toCharArray())); + return Settings.builder().put(super.restClientSettings()).put(ThreadContext.PREFIX + ".Authorization", token).build(); + } +} diff --git a/x-pack/plugin/ml/qa/multi-cluster-tests-with-security/src/test/resources/rest-api-spec/test/multi_cluster/30_jobs.yml b/x-pack/plugin/ml/qa/multi-cluster-tests-with-security/src/test/resources/rest-api-spec/test/multi_cluster/30_jobs.yml new file mode 100644 index 000000000000..540fed64ec53 --- /dev/null +++ b/x-pack/plugin/ml/qa/multi-cluster-tests-with-security/src/test/resources/rest-api-spec/test/multi_cluster/30_jobs.yml @@ -0,0 +1,118 @@ +--- +setup: + - skip: + features: headers + + - do: + cluster.health: + wait_for_status: yellow + + - do: + security.put_user: + username: "joe" + body: > + { + "password": "joe-password", + "roles" : [ "x_cluster_role" ] + } + - do: + security.put_role: + name: "x_cluster_role" + body: > + { + "cluster": ["manage_ml"], + "indices": [ + { + "names": ["test_index"], + "privileges": ["read", "view_index_metadata"] + } + ] + } +--- +teardown: + - do: + security.delete_user: + username: "joe" + ignore: 404 + +--- +"Test ML job with local indices only": + - do: + catch: /Cannot create datafeed \[job-with-local-indices\] because user joe lacks permissions on the indices/ + headers: { Authorization: "Basic am9lOmpvZS1wYXNzd29yZA==" } # This is joe + ml.put_job: + job_id: job-with-local-indices + body: > + { + "description":"Analysis of response time by airline", + "analysis_config" : { + "detectors" :[{"function":"count"}] + }, + "analysis_limits" : { + "model_memory_limit": "20mb" + }, + "data_description" : { + "format":"xcontent" + }, + "datafeed_config": { + "indexes":["test_index", "test_index-2"] + } + } + +--- +"Test ML job with local and remote index": + - do: + headers: { Authorization: "Basic am9lOmpvZS1wYXNzd29yZA==" } # This is joe + ml.put_job: + job_id: job-with-local-and-remote-index + body: > + { + "description":"Analysis of response time by airline", + "analysis_config" : { + "detectors" :[{"function":"count"}] + }, + "analysis_limits" : { + "model_memory_limit": "20mb" + }, + "data_description" : { + "format":"xcontent" + }, + "datafeed_config": { + "indexes":["test_index", "my_remote_cluster:remote_test_index"] + } + } + - match: { job_id: "job-with-local-and-remote-index" } + - is_true: datafeed_config + - match: { datafeed_config.job_id: "job-with-local-and-remote-index" } + - match: { datafeed_config.datafeed_id: "job-with-local-and-remote-index" } + - is_true: datafeed_config.authorization.roles + - is_true: create_time + +--- +"Test ML job with remote index only": + - do: + headers: { Authorization: "Basic am9lOmpvZS1wYXNzd29yZA==" } # This is joe + ml.put_job: + job_id: job-with-remote-index-only + body: > + { + "description":"Analysis of response time by airline", + "analysis_config" : { + "detectors" :[{"function":"count"}] + }, + "analysis_limits" : { + "model_memory_limit": "20mb" + }, + "data_description" : { + "format":"xcontent" + }, + "datafeed_config": { + "indexes":["my_remote_cluster:remote_test_index"] + } + } + - match: { job_id: "job-with-remote-index-only" } + - is_true: datafeed_config + - match: { datafeed_config.job_id: "job-with-remote-index-only" } + - match: { datafeed_config.datafeed_id: "job-with-remote-index-only" } + - is_true: datafeed_config.authorization.roles + - is_true: create_time diff --git a/x-pack/plugin/ml/qa/multi-cluster-tests-with-security/src/test/resources/rest-api-spec/test/remote_cluster/30_jobs.yml b/x-pack/plugin/ml/qa/multi-cluster-tests-with-security/src/test/resources/rest-api-spec/test/remote_cluster/30_jobs.yml new file mode 100644 index 000000000000..2e4e7866fb07 --- /dev/null +++ b/x-pack/plugin/ml/qa/multi-cluster-tests-with-security/src/test/resources/rest-api-spec/test/remote_cluster/30_jobs.yml @@ -0,0 +1,97 @@ +--- +setup: + - skip: + features: headers + + - do: + cluster.health: + wait_for_status: yellow + - do: + security.put_user: + username: "joe" + body: > + { + "password": "joe-password", + "roles" : [ "x_cluster_role" ] + } + - do: + security.put_role: + name: "x_cluster_role" + body: > + { + "cluster": [], + "indices": [ + { + "names": ["remote_test_index"], + "privileges": ["read", "view_index_metadata"] + } + ] + } +--- +teardown: + - do: + security.delete_user: + username: "joe" + ignore: 404 + +--- +"Index data on the remote cluster": + - do: + indices.create: + index: remote_test_index + body: + settings: + index: + number_of_shards: 3 + number_of_replicas: 0 + aliases: + test_alias: {} + mappings: + properties: + time: + type: date + user: + type: keyword + stars: + type: integer + coolness: + type: integer + + - do: + bulk: + refresh: true + body: + - '{"index": {"_index": "remote_test_index"}}' + - '{"user": "a", "stars": 1, "date" : "2018-10-29T12:12:12.123456789Z"}' + - '{"index": {"_index": "remote_test_index"}}' + - '{"user": "a", "stars": 4, "date" : "2018-10-29T12:14:12.123456789Z"}' + - '{"index": {"_index": "remote_test_index"}}' + - '{"user": "a", "stars": 5, "date" : "2018-10-29T12:16:12.123456789Z"}' + - '{"index": {"_index": "remote_test_index"}}' + - '{"user": "b", "stars": 2, "date" : "2018-10-29T12:17:12.123456789Z"}' + - '{"index": {"_index": "remote_test_index"}}' + - '{"user": "b", "stars": 3, "date" : "2018-10-29T12:22:12.123456789Z"}' + - '{"index": {"_index": "remote_test_index"}}' + - '{"user": "a", "stars": 5, "date" : "2018-10-29T12:23:12.123456789Z"}' + - '{"index": {"_index": "remote_test_index"}}' + - '{"user": "b", "stars": 1, "date" : "2018-10-29T12:32:12.123456789Z"}' + - '{"index": {"_index": "remote_test_index"}}' + - '{"user": "a", "stars": 3, "date" : "2018-10-29T12:34:12.123456789Z"}' + - '{"index": {"_index": "remote_test_index"}}' + - '{"user": "c", "stars": 4, "date" : "2018-10-29T12:35:12.123456789Z"}' + - do: + headers: { Authorization: "Basic am9lOmpvZS1wYXNzd29yZA==" } # This is joe + search: + rest_total_hits_as_int: true + index: remote_test_index + body: + aggs: + user: + terms: + field: user + + - match: { _shards.total: 3 } + - match: { hits.total: 9 } + - length: { aggregations.user.buckets: 3 } + - match: { aggregations.user.buckets.0.key: "a" } + - match: { aggregations.user.buckets.0.doc_count: 5 } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDataFrameAnalyticsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDataFrameAnalyticsAction.java index 802be705e7ea..a4982addd1d0 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDataFrameAnalyticsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDataFrameAnalyticsAction.java @@ -25,6 +25,7 @@ import org.elasticsearch.core.TimeValue; import org.elasticsearch.license.License; import org.elasticsearch.license.LicenseUtils; +import org.elasticsearch.license.RemoteClusterLicenseChecker; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; @@ -54,10 +55,12 @@ import java.io.IOException; import java.time.Instant; +import java.util.Arrays; import java.util.Map; import java.util.Objects; import java.util.function.Supplier; +import static java.util.function.Predicate.not; import static org.elasticsearch.xpack.ml.utils.SecondaryAuthorizationUtils.useSecondaryAuthIfAvailable; public class TransportPutDataFrameAnalyticsAction extends TransportMasterNodeAction< @@ -160,10 +163,12 @@ private void putValidatedConfig( if (securityContext != null) { useSecondaryAuthIfAvailable(securityContext, () -> { final String username = securityContext.getUser().principal(); - RoleDescriptor.IndicesPrivileges sourceIndexPrivileges = RoleDescriptor.IndicesPrivileges.builder() - .indices(preparedForPutConfig.getSource().getIndex()) - .privileges("read") - .build(); + // DFA doesn't support CCS, but if it did it would need this filter, so it's safest to have the filter + // in place even though it's a no-op. + // TODO: Remove this filter once https://github.com/elastic/elasticsearch/issues/67798 is fixed. + final String[] sourceIndices = Arrays.stream(preparedForPutConfig.getSource().getIndex()) + .filter(not(RemoteClusterLicenseChecker::isRemoteIndex)) + .toArray(String[]::new); RoleDescriptor.IndicesPrivileges destIndexPrivileges = RoleDescriptor.IndicesPrivileges.builder() .indices(preparedForPutConfig.getDest().getIndex()) .privileges("read", "index", "create_index") @@ -173,7 +178,17 @@ private void putValidatedConfig( privRequest.applicationPrivileges(new RoleDescriptor.ApplicationResourcePrivileges[0]); privRequest.username(username); privRequest.clusterPrivileges(Strings.EMPTY_ARRAY); - privRequest.indexPrivileges(sourceIndexPrivileges, destIndexPrivileges); + RoleDescriptor.IndicesPrivileges[] indicesPrivileges; + if (sourceIndices.length > 0) { + RoleDescriptor.IndicesPrivileges sourceIndexPrivileges = RoleDescriptor.IndicesPrivileges.builder() + .indices(sourceIndices) + .privileges("read") + .build(); + indicesPrivileges = new RoleDescriptor.IndicesPrivileges[] { sourceIndexPrivileges, destIndexPrivileges }; + } else { + indicesPrivileges = new RoleDescriptor.IndicesPrivileges[] { destIndexPrivileges }; + } + privRequest.indexPrivileges(indicesPrivileges); ActionListener privResponseListener = ActionListener.wrap( r -> handlePrivsResponse(username, preparedForPutConfig, r, masterNodeTimeout, listener), diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedManager.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedManager.java index 4c6b9a78e306..7fcf7cf98024 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedManager.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedManager.java @@ -59,6 +59,7 @@ import java.util.Set; import java.util.stream.Collectors; +import static java.util.function.Predicate.not; import static org.elasticsearch.xpack.core.ClientHelper.ML_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin; import static org.elasticsearch.xpack.ml.utils.SecondaryAuthorizationUtils.useSecondaryAuthIfAvailable; @@ -106,7 +107,12 @@ public void putDatafeed( ) { if (XPackSettings.SECURITY_ENABLED.get(settings)) { useSecondaryAuthIfAvailable(securityContext, () -> { - final String[] indices = request.getDatafeed().getIndices().toArray(new String[0]); + // TODO: Remove this filter once https://github.com/elastic/elasticsearch/issues/67798 is fixed. + final String[] indices = request.getDatafeed() + .getIndices() + .stream() + .filter(not(RemoteClusterLicenseChecker::isRemoteIndex)) + .toArray(String[]::new); final String username = securityContext.getUser().principal(); final HasPrivilegesRequest privRequest = new HasPrivilegesRequest(); @@ -128,8 +134,12 @@ public void putDatafeed( } else { indicesPrivilegesBuilder.privileges(SearchAction.NAME, RollupSearchAction.NAME); } - privRequest.indexPrivileges(indicesPrivilegesBuilder.build()); - client.execute(HasPrivilegesAction.INSTANCE, privRequest, privResponseListener); + if (indices.length == 0) { + privResponseListener.onResponse(new HasPrivilegesResponse()); + } else { + privRequest.indexPrivileges(indicesPrivilegesBuilder.build()); + client.execute(HasPrivilegesAction.INSTANCE, privRequest, privResponseListener); + } }, e -> { if (ExceptionsHelper.unwrapCause(e) instanceof IndexNotFoundException) { indicesPrivilegesBuilder.privileges(SearchAction.NAME); From 671fe3faac6232679c5e19dda185a7ff62395aba Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 24 Nov 2022 16:21:51 +0000 Subject: [PATCH 079/919] Clarify index recovery docs (#91861) Mentions that we only report on recoveries for shard copies that actually exist in the cluster, so you don't see all historical data if, e.g., the shard copy relocates elsewhere. --- docs/reference/indices/recovery.asciidoc | 27 +++++++++++++++--------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/reference/indices/recovery.asciidoc b/docs/reference/indices/recovery.asciidoc index 4a61155175d4..81b3aa13580c 100644 --- a/docs/reference/indices/recovery.asciidoc +++ b/docs/reference/indices/recovery.asciidoc @@ -4,10 +4,9 @@ Index recovery ++++ - -Returns information about ongoing and completed shard recoveries for one or more -indices. For data streams, the API returns information for the stream's backing -indices. +Returns information about ongoing and completed shard recoveries for one or +more indices. For data streams, the API returns information for the stream's +backing indices. [source,console] ---- @@ -33,14 +32,14 @@ index, or alias. [[index-recovery-api-desc]] ==== {api-description-title} -Use the index recovery API -to get information about ongoing and completed shard recoveries. +Use the index recovery API to get information about ongoing and completed shard +recoveries. // tag::shard-recovery-desc[] -Shard recovery is the process -of syncing a replica shard from a primary shard. -Upon completion, -the replica shard is available for search. +Shard recovery is the process of initializing a shard copy, such as restoring a +primary shard from a snapshot or syncing a replica shard from a primary shard. +When a shard recovery completes, the recovered shard is available for search +and indexing. Recovery automatically occurs during the following processes: @@ -52,6 +51,14 @@ Recovery automatically occurs during the following processes: <> operation. // end::shard-recovery-desc[] +The index recovery API reports information about completed recoveries only for +shard copies that currently exist in the cluster. It only reports the last +recovery for each shard copy and does not report historical information about +earlier recoveries, nor does it report information about the recoveries of +shard copies that no longer exist. This means that if a shard copy completes a +recovery and then {es} relocates it onto a different node then the information +about the original recovery will not be shown in the recovery API. + [[index-recovery-api-path-params]] ==== {api-path-parms-title} From c66ac71f9e5100787743b0e26747ab557dbe9458 Mon Sep 17 00:00:00 2001 From: amyjtechwriter <61687663+amyjtechwriter@users.noreply.github.com> Date: Thu, 24 Nov 2022 17:30:37 +0000 Subject: [PATCH 080/919] typo fixed (#91928) --- docs/reference/commands/reconfigure-node.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/commands/reconfigure-node.asciidoc b/docs/reference/commands/reconfigure-node.asciidoc index f06aee9f94db..1ffd7b16f011 100644 --- a/docs/reference/commands/reconfigure-node.asciidoc +++ b/docs/reference/commands/reconfigure-node.asciidoc @@ -28,7 +28,7 @@ cluster where security features are already enabled and configured. Before starting your new node, run the <> tool with the `-s node` option to generate an enrollment token on any node in your -existing cluster. On your new node, run the the +existing cluster. On your new node, run the `elasticsearch-reconfigure-node` tool and pass the enrollment token as a parameter. From 7dbc1ea36e258ed718db3adc94c2b8a77aa9d78f Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Thu, 24 Nov 2022 18:43:49 +0100 Subject: [PATCH 081/919] Use chunked encoding for indices stats response (#91760) These responses can become huge, lets chunk them by index. --- .../segments/IndicesSegmentResponse.java | 126 +++++++++--------- .../indices/stats/IndicesStatsResponse.java | 57 ++++---- .../broadcast/ChunkedBroadcastResponse.java | 45 +++++++ .../admin/indices/RestIndicesStatsAction.java | 4 +- .../segments/IndicesSegmentResponseTests.java | 2 +- .../stats/IndicesStatsResponseTests.java | 48 ++++++- 6 files changed, 188 insertions(+), 94 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/action/support/broadcast/ChunkedBroadcastResponse.java diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/segments/IndicesSegmentResponse.java b/server/src/main/java/org/elasticsearch/action/admin/indices/segments/IndicesSegmentResponse.java index c0a7db846043..5377a5af883f 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/segments/IndicesSegmentResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/segments/IndicesSegmentResponse.java @@ -13,15 +13,13 @@ import org.apache.lucene.search.SortedNumericSortField; import org.apache.lucene.search.SortedSetSortField; import org.elasticsearch.action.support.DefaultShardOperationFailedException; -import org.elasticsearch.action.support.broadcast.BaseBroadcastResponse; +import org.elasticsearch.action.support.broadcast.ChunkedBroadcastResponse; import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.unit.ByteSizeValue; -import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.core.RestApiVersion; import org.elasticsearch.index.engine.Segment; -import org.elasticsearch.rest.action.RestActions; import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentBuilder; @@ -33,7 +31,7 @@ import java.util.Locale; import java.util.Map; -public class IndicesSegmentResponse extends BaseBroadcastResponse implements ChunkedToXContent { +public class IndicesSegmentResponse extends ChunkedBroadcastResponse { private final ShardSegments[] shards; @@ -79,72 +77,72 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public Iterator toXContentChunked(ToXContent.Params outerParams) { - return Iterators.concat(Iterators.single(((builder, params) -> { - builder.startObject(); - RestActions.buildBroadcastShardsHeader(builder, params, this); - return builder.startObject(Fields.INDICES); - })), getIndices().values().stream().map(indexSegments -> (ToXContent) (builder, params) -> { - builder.startObject(indexSegments.getIndex()); - - builder.startObject(Fields.SHARDS); - for (IndexShardSegments indexSegment : indexSegments) { - builder.startArray(Integer.toString(indexSegment.shardId().id())); - for (ShardSegments shardSegments : indexSegment) { - builder.startObject(); - - builder.startObject(Fields.ROUTING); - builder.field(Fields.STATE, shardSegments.getShardRouting().state()); - builder.field(Fields.PRIMARY, shardSegments.getShardRouting().primary()); - builder.field(Fields.NODE, shardSegments.getShardRouting().currentNodeId()); - if (shardSegments.getShardRouting().relocatingNodeId() != null) { - builder.field(Fields.RELOCATING_NODE, shardSegments.getShardRouting().relocatingNodeId()); - } - builder.endObject(); - - builder.field(Fields.NUM_COMMITTED_SEGMENTS, shardSegments.getNumberOfCommitted()); - builder.field(Fields.NUM_SEARCH_SEGMENTS, shardSegments.getNumberOfSearch()); - - builder.startObject(Fields.SEGMENTS); - for (Segment segment : shardSegments) { - builder.startObject(segment.getName()); - builder.field(Fields.GENERATION, segment.getGeneration()); - builder.field(Fields.NUM_DOCS, segment.getNumDocs()); - builder.field(Fields.DELETED_DOCS, segment.getDeletedDocs()); - builder.humanReadableField(Fields.SIZE_IN_BYTES, Fields.SIZE, segment.getSize()); - if (builder.getRestApiVersion() == RestApiVersion.V_7) { - builder.humanReadableField(Fields.MEMORY_IN_BYTES, Fields.MEMORY, ByteSizeValue.ZERO); - } - builder.field(Fields.COMMITTED, segment.isCommitted()); - builder.field(Fields.SEARCH, segment.isSearch()); - if (segment.getVersion() != null) { - builder.field(Fields.VERSION, segment.getVersion()); - } - if (segment.isCompound() != null) { - builder.field(Fields.COMPOUND, segment.isCompound()); + protected Iterator customXContentChunks(ToXContent.Params params) { + return Iterators.concat( + Iterators.single((builder, p) -> builder.startObject(Fields.INDICES)), + getIndices().values().stream().map(indexSegments -> (ToXContent) (builder, p) -> { + builder.startObject(indexSegments.getIndex()); + + builder.startObject(Fields.SHARDS); + for (IndexShardSegments indexSegment : indexSegments) { + builder.startArray(Integer.toString(indexSegment.shardId().id())); + for (ShardSegments shardSegments : indexSegment) { + builder.startObject(); + + builder.startObject(Fields.ROUTING); + builder.field(Fields.STATE, shardSegments.getShardRouting().state()); + builder.field(Fields.PRIMARY, shardSegments.getShardRouting().primary()); + builder.field(Fields.NODE, shardSegments.getShardRouting().currentNodeId()); + if (shardSegments.getShardRouting().relocatingNodeId() != null) { + builder.field(Fields.RELOCATING_NODE, shardSegments.getShardRouting().relocatingNodeId()); } - if (segment.getMergeId() != null) { - builder.field(Fields.MERGE_ID, segment.getMergeId()); - } - if (segment.getSegmentSort() != null) { - toXContent(builder, segment.getSegmentSort()); - } - if (segment.attributes != null && segment.attributes.isEmpty() == false) { - builder.field("attributes", segment.attributes); + builder.endObject(); + + builder.field(Fields.NUM_COMMITTED_SEGMENTS, shardSegments.getNumberOfCommitted()); + builder.field(Fields.NUM_SEARCH_SEGMENTS, shardSegments.getNumberOfSearch()); + + builder.startObject(Fields.SEGMENTS); + for (Segment segment : shardSegments) { + builder.startObject(segment.getName()); + builder.field(Fields.GENERATION, segment.getGeneration()); + builder.field(Fields.NUM_DOCS, segment.getNumDocs()); + builder.field(Fields.DELETED_DOCS, segment.getDeletedDocs()); + builder.humanReadableField(Fields.SIZE_IN_BYTES, Fields.SIZE, segment.getSize()); + if (builder.getRestApiVersion() == RestApiVersion.V_7) { + builder.humanReadableField(Fields.MEMORY_IN_BYTES, Fields.MEMORY, ByteSizeValue.ZERO); + } + builder.field(Fields.COMMITTED, segment.isCommitted()); + builder.field(Fields.SEARCH, segment.isSearch()); + if (segment.getVersion() != null) { + builder.field(Fields.VERSION, segment.getVersion()); + } + if (segment.isCompound() != null) { + builder.field(Fields.COMPOUND, segment.isCompound()); + } + if (segment.getMergeId() != null) { + builder.field(Fields.MERGE_ID, segment.getMergeId()); + } + if (segment.getSegmentSort() != null) { + toXContent(builder, segment.getSegmentSort()); + } + if (segment.attributes != null && segment.attributes.isEmpty() == false) { + builder.field("attributes", segment.attributes); + } + builder.endObject(); } builder.endObject(); - } - builder.endObject(); - builder.endObject(); + builder.endObject(); + } + builder.endArray(); } - builder.endArray(); - } - builder.endObject(); + builder.endObject(); - builder.endObject(); - return builder; - }).iterator(), Iterators.single((builder, params) -> builder.endObject().endObject())); + builder.endObject(); + return builder; + }).iterator(), + Iterators.single((builder, p) -> builder.endObject()) + ); } private static void toXContent(XContentBuilder builder, Sort sort) throws IOException { diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsResponse.java b/server/src/main/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsResponse.java index 25c804a340a7..85c28d57820b 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsResponse.java @@ -11,20 +11,23 @@ import org.elasticsearch.Version; import org.elasticsearch.action.admin.indices.stats.IndexStats.IndexStatsBuilder; import org.elasticsearch.action.support.DefaultShardOperationFailedException; -import org.elasticsearch.action.support.broadcast.BroadcastResponse; +import org.elasticsearch.action.support.broadcast.ChunkedBroadcastResponse; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.health.ClusterIndexHealth; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.routing.ShardRouting; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.index.Index; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -33,7 +36,7 @@ import static java.util.Collections.unmodifiableMap; -public class IndicesStatsResponse extends BroadcastResponse { +public class IndicesStatsResponse extends ChunkedBroadcastResponse { private final Map indexHealthMap; @@ -171,7 +174,7 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - protected void addCustomXContentFields(XContentBuilder builder, Params params) throws IOException { + protected Iterator customXContentChunks(ToXContent.Params params) { final String level = params.param("level", "indices"); final boolean isLevelValid = "cluster".equalsIgnoreCase(level) || "indices".equalsIgnoreCase(level) @@ -179,22 +182,11 @@ protected void addCustomXContentFields(XContentBuilder builder, Params params) t if (isLevelValid == false) { throw new IllegalArgumentException("level parameter must be one of [cluster] or [indices] or [shards] but was [" + level + "]"); } - - builder.startObject("_all"); - - builder.startObject("primaries"); - getPrimaries().toXContent(builder, params); - builder.endObject(); - - builder.startObject("total"); - getTotal().toXContent(builder, params); - builder.endObject(); - - builder.endObject(); - if ("indices".equalsIgnoreCase(level) || "shards".equalsIgnoreCase(level)) { - builder.startObject(Fields.INDICES); - for (IndexStats indexStats : getIndices().values()) { + return Iterators.concat(Iterators.single(((builder, p) -> { + commonStats(builder, p); + return builder.startObject(Fields.INDICES); + })), getIndices().values().stream().map(indexStats -> (builder, p) -> { builder.startObject(indexStats.getIndex()); builder.field("uuid", indexStats.getUuid()); if (indexStats.getHealth() != null) { @@ -204,11 +196,11 @@ protected void addCustomXContentFields(XContentBuilder builder, Params params) t builder.field("status", indexStats.getState().toString().toLowerCase(Locale.ROOT)); } builder.startObject("primaries"); - indexStats.getPrimaries().toXContent(builder, params); + indexStats.getPrimaries().toXContent(builder, p); builder.endObject(); builder.startObject("total"); - indexStats.getTotal().toXContent(builder, params); + indexStats.getTotal().toXContent(builder, p); builder.endObject(); if ("shards".equalsIgnoreCase(level)) { @@ -217,17 +209,34 @@ protected void addCustomXContentFields(XContentBuilder builder, Params params) t builder.startArray(Integer.toString(indexShardStats.getShardId().id())); for (ShardStats shardStats : indexShardStats) { builder.startObject(); - shardStats.toXContent(builder, params); + shardStats.toXContent(builder, p); builder.endObject(); } builder.endArray(); } builder.endObject(); } - builder.endObject(); - } - builder.endObject(); + return builder.endObject(); + }).iterator(), Iterators.single((b, p) -> b.endObject())); } + return Iterators.single((b, p) -> { + commonStats(b, p); + return b; + }); + } + + private void commonStats(XContentBuilder builder, ToXContent.Params p) throws IOException { + builder.startObject("_all"); + + builder.startObject("primaries"); + getPrimaries().toXContent(builder, p); + builder.endObject(); + + builder.startObject("total"); + getTotal().toXContent(builder, p); + builder.endObject(); + + builder.endObject(); } static final class Fields { diff --git a/server/src/main/java/org/elasticsearch/action/support/broadcast/ChunkedBroadcastResponse.java b/server/src/main/java/org/elasticsearch/action/support/broadcast/ChunkedBroadcastResponse.java new file mode 100644 index 000000000000..d65879578b99 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/action/support/broadcast/ChunkedBroadcastResponse.java @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +package org.elasticsearch.action.support.broadcast; + +import org.elasticsearch.action.support.DefaultShardOperationFailedException; +import org.elasticsearch.common.collect.Iterators; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.ChunkedToXContent; +import org.elasticsearch.rest.action.RestActions; +import org.elasticsearch.xcontent.ToXContent; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +public abstract class ChunkedBroadcastResponse extends BaseBroadcastResponse implements ChunkedToXContent { + public ChunkedBroadcastResponse(StreamInput in) throws IOException { + super(in); + } + + public ChunkedBroadcastResponse( + int totalShards, + int successfulShards, + int failedShards, + List shardFailures + ) { + super(totalShards, successfulShards, failedShards, shardFailures); + } + + @Override + public final Iterator toXContentChunked(ToXContent.Params params) { + return Iterators.concat(Iterators.single((b, p) -> { + b.startObject(); + RestActions.buildBroadcastShardsHeader(b, p, this); + return b; + }), customXContentChunks(params), Iterators.single((builder, p) -> builder.endObject())); + } + + protected abstract Iterator customXContentChunks(ToXContent.Params params); +} diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestIndicesStatsAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestIndicesStatsAction.java index 737c1ccc4cba..74924e4bfaf7 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestIndicesStatsAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestIndicesStatsAction.java @@ -19,7 +19,7 @@ import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.action.RestCancellableNodeClient; -import org.elasticsearch.rest.action.RestToXContentListener; +import org.elasticsearch.rest.action.RestChunkedToXContentListener; import org.elasticsearch.rest.action.document.RestMultiTermVectorsAction; import java.io.IOException; @@ -140,7 +140,7 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC return channel -> new RestCancellableNodeClient(client, request.getHttpChannel()).admin() .indices() - .stats(indicesStatsRequest, new RestToXContentListener<>(channel)); + .stats(indicesStatsRequest, new RestChunkedToXContentListener<>(channel)); } @Override diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/segments/IndicesSegmentResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/segments/IndicesSegmentResponseTests.java index 2c211a66c7b2..012b24330ed3 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/segments/IndicesSegmentResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/segments/IndicesSegmentResponseTests.java @@ -74,6 +74,6 @@ public void testSerializesOneChunkPerIndex() { iterator.next(); chunks++; } - assertEquals(indices + 2, chunks); + assertEquals(indices + 4, chunks); } } diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsResponseTests.java index e1873b7714bc..cec44f8fd063 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsResponseTests.java @@ -13,13 +13,17 @@ import org.elasticsearch.cluster.routing.ShardRoutingState; import org.elasticsearch.cluster.routing.TestShardRouting; import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.io.Streams; import org.elasticsearch.index.Index; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.shard.ShardPath; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xcontent.json.JsonXContent; +import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; @@ -41,7 +45,7 @@ public void testInvalidLevel() { final ToXContent.Params params = new ToXContent.MapParams(Collections.singletonMap("level", level)); final IllegalArgumentException e = expectThrows( IllegalArgumentException.class, - () -> response.toXContent(JsonXContent.contentBuilder(), params) + () -> response.toXContentChunked(params).next().toXContent(JsonXContent.contentBuilder(), params) ); assertThat( e, @@ -64,7 +68,7 @@ public void testGetIndices() { ShardId shId = new ShardId(index, shardId); Path path = createTempDir().resolve("indices").resolve(index.getUUID()).resolve(String.valueOf(shardId)); ShardPath shardPath = new ShardPath(false, path, path, shId); - ShardRouting routing = createShardRouting(index, shId, (shardId == 0)); + ShardRouting routing = createShardRouting(shId, (shardId == 0)); shards.add(new ShardStats(routing, shardPath, null, null, null, null)); AtomicLong primaryShardsCounter = expectedIndexToPrimaryShardsCount.computeIfAbsent( index.getName(), @@ -105,7 +109,45 @@ public void testGetIndices() { } } - private ShardRouting createShardRouting(Index index, ShardId shardId, boolean isPrimary) { + public void testChunkedEncodingPerIndex() throws IOException { + final int shards = randomIntBetween(1, 10); + final List stats = new ArrayList<>(shards); + for (int i = 0; i < shards; i++) { + ShardId shId = new ShardId(createIndex("index-" + i), randomIntBetween(0, 1)); + Path path = createTempDir().resolve("indices").resolve(shId.getIndex().getUUID()).resolve(String.valueOf(shId.id())); + ShardPath shardPath = new ShardPath(false, path, path, shId); + ShardRouting routing = createShardRouting(shId, (shId.id() == 0)); + stats.add(new ShardStats(routing, shardPath, new CommonStats(), null, null, null)); + } + final IndicesStatsResponse indicesStatsResponse = new IndicesStatsResponse( + stats.toArray(new ShardStats[0]), + shards, + shards, + 0, + null, + ClusterState.EMPTY_STATE + ); + final ToXContent.Params paramsClusterLevel = new ToXContent.MapParams(Map.of("level", "cluster")); + final var iteratorClusterLevel = indicesStatsResponse.toXContentChunked(paramsClusterLevel); + int chunksSeenClusterLevel = 0; + final XContentBuilder builder = new XContentBuilder(XContentType.JSON.xContent(), Streams.NULL_OUTPUT_STREAM); + while (iteratorClusterLevel.hasNext()) { + iteratorClusterLevel.next().toXContent(builder, paramsClusterLevel); + chunksSeenClusterLevel++; + } + assertEquals(3, chunksSeenClusterLevel); + + final ToXContent.Params paramsIndexLevel = new ToXContent.MapParams(Map.of("level", "indices")); + final var iteratorIndexLevel = indicesStatsResponse.toXContentChunked(paramsIndexLevel); + int chunksSeenIndexLevel = 0; + while (iteratorIndexLevel.hasNext()) { + iteratorIndexLevel.next().toXContent(builder, paramsIndexLevel); + chunksSeenIndexLevel++; + } + assertEquals(4 + shards, chunksSeenIndexLevel); + } + + private ShardRouting createShardRouting(ShardId shardId, boolean isPrimary) { return TestShardRouting.newShardRouting(shardId, randomAlphaOfLength(4), isPrimary, ShardRoutingState.STARTED); } From 2d74bb795f1e7619a806aad3059eafec677a36b6 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Thu, 24 Nov 2022 17:53:15 +0000 Subject: [PATCH 082/919] [ML] ML stats failures should not stop the usage API working (#91917) It is possible to meddle with internal ML state such that calls to the ML stats APIs return errors. It is justifiable for these single purpose APIs to return errors when the internal state of ML is corrupted. However, it is undesirable for these low level problems to completely prevent the overall usage API from returning, because then callers cannot find out usage information from any part of the system. This change makes errors in the ML stats APIs non-fatal to the overall response of the usage API. When an ML stats APIs returns an error, the corresponding section of the ML usage information will be blank. Fixes #91893 --- docs/changelog/91917.yaml | 6 ++ .../ml/qa/ml-with-security/build.gradle | 1 + .../MachineLearningUsageTransportAction.java | 69 ++++++++++++++----- .../rest-api-spec/test/ml/jobs_get_stats.yml | 54 +++++++++++++++ 4 files changed, 113 insertions(+), 17 deletions(-) create mode 100644 docs/changelog/91917.yaml diff --git a/docs/changelog/91917.yaml b/docs/changelog/91917.yaml new file mode 100644 index 000000000000..92304d353c94 --- /dev/null +++ b/docs/changelog/91917.yaml @@ -0,0 +1,6 @@ +pr: 91917 +summary: ML stats failures should not stop the usage API working +area: Machine Learning +type: bug +issues: + - 91893 diff --git a/x-pack/plugin/ml/qa/ml-with-security/build.gradle b/x-pack/plugin/ml/qa/ml-with-security/build.gradle index 478736168d33..50b8b16c2dd0 100644 --- a/x-pack/plugin/ml/qa/ml-with-security/build.gradle +++ b/x-pack/plugin/ml/qa/ml-with-security/build.gradle @@ -197,6 +197,7 @@ tasks.named("yamlRestTest").configure { 'ml/jobs_get_result_overall_buckets/Test overall buckets given invalid start param', 'ml/jobs_get_result_overall_buckets/Test overall buckets given invalid end param', 'ml/jobs_get_result_overall_buckets/Test overall buckets given bucket_span is smaller than max job bucket_span', + 'ml/jobs_get_stats/Test closed results index', 'ml/jobs_get_stats/Test get job stats given missing job', 'ml/jobs_get_stats/Test no exception on get job stats with missing index', 'ml/job_groups/Test put job with empty group', diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearningUsageTransportAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearningUsageTransportAction.java index c99af373e525..5129d0d45fde 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearningUsageTransportAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearningUsageTransportAction.java @@ -20,6 +20,8 @@ import org.elasticsearch.common.util.Maps; import org.elasticsearch.env.Environment; import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.logging.LogManager; +import org.elasticsearch.logging.Logger; import org.elasticsearch.protocol.xpack.XPackUsageRequest; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; @@ -66,6 +68,8 @@ public class MachineLearningUsageTransportAction extends XPackUsageFeatureTransportAction { + private static final Logger logger = LogManager.getLogger(MachineLearningUsageTransportAction.class); + private final Client client; private final XPackLicenseState licenseState; private final JobManagerHolder jobManagerHolder; @@ -124,8 +128,8 @@ protected void masterOperation( int nodeCount = mlNodeCount(state); // Step 5. return final ML usage - ActionListener> inferenceUsageListener = ActionListener.wrap(inferenceUsage -> { - listener.onResponse( + ActionListener> inferenceUsageListener = ActionListener.wrap( + inferenceUsage -> listener.onResponse( new XPackUsageFeatureResponse( new MachineLearningFeatureSetUsage( MachineLearningField.ML_API_FEATURE.checkWithoutTracking(licenseState), @@ -137,45 +141,76 @@ protected void masterOperation( nodeCount ) ) - ); - }, listener::onFailure); + ), + e -> { + logger.warn("Failed to get inference usage to include in ML usage", e); + listener.onResponse( + new XPackUsageFeatureResponse( + new MachineLearningFeatureSetUsage( + MachineLearningField.ML_API_FEATURE.checkWithoutTracking(licenseState), + enabled, + jobsUsage, + datafeedsUsage, + analyticsUsage, + Collections.emptyMap(), + nodeCount + ) + ) + ); + } + ); // Step 4. Extract usage from data frame analytics configs and then get inference usage ActionListener dataframeAnalyticsListener = ActionListener.wrap(response -> { addDataFrameAnalyticsUsage(response, analyticsUsage); addInferenceUsage(inferenceUsageListener); - }, listener::onFailure); + }, e -> { + logger.warn("Failed to get data frame analytics configs to include in ML usage", e); + addInferenceUsage(inferenceUsageListener); + }); // Step 3. Extract usage from data frame analytics stats and then request data frame analytics configs + GetDataFrameAnalyticsAction.Request getDfaRequest = new GetDataFrameAnalyticsAction.Request(Metadata.ALL); + getDfaRequest.setPageParams(new PageParams(0, 10_000)); ActionListener dataframeAnalyticsStatsListener = ActionListener.wrap(response -> { addDataFrameAnalyticsStatsUsage(response, analyticsUsage); - GetDataFrameAnalyticsAction.Request getDfaRequest = new GetDataFrameAnalyticsAction.Request(Metadata.ALL); - getDfaRequest.setPageParams(new PageParams(0, 10_000)); client.execute(GetDataFrameAnalyticsAction.INSTANCE, getDfaRequest, dataframeAnalyticsListener); - }, listener::onFailure); + }, e -> { + logger.warn("Failed to get data frame analytics stats to include in ML usage", e); + client.execute(GetDataFrameAnalyticsAction.INSTANCE, getDfaRequest, dataframeAnalyticsListener); + }); // Step 2. Extract usage from datafeeds stats and then request stats for data frame analytics + GetDataFrameAnalyticsStatsAction.Request dataframeAnalyticsStatsRequest = new GetDataFrameAnalyticsStatsAction.Request( + Metadata.ALL + ); + dataframeAnalyticsStatsRequest.setPageParams(new PageParams(0, 10_000)); ActionListener datafeedStatsListener = ActionListener.wrap(response -> { addDatafeedsUsage(response, datafeedsUsage); - GetDataFrameAnalyticsStatsAction.Request dataframeAnalyticsStatsRequest = new GetDataFrameAnalyticsStatsAction.Request( - Metadata.ALL - ); - dataframeAnalyticsStatsRequest.setPageParams(new PageParams(0, 10_000)); client.execute(GetDataFrameAnalyticsStatsAction.INSTANCE, dataframeAnalyticsStatsRequest, dataframeAnalyticsStatsListener); - }, listener::onFailure); + }, e -> { + logger.warn("Failed to get datafeed stats to include in ML usage", e); + client.execute(GetDataFrameAnalyticsStatsAction.INSTANCE, dataframeAnalyticsStatsRequest, dataframeAnalyticsStatsListener); + }); // Step 1. Extract usage from jobs stats and then request stats for all datafeeds - GetJobsStatsAction.Request jobStatsRequest = new GetJobsStatsAction.Request(Metadata.ALL); + GetDatafeedsStatsAction.Request datafeedStatsRequest = new GetDatafeedsStatsAction.Request(Metadata.ALL); ActionListener jobStatsListener = ActionListener.wrap( response -> jobManagerHolder.getJobManager().expandJobs(Metadata.ALL, true, ActionListener.wrap(jobs -> { addJobsUsage(response, jobs.results(), jobsUsage); - GetDatafeedsStatsAction.Request datafeedStatsRequest = new GetDatafeedsStatsAction.Request(Metadata.ALL); client.execute(GetDatafeedsStatsAction.INSTANCE, datafeedStatsRequest, datafeedStatsListener); - }, listener::onFailure)), - listener::onFailure + }, e -> { + logger.warn("Failed to get job configs to include in ML usage", e); + client.execute(GetDatafeedsStatsAction.INSTANCE, datafeedStatsRequest, datafeedStatsListener); + })), + e -> { + logger.warn("Failed to get job stats to include in ML usage", e); + client.execute(GetDatafeedsStatsAction.INSTANCE, datafeedStatsRequest, datafeedStatsListener); + } ); // Step 0. Kick off the chain of callbacks by requesting jobs stats + GetJobsStatsAction.Request jobStatsRequest = new GetJobsStatsAction.Request(Metadata.ALL); client.execute(GetJobsStatsAction.INSTANCE, jobStatsRequest, jobStatsListener); } diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/jobs_get_stats.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/jobs_get_stats.yml index 798288d72700..6aab1fb9e894 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/jobs_get_stats.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/jobs_get_stats.yml @@ -397,3 +397,57 @@ setup: - is_false: jobs.1.timing_stats.maximum_bucket_processing_time_ms - is_false: jobs.1.timing_stats.average_bucket_processing_time_ms - is_false: jobs.1.timing_stats.exponential_average_bucket_processing_time_ms + +--- +"Test closed results index": + + - skip: + features: + - "warnings" + + - do: + warnings: + - 'Posting data directly to anomaly detection jobs is deprecated, in a future major version it will be compulsory to use a datafeed' + ml.post_data: + job_id: job-stats-test + body: > + {"airline":"AAL","responsetime":"132.2046","time":"1403481600"} + {"airline":"JZA","responsetime":"990.4628","time":"1403481600"} + + - do: + ml.close_job: + job_id: jobs-get-stats-datafeed-job + - match: { closed: true } + + - do: + ml.close_job: + job_id: job-stats-test + - match: { closed: true } + + - do: + ml.get_job_stats: {} + - length: { jobs : 2 } + + - do: + xpack.usage: {} + - match: { ml.available: true } + - match: { ml.enabled: true } + - match: { ml.jobs.closed.count: 2 } + + - do: + indices.close: + index: .ml-anomalies-shared + + # With the index closed the low level ML API reports a problem + - do: + catch: /type=cluster_block_exception, reason=index \[.ml-anomalies-shared\] blocked by. \[FORBIDDEN\/.\/index closed\]/ + ml.get_job_stats: {} + + # But the high level X-Pack API returns what it can - we do this + # so that corruption to ML doesn't blind observers of the general + # cluster status + - do: + xpack.usage: {} + - match: { ml.available: true } + - match: { ml.enabled: true } + - is_false: ml.jobs.closed.count From a88bea9d5d34638a8a094fe6e5d3f98cda808d1d Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 24 Nov 2022 17:56:52 +0000 Subject: [PATCH 083/919] Chunked encoding for pending tasks API (#91929) This response can reach a few MiB in size in an overwhelmed cluster, let's use chunking so as not to make things worse than they already are. Relates #89838 --- .../cluster/tasks/PendingTasksBlocksIT.java | 6 +-- .../cluster/service/ClusterServiceIT.java | 4 +- .../tasks/PendingClusterTasksResponse.java | 41 +++++++--------- .../RestPendingClusterTasksAction.java | 6 ++- .../cat/RestPendingClusterTasksAction.java | 4 +- .../PendingClusterTasksResponseTests.java | 49 +++++++++++++++++++ .../elasticsearch/test/ESIntegTestCase.java | 6 ++- 7 files changed, 82 insertions(+), 34 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/action/admin/cluster/tasks/PendingClusterTasksResponseTests.java diff --git a/server/src/internalClusterTest/java/org/elasticsearch/action/admin/cluster/tasks/PendingTasksBlocksIT.java b/server/src/internalClusterTest/java/org/elasticsearch/action/admin/cluster/tasks/PendingTasksBlocksIT.java index 8b9173ed9945..568353ab2f1b 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/action/admin/cluster/tasks/PendingTasksBlocksIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/action/admin/cluster/tasks/PendingTasksBlocksIT.java @@ -38,7 +38,7 @@ public void testPendingTasksWithIndexBlocks() { try { enableIndexBlock("test", blockSetting); PendingClusterTasksResponse response = client().admin().cluster().preparePendingClusterTasks().get(); - assertNotNull(response.getPendingTasks()); + assertNotNull(response.pendingTasks()); } finally { disableIndexBlock("test", blockSetting); } @@ -54,7 +54,7 @@ public void testPendingTasksWithClusterReadOnlyBlock() { try { setClusterReadOnly(true); PendingClusterTasksResponse response = client().admin().cluster().preparePendingClusterTasks().get(); - assertNotNull(response.getPendingTasks()); + assertNotNull(response.pendingTasks()); } finally { setClusterReadOnly(false); } @@ -80,7 +80,7 @@ public boolean validateClusterForming() { } }); - assertNotNull(client().admin().cluster().preparePendingClusterTasks().get().getPendingTasks()); + assertNotNull(client().admin().cluster().preparePendingClusterTasks().get().pendingTasks()); // starting one more node allows the cluster to recover internalCluster().startNode(); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/cluster/service/ClusterServiceIT.java b/server/src/internalClusterTest/java/org/elasticsearch/cluster/service/ClusterServiceIT.java index 064949bf4bae..cd2f3ebf561d 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/cluster/service/ClusterServiceIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/cluster/service/ClusterServiceIT.java @@ -366,7 +366,7 @@ public void clusterStateProcessed(ClusterState oldState, ClusterState newState) assertThat(response.pendingTasks().size(), greaterThanOrEqualTo(10)); assertThat(response.pendingTasks().get(0).getSource().string(), equalTo("1")); assertThat(response.pendingTasks().get(0).isExecuting(), equalTo(true)); - for (PendingClusterTask task : response) { + for (PendingClusterTask task : response.pendingTasks()) { controlSources.remove(task.getSource().string()); } assertTrue(controlSources.isEmpty()); @@ -431,7 +431,7 @@ public void onFailure(Exception e) { response = internalCluster().coordOnlyNodeClient().admin().cluster().preparePendingClusterTasks().get(); assertThat(response.pendingTasks().size(), greaterThanOrEqualTo(5)); controlSources = new HashSet<>(Arrays.asList("1", "2", "3", "4", "5")); - for (PendingClusterTask task : response) { + for (PendingClusterTask task : response.pendingTasks()) { if (controlSources.remove(task.getSource().string())) { assertThat(task.getTimeInQueueInMillis(), greaterThan(0L)); } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/tasks/PendingClusterTasksResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/tasks/PendingClusterTasksResponse.java index 04fc4075380a..ec9c245381e4 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/tasks/PendingClusterTasksResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/tasks/PendingClusterTasksResponse.java @@ -10,16 +10,17 @@ import org.elasticsearch.action.ActionResponse; import org.elasticsearch.cluster.service.PendingClusterTask; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.xcontent.ToXContentObject; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.ChunkedToXContent; +import org.elasticsearch.xcontent.ToXContent; import java.io.IOException; import java.util.Iterator; import java.util.List; -public class PendingClusterTasksResponse extends ActionResponse implements Iterable, ToXContentObject { +public class PendingClusterTasksResponse extends ActionResponse implements ChunkedToXContent { private final List pendingTasks; @@ -36,23 +37,11 @@ public List pendingTasks() { return pendingTasks; } - /** - * The pending cluster tasks - */ - public List getPendingTasks() { - return pendingTasks(); - } - - @Override - public Iterator iterator() { - return pendingTasks.iterator(); - } - @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("tasks: (").append(pendingTasks.size()).append("):\n"); - for (PendingClusterTask pendingClusterTask : this) { + for (PendingClusterTask pendingClusterTask : pendingTasks) { sb.append(pendingClusterTask.getInsertOrder()) .append("/") .append(pendingClusterTask.getPriority()) @@ -66,10 +55,12 @@ public String toString() { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.startArray(Fields.TASKS); - for (PendingClusterTask pendingClusterTask : this) { + public Iterator toXContentChunked(ToXContent.Params params) { + return Iterators.concat(Iterators.single((builder, p) -> { + builder.startObject(); + builder.startArray(Fields.TASKS); + return builder; + }), pendingTasks.stream().map(pendingClusterTask -> (builder, p) -> { builder.startObject(); builder.field(Fields.INSERT_ORDER, pendingClusterTask.getInsertOrder()); builder.field(Fields.PRIORITY, pendingClusterTask.getPriority()); @@ -78,10 +69,12 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(Fields.TIME_IN_QUEUE_MILLIS, pendingClusterTask.getTimeInQueueInMillis()); builder.field(Fields.TIME_IN_QUEUE, pendingClusterTask.getTimeInQueue()); builder.endObject(); - } - builder.endArray(); - builder.endObject(); - return builder; + return builder; + }).iterator(), Iterators.single((builder, p) -> { + builder.endArray(); + builder.endObject(); + return builder; + })); } static final class Fields { diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestPendingClusterTasksAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestPendingClusterTasksAction.java index b150fe539255..b5dc4eb50b88 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestPendingClusterTasksAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestPendingClusterTasksAction.java @@ -12,7 +12,7 @@ import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; -import org.elasticsearch.rest.action.RestToXContentListener; +import org.elasticsearch.rest.action.RestChunkedToXContentListener; import java.io.IOException; import java.util.List; @@ -36,6 +36,8 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC PendingClusterTasksRequest pendingClusterTasksRequest = new PendingClusterTasksRequest(); pendingClusterTasksRequest.masterNodeTimeout(request.paramAsTime("master_timeout", pendingClusterTasksRequest.masterNodeTimeout())); pendingClusterTasksRequest.local(request.paramAsBoolean("local", pendingClusterTasksRequest.local())); - return channel -> client.admin().cluster().pendingClusterTasks(pendingClusterTasksRequest, new RestToXContentListener<>(channel)); + return channel -> client.admin() + .cluster() + .pendingClusterTasks(pendingClusterTasksRequest, new RestChunkedToXContentListener<>(channel)); } } diff --git a/server/src/main/java/org/elasticsearch/rest/action/cat/RestPendingClusterTasksAction.java b/server/src/main/java/org/elasticsearch/rest/action/cat/RestPendingClusterTasksAction.java index 90f0254b67c7..56c9d8f55536 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/cat/RestPendingClusterTasksAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/cat/RestPendingClusterTasksAction.java @@ -66,10 +66,10 @@ protected Table getTableWithHeader(final RestRequest request) { return t; } - private Table buildTable(RestRequest request, PendingClusterTasksResponse tasks) { + private Table buildTable(RestRequest request, PendingClusterTasksResponse response) { Table t = getTableWithHeader(request); - for (PendingClusterTask task : tasks) { + for (PendingClusterTask task : response.pendingTasks()) { t.startRow(); t.addCell(task.getInsertOrder()); t.addCell(task.getTimeInQueue()); diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/tasks/PendingClusterTasksResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/tasks/PendingClusterTasksResponseTests.java new file mode 100644 index 000000000000..9e5c8bedf756 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/tasks/PendingClusterTasksResponseTests.java @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.action.admin.cluster.tasks; + +import org.elasticsearch.cluster.service.PendingClusterTask; +import org.elasticsearch.common.Priority; +import org.elasticsearch.common.text.Text; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.ArrayList; + +import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; + +public class PendingClusterTasksResponseTests extends ESTestCase { + public void testPendingClusterTasksResponseChunking() throws IOException { + final var tasks = new ArrayList(); + for (int i = between(0, 10); i > 0; i--) { + tasks.add( + new PendingClusterTask( + randomNonNegativeLong(), + randomFrom(Priority.values()), + new Text(randomAlphaOfLengthBetween(1, 10)), + randomNonNegativeLong(), + randomBoolean() + ) + ); + } + + int chunkCount = 0; + try (XContentBuilder builder = jsonBuilder()) { + final var iterator = new PendingClusterTasksResponse(tasks).toXContentChunked(ToXContent.EMPTY_PARAMS); + while (iterator.hasNext()) { + iterator.next().toXContent(builder, ToXContent.EMPTY_PARAMS); + chunkCount += 1; + } + } // closing the builder verifies that the XContent is well-formed + + assertEquals(tasks.size() + 2, chunkCount); + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java index 1e1381929e4b..68c0274d1dbc 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java @@ -842,7 +842,11 @@ public void waitNoPendingTasksOnAll() throws Exception { ClusterHealthResponse clusterHealth = client.admin().cluster().prepareHealth().setLocal(true).get(); assertThat("client " + client + " still has in flight fetch", clusterHealth.getNumberOfInFlightFetch(), equalTo(0)); PendingClusterTasksResponse pendingTasks = client.admin().cluster().preparePendingClusterTasks().setLocal(true).get(); - assertThat("client " + client + " still has pending tasks " + pendingTasks, pendingTasks, Matchers.emptyIterable()); + assertThat( + "client " + client + " still has pending tasks " + pendingTasks, + pendingTasks.pendingTasks(), + Matchers.emptyIterable() + ); clusterHealth = client.admin().cluster().prepareHealth().setLocal(true).get(); assertThat("client " + client + " still has in flight fetch", clusterHealth.getNumberOfInFlightFetch(), equalTo(0)); } From 6b7e8ffeafc71ad361bb6ce301b5e6ebbeaad006 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Thu, 24 Nov 2022 20:39:15 +0100 Subject: [PATCH 084/919] Disable filter by filter optimization when agg context requires in sort order execution (#91702) Before this change `TimeSeriesIndexSearcher` caused a `CollectionTerminatedException` because `FilterByFilterAggregation#getLeafCollector()` always returns a `LeafBucketCollector.NO_OP_COLLECTOR` instance. And `TimeSeriesIndexSearcher` can't deal with this when initializing its leaf walkers. Maybe there is a way to also have the filter by filter optimization with `TimeSeriesIndexSearcher`. The tricky part is that filter by filter optimization executes when getting the reference to the leaf bucket collector. By then the `TimeSeriesIndexSearcher` hasn't initialized the other leafs and the tsid accounting. This commit ensure that it is at least possible to run aggregations that use the filter by filter optimization and have time series agg as sub agg. This commit also: * Removes unused code in FilterByFilterAggregator * and also increment segmentsWithDeletedDocs field when there is a segment with live docs. --- .../timeseries/TimeSeriesAggregatorTests.java | 36 +++++++++++++++++++ .../aggregation/AggregationProfilerIT.java | 3 +- .../filter/FilterByFilterAggregator.java | 27 +++----------- .../bucket/filter/FiltersAggregatorTests.java | 24 +++++++++++-- 4 files changed, 63 insertions(+), 27 deletions(-) diff --git a/modules/aggregations/src/test/java/org/elasticsearch/aggregations/bucket/timeseries/TimeSeriesAggregatorTests.java b/modules/aggregations/src/test/java/org/elasticsearch/aggregations/bucket/timeseries/TimeSeriesAggregatorTests.java index b98e464daefb..dfd3a7019b5f 100644 --- a/modules/aggregations/src/test/java/org/elasticsearch/aggregations/bucket/timeseries/TimeSeriesAggregatorTests.java +++ b/modules/aggregations/src/test/java/org/elasticsearch/aggregations/bucket/timeseries/TimeSeriesAggregatorTests.java @@ -10,6 +10,7 @@ import org.apache.lucene.document.DoubleDocValuesField; import org.apache.lucene.document.FloatDocValuesField; +import org.apache.lucene.document.LongPoint; import org.apache.lucene.document.NumericDocValuesField; import org.apache.lucene.document.SortedDocValuesField; import org.apache.lucene.document.SortedNumericDocValuesField; @@ -26,6 +27,9 @@ import org.elasticsearch.index.mapper.NumberFieldMapper; import org.elasticsearch.index.mapper.TimeSeriesIdFieldMapper; import org.elasticsearch.index.mapper.TimeSeriesIdFieldMapper.TimeSeriesIdBuilder; +import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; +import org.elasticsearch.search.aggregations.bucket.histogram.InternalDateHistogram; import org.elasticsearch.search.aggregations.metrics.Sum; import org.elasticsearch.search.aggregations.support.ValuesSourceType; @@ -77,6 +81,7 @@ public void testStandAloneTimeSeriesWithSum() throws IOException { public static void writeTS(RandomIndexWriter iw, long timestamp, Object[] dimensions, Object[] metrics) throws IOException { final List fields = new ArrayList<>(); fields.add(new SortedNumericDocValuesField(DataStreamTimestampFieldMapper.DEFAULT_PATH, timestamp)); + fields.add(new LongPoint(DataStreamTimestampFieldMapper.DEFAULT_PATH, timestamp)); final TimeSeriesIdBuilder builder = new TimeSeriesIdBuilder(null); for (int i = 0; i < dimensions.length; i += 2) { if (dimensions[i + 1]instanceof Number n) { @@ -99,6 +104,37 @@ public static void writeTS(RandomIndexWriter iw, long timestamp, Object[] dimens iw.addDocument(fields); } + public void testWithDateHistogramExecutedAsFilterByFilterWithTimeSeriesIndexSearcher() throws IOException { + DateHistogramAggregationBuilder aggregationBuilder = new DateHistogramAggregationBuilder("by_timestamp").field("@timestamp") + .fixedInterval(DateHistogramInterval.HOUR) + .subAggregation(new TimeSeriesAggregationBuilder("ts").subAggregation(sum("sum").field("val1"))); + + // Before this threw a CollectionTerminatedException because FilterByFilterAggregation#getLeafCollector() always returns a + // LeafBucketCollector.NO_OP_COLLECTOR instance. And TimeSeriesIndexSearcher can't deal with this when initializing the + // leaf walkers. + testCase(iw -> { + long startTime = DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.parseMillis("2023-01-01T00:00:00Z"); + for (int i = 1; i <= 5000; i++) { + writeTS(iw, startTime++, new Object[] { "dim1", "aaa" }, new Object[] { "val1", 1 }); + } + }, internalAggregation -> { + InternalDateHistogram dateHistogram = (InternalDateHistogram) internalAggregation; + assertThat(dateHistogram.getBuckets(), hasSize(1)); + InternalTimeSeries timeSeries = dateHistogram.getBuckets().get(0).getAggregations().get("ts"); + assertThat(timeSeries.getBuckets(), hasSize(1)); + Sum sum = timeSeries.getBuckets().get(0).getAggregations().get("sum"); + assertThat(sum.value(), equalTo(5000.0)); + }, + new AggTestConfig( + aggregationBuilder, + TimeSeriesIdFieldMapper.FIELD_TYPE, + new DateFieldMapper.DateFieldType("@timestamp"), + new KeywordFieldMapper.KeywordFieldType("dim1"), + new NumberFieldMapper.NumberFieldType("val1", NumberFieldMapper.NumberType.INTEGER) + ).withQuery(new MatchAllDocsQuery()) + ); + } + private void timeSeriesTestCase( TimeSeriesAggregationBuilder builder, Query query, diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/profile/aggregation/AggregationProfilerIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/profile/aggregation/AggregationProfilerIT.java index cc14f36d02c9..f63ce82d3490 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/profile/aggregation/AggregationProfilerIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/profile/aggregation/AggregationProfilerIT.java @@ -46,6 +46,7 @@ import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.notNullValue; @ESIntegTestCase.SuiteScopeTestCase @@ -690,7 +691,7 @@ public void testFilterByFilter() throws InterruptedException, IOException { .entry("delegate", "FilterByFilterAggregator") .entry( "delegate_debug", - matchesMap().entry("segments_with_deleted_docs", 0) + matchesMap().entry("segments_with_deleted_docs", greaterThanOrEqualTo(0)) .entry("segments_with_doc_count_field", 0) .entry("segments_counted", 0) .entry("segments_collected", greaterThan(0)) diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterByFilterAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterByFilterAggregator.java index eaa35d5fe9c1..312df1cf8b3e 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterByFilterAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterByFilterAggregator.java @@ -13,7 +13,6 @@ import org.apache.lucene.search.Query; import org.apache.lucene.search.Scorable; import org.apache.lucene.util.Bits; -import org.elasticsearch.common.CheckedSupplier; import org.elasticsearch.core.CheckedFunction; import org.elasticsearch.search.aggregations.AdaptingAggregator; import org.elasticsearch.search.aggregations.AggregationExecutionContext; @@ -70,7 +69,7 @@ public AdapterBuilder( this.cardinality = cardinality; this.metadata = metadata; this.rewrittenTopLevelQuery = aggCtx.searcher().rewrite(aggCtx.query()); - this.valid = parent == null && otherBucketKey == null; + this.valid = parent == null && otherBucketKey == null && aggCtx.isInSortOrderExecutionRequired() == false; } /** @@ -224,6 +223,9 @@ protected LeafBucketCollector getLeafCollector(AggregationExecutionContext aggCt return LeafBucketCollector.NO_OP_COLLECTOR; } Bits live = aggCtx.getLeafReaderContext().reader().getLiveDocs(); + if (live != null) { + segmentsWithDeletedDocs++; + } if (false == docCountProvider.alwaysOne()) { segmentsWithDocCountField++; } @@ -304,25 +306,4 @@ public void collectDebugInfo(BiConsumer add) { add.accept("segments_with_doc_count_field", segmentsWithDocCountField); } - CheckedSupplier canUseMetadata(LeafReaderContext ctx) { - return new CheckedSupplier() { - Boolean canUse; - - @Override - public Boolean get() throws IOException { - if (canUse == null) { - canUse = canUse(); - } - return canUse; - } - - private boolean canUse() throws IOException { - if (ctx.reader().getLiveDocs() != null) { - return false; - } - docCountProvider.setLeafReaderContext(ctx); - return docCountProvider.alwaysOne(); - } - }; - } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregatorTests.java index a3c1ed5d7793..548d8a140cad 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregatorTests.java @@ -665,6 +665,12 @@ public void onCache(ShardId shardId, Accountable accountable) {} LongPoint.newRangeQuery("t", 5, Long.MAX_VALUE) ); IndexSearcher searcher = newIndexSearcher(limitedReader); + int segmentsWithLiveDocs = (int) searcher.getIndexReader() + .leaves() + .stream() + .map(LeafReaderContext::reader) + .filter(leafReader -> leafReader.getLiveDocs() != null) + .count(); debugTestCase( builder, new MatchAllDocsQuery(), @@ -679,7 +685,7 @@ public void onCache(ShardId shardId, Accountable accountable) {} matchesMap().entry("segments_counted", greaterThanOrEqualTo(1)) .entry("segments_collected", 0) .entry("segments_with_doc_count_field", 0) - .entry("segments_with_deleted_docs", 0) + .entry("segments_with_deleted_docs", segmentsWithLiveDocs) .entry( "filters", matchesList().item( @@ -730,6 +736,12 @@ public void onCache(ShardId shardId, Accountable accountable) {} LongPoint.newRangeQuery("t", 5, Long.MAX_VALUE) ); IndexSearcher searcher = newIndexSearcher(limitedReader); + int segmentsWithLiveDocs = (int) searcher.getIndexReader() + .leaves() + .stream() + .map(LeafReaderContext::reader) + .filter(leafReader -> leafReader.getLiveDocs() != null) + .count(); debugTestCase( builder, new MatchAllDocsQuery(), @@ -744,7 +756,7 @@ public void onCache(ShardId shardId, Accountable accountable) {} matchesMap().entry("segments_counted", greaterThanOrEqualTo(1)) .entry("segments_collected", 0) .entry("segments_with_doc_count_field", 0) - .entry("segments_with_deleted_docs", 0) + .entry("segments_with_deleted_docs", segmentsWithLiveDocs) .entry( "filters", matchesList().item( @@ -792,6 +804,12 @@ public void onCache(ShardId shardId, Accountable accountable) {} LongPoint.newRangeQuery("t", Long.MIN_VALUE, Long.MAX_VALUE) ); IndexSearcher searcher = newIndexSearcher(limitedReader); + int segmentsWithLiveDocs = (int) searcher.getIndexReader() + .leaves() + .stream() + .map(LeafReaderContext::reader) + .filter(leafReader -> leafReader.getLiveDocs() != null) + .count(); debugTestCase( builder, @@ -807,7 +825,7 @@ public void onCache(ShardId shardId, Accountable accountable) {} matchesMap().entry("segments_counted", greaterThanOrEqualTo(1)) .entry("segments_collected", 0) .entry("segments_with_doc_count_field", 0) - .entry("segments_with_deleted_docs", 0) + .entry("segments_with_deleted_docs", segmentsWithLiveDocs) .entry( "filters", matchesList().item( From 1efb95b857a18d2e12d129ab40603e64d4399756 Mon Sep 17 00:00:00 2001 From: Rene Groeschke Date: Thu, 24 Nov 2022 21:30:41 +0100 Subject: [PATCH 085/919] Ensure all sourceSets are compiled as part of Gradle precommit (#91897) --- .../internal/conventions/precommit/PrecommitTaskPlugin.java | 4 ++-- .../gradle/internal/ElasticsearchJavaBasePlugin.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build-conventions/src/main/java/org/elasticsearch/gradle/internal/conventions/precommit/PrecommitTaskPlugin.java b/build-conventions/src/main/java/org/elasticsearch/gradle/internal/conventions/precommit/PrecommitTaskPlugin.java index f6a5db279792..49148330e02e 100644 --- a/build-conventions/src/main/java/org/elasticsearch/gradle/internal/conventions/precommit/PrecommitTaskPlugin.java +++ b/build-conventions/src/main/java/org/elasticsearch/gradle/internal/conventions/precommit/PrecommitTaskPlugin.java @@ -31,10 +31,10 @@ public void apply(Project project) { "lifecycle-base", p -> project.getTasks().named(LifecycleBasePlugin.CHECK_TASK_NAME).configure(t -> t.dependsOn(precommit)) ); - project.getPluginManager().withPlugin("java", p -> { + project.getPluginManager().withPlugin("java-base", p -> { // run compilation as part of precommit project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets().all(sourceSet -> - precommit.configure(t -> t.shouldRunAfter(sourceSet.getClassesTaskName())) + precommit.configure(t -> t.dependsOn(sourceSet.getClassesTaskName())) ); // make sure tests run after all precommit tasks project.getTasks().withType(Test.class).configureEach(t -> t.mustRunAfter(precommit)); diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchJavaBasePlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchJavaBasePlugin.java index fa578ffa7e20..9dfc37d744b3 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchJavaBasePlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchJavaBasePlugin.java @@ -38,8 +38,8 @@ public class ElasticsearchJavaBasePlugin implements Plugin { public void apply(Project project) { // make sure the global build info plugin is applied to the root project project.getRootProject().getPluginManager().apply(GlobalBuildInfoPlugin.class); - // common repositories setup project.getPluginManager().apply(JavaBasePlugin.class); + // common repositories setup project.getPluginManager().apply(RepositoriesSetupPlugin.class); project.getPluginManager().apply(ElasticsearchTestBasePlugin.class); project.getPluginManager().apply(PrecommitTaskPlugin.class); From 2e6f9a965f67fc3af55cb0adcfa7ae73f27ba0e8 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Fri, 25 Nov 2022 19:51:49 +0100 Subject: [PATCH 086/919] Chunked FieldUsageStatsResponse (#91942) These responses can become extremely large, chunk them. --- .../stats/FieldUsageStatsResponse.java | 19 +++--- .../indices/RestFieldUsageStatsAction.java | 4 +- .../stats/FieldUsageStatsResponseTests.java | 60 +++++++++++++++++++ 3 files changed, 70 insertions(+), 13 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/action/admin/indices/stats/FieldUsageStatsResponseTests.java diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/stats/FieldUsageStatsResponse.java b/server/src/main/java/org/elasticsearch/action/admin/indices/stats/FieldUsageStatsResponse.java index 7132449cee10..9b2efa83746e 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/stats/FieldUsageStatsResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/stats/FieldUsageStatsResponse.java @@ -9,16 +9,17 @@ package org.elasticsearch.action.admin.indices.stats; import org.elasticsearch.action.support.DefaultShardOperationFailedException; -import org.elasticsearch.action.support.broadcast.BroadcastResponse; +import org.elasticsearch.action.support.broadcast.ChunkedBroadcastResponse; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import java.io.IOException; +import java.util.Iterator; import java.util.List; import java.util.Map; -public class FieldUsageStatsResponse extends BroadcastResponse { +public class FieldUsageStatsResponse extends ChunkedBroadcastResponse { private final Map> stats; FieldUsageStatsResponse( @@ -48,19 +49,15 @@ public Map> getStats() { } @Override - protected void addCustomXContentFields(XContentBuilder builder, Params params) throws IOException { - final List>> sortedEntries = stats.entrySet() - .stream() - .sorted(Map.Entry.comparingByKey()) - .toList(); - for (Map.Entry> entry : sortedEntries) { + protected Iterator customXContentChunks(ToXContent.Params params) { + return stats.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(entry -> (ToXContent) (builder, p) -> { builder.startObject(entry.getKey()); builder.startArray("shards"); for (FieldUsageShardResponse resp : entry.getValue()) { resp.toXContent(builder, params); } builder.endArray(); - builder.endObject(); - } + return builder.endObject(); + }).iterator(); } } diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestFieldUsageStatsAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestFieldUsageStatsAction.java index c1c20724a8c7..9a5f54013998 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestFieldUsageStatsAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestFieldUsageStatsAction.java @@ -17,7 +17,7 @@ import org.elasticsearch.rest.RestHandler; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.action.RestCancellableNodeClient; -import org.elasticsearch.rest.action.RestToXContentListener; +import org.elasticsearch.rest.action.RestChunkedToXContentListener; import java.io.IOException; import java.util.List; @@ -42,7 +42,7 @@ public BaseRestHandler.RestChannelConsumer prepareRequest(final RestRequest requ fusRequest.fields(request.paramAsStringArray("fields", fusRequest.fields())); return channel -> { final RestCancellableNodeClient cancelClient = new RestCancellableNodeClient(client, request.getHttpChannel()); - cancelClient.execute(FieldUsageStatsAction.INSTANCE, fusRequest, new RestToXContentListener<>(channel)); + cancelClient.execute(FieldUsageStatsAction.INSTANCE, fusRequest, new RestChunkedToXContentListener<>(channel)); }; } } diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/stats/FieldUsageStatsResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/stats/FieldUsageStatsResponseTests.java new file mode 100644 index 000000000000..be98f601c5f5 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/stats/FieldUsageStatsResponseTests.java @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.action.admin.indices.stats; + +import org.elasticsearch.cluster.routing.ShardRoutingState; +import org.elasticsearch.cluster.routing.TestShardRouting; +import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.util.Maps; +import org.elasticsearch.index.search.stats.FieldUsageStats; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.json.JsonXContent; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public class FieldUsageStatsResponseTests extends ESTestCase { + + public void testToXContentChunkPerIndex() throws IOException { + final int indices = randomIntBetween(0, 100); + final Map> perIndex = Maps.newMapWithExpectedSize(indices); + for (int i = 0; i < indices; i++) { + perIndex.put( + "index-" + i, + List.of( + new FieldUsageShardResponse( + "tracking_id", + TestShardRouting.newShardRouting( + new ShardId("index" + i, UUIDs.randomBase64UUID(random()), 0), + "node_id", + true, + ShardRoutingState.STARTED + ), + 0, + new FieldUsageStats() + ) + ) + ); + } + final FieldUsageStatsResponse response = new FieldUsageStatsResponse(indices, indices, 0, List.of(), perIndex); + + final XContentBuilder builder = JsonXContent.contentBuilder(); + final var iterator = response.toXContentChunked(ToXContent.EMPTY_PARAMS); + int chunks = 0; + while (iterator.hasNext()) { + iterator.next().toXContent(builder, ToXContent.EMPTY_PARAMS); + chunks++; + } + assertEquals(indices + 2, chunks); + } +} From 08313e6071e309e8607015d86b0c44294e3ca413 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 28 Nov 2022 09:13:06 +1100 Subject: [PATCH 087/919] Record timestamp on API key invalidation (#91873) This PR enhances the API key invalidation process so that it records the timestamp in addition to setting the invalidated flag. Relates: #91738 --- docs/changelog/91873.yaml | 5 + .../authc/apikey/ApiKeySingleNodeTests.java | 34 +++++ .../xpack/security/authc/ApiKeyService.java | 3 +- .../support/SecuritySystemIndices.java | 5 + .../security/authc/ApiKeyServiceTests.java | 125 +++++++++++++++++- 5 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 docs/changelog/91873.yaml diff --git a/docs/changelog/91873.yaml b/docs/changelog/91873.yaml new file mode 100644 index 000000000000..3f2fb776bb63 --- /dev/null +++ b/docs/changelog/91873.yaml @@ -0,0 +1,5 @@ +pr: 91873 +summary: Record timestamp on API key invalidation +area: Security +type: enhancement +issues: [] diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java index eab4f59d7cd9..5eb6614b5b43 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java @@ -41,6 +41,8 @@ import org.elasticsearch.xpack.core.security.action.apikey.GetApiKeyResponse; import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyAction; import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyRequest; +import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyAction; +import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyAction; import org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyRequest; import org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyResponse; @@ -74,7 +76,12 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.emptyArray; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.hamcrest.Matchers.nullValue; public class ApiKeySingleNodeTests extends SecuritySingleNodeTestCase { @@ -376,6 +383,33 @@ public void testGrantApiKeyForUserWithRunAs() throws IOException { ); } + public void testInvalidateApiKeyWillRecordTimestamp() { + final String apiKeyId = client().execute( + CreateApiKeyAction.INSTANCE, + new CreateApiKeyRequest(randomAlphaOfLengthBetween(3, 8), null, TimeValue.timeValueMillis(randomLongBetween(1, 1000)), null) + ).actionGet().getId(); + assertThat(getApiKeyDocument(apiKeyId).get("invalidation_time"), nullValue()); + + final long start = Instant.now().toEpochMilli(); + final List invalidatedApiKeys = client().execute( + InvalidateApiKeyAction.INSTANCE, + InvalidateApiKeyRequest.usingApiKeyId(apiKeyId, true) + ).actionGet().getInvalidatedApiKeys(); + final long finish = Instant.now().toEpochMilli(); + + assertThat(invalidatedApiKeys, equalTo(List.of(apiKeyId))); + final Map apiKeyDocument = getApiKeyDocument(apiKeyId); + assertThat(apiKeyDocument.get("api_key_invalidated"), is(true)); + assertThat(apiKeyDocument.get("invalidation_time"), instanceOf(Long.class)); + final long invalidationTime = (long) apiKeyDocument.get("invalidation_time"); + assertThat(invalidationTime, greaterThanOrEqualTo(start)); + assertThat(invalidationTime, lessThanOrEqualTo(finish)); + + // Invalidate it again won't change the timestamp + client().execute(InvalidateApiKeyAction.INSTANCE, InvalidateApiKeyRequest.usingApiKeyId(apiKeyId, true)).actionGet(); + assertThat((long) getApiKeyDocument(apiKeyId).get("invalidation_time"), equalTo(invalidationTime)); + } + private GrantApiKeyRequest buildGrantApiKeyRequest(String username, SecureString password, String runAsUsername) throws IOException { final SecureString clonedPassword = password.clone(); final GrantApiKeyRequest grantApiKeyRequest = new GrantApiKeyRequest(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 64dcfc683ac6..e18a8a6ea011 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -1363,9 +1363,10 @@ private void indexInvalidation(Collection apiKeyIds, ActionListener Instant.now()).when(clock).instant(); } public void testCreateApiKeyUsesBulkIndexAction() throws Exception { @@ -349,6 +366,105 @@ public void testInvalidateApiKeys() throws Exception { assertThat(invalidateApiKeyResponse.getInvalidatedApiKeys(), emptyIterable()); } + @SuppressWarnings("unchecked") + public void testInvalidateApiKeysWillSetInvalidatedFlagAndRecordTimestamp() { + final int docId = randomIntBetween(0, Integer.MAX_VALUE); + final String apiKeyId = randomAlphaOfLength(20); + + // Mock the search request for keys to invalidate + when(client.threadPool()).thenReturn(threadPool); + when(client.prepareSearch(eq(SECURITY_MAIN_ALIAS))).thenReturn(new SearchRequestBuilder(client, SearchAction.INSTANCE)); + doAnswer(invocation -> { + final var listener = (ActionListener) invocation.getArguments()[1]; + final var searchHit = new SearchHit(docId, apiKeyId); + try (XContentBuilder builder = JsonXContent.contentBuilder()) { + builder.map(buildApiKeySourceDoc("some_hash".toCharArray())); + searchHit.sourceRef(BytesReference.bytes(builder)); + } + final var internalSearchResponse = new InternalSearchResponse( + new SearchHits( + new SearchHit[] { searchHit }, + new TotalHits(1, TotalHits.Relation.EQUAL_TO), + randomFloat(), + null, + null, + null + ), + null, + null, + null, + false, + null, + 0 + ); + final var searchResponse = new SearchResponse( + internalSearchResponse, + randomAlphaOfLengthBetween(3, 8), + 1, + 1, + 0, + 10, + null, + null + ); + listener.onResponse(searchResponse); + return null; + }).when(client).search(any(SearchRequest.class), anyActionListener()); + + // Capture the Update request so that we can verify it is configured as expected + when(client.prepareBulk()).thenReturn(new BulkRequestBuilder(client, BulkAction.INSTANCE)); + final var updateRequestBuilder = Mockito.spy(new UpdateRequestBuilder(client, UpdateAction.INSTANCE)); + when(client.prepareUpdate(eq(SECURITY_MAIN_ALIAS), eq(apiKeyId))).thenReturn(updateRequestBuilder); + + // Stub bulk and cache clearing calls so that the entire action flow can complete (not strictly necessary but nice to have) + doAnswer(invocation -> { + final var listener = (ActionListener) invocation.getArguments()[1]; + listener.onResponse( + new BulkResponse( + new BulkItemResponse[] { + BulkItemResponse.success( + docId, + DocWriteRequest.OpType.UPDATE, + new UpdateResponse( + mock(ShardId.class), + apiKeyId, + randomLong(), + randomLong(), + randomLong(), + DocWriteResponse.Result.UPDATED + ) + ) }, + randomLongBetween(1, 100) + ) + ); + return null; + }).when(client).bulk(any(BulkRequest.class), anyActionListener()); + doAnswer(invocation -> { + final var listener = (ActionListener) invocation.getArguments()[2]; + listener.onResponse(mock(ClearSecurityCacheResponse.class)); + return null; + }).when(client).execute(eq(ClearSecurityCacheAction.INSTANCE), any(ClearSecurityCacheRequest.class), anyActionListener()); + + final long invalidationTime = randomMillisUpToYear9999(); + when(clock.instant()).thenReturn(Instant.ofEpochMilli(invalidationTime)); + final ApiKeyService service = createApiKeyService(); + PlainActionFuture future = new PlainActionFuture<>(); + service.invalidateApiKeys(null, null, null, new String[] { apiKeyId }, future); + final InvalidateApiKeyResponse invalidateApiKeyResponse = future.actionGet(); + + assertThat(invalidateApiKeyResponse.getInvalidatedApiKeys(), equalTo(List.of(apiKeyId))); + verify(updateRequestBuilder).setDoc( + argThat( + (ArgumentMatcher>) argument -> Map.of( + "api_key_invalidated", + true, + "invalidation_time", + invalidationTime + ).equals(argument) + ) + ); + } + public void testCreateApiKeyWillCacheOnCreation() { final Settings settings = Settings.builder().put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), true).build(); final ApiKeyService service = createApiKeyService(settings); @@ -1994,7 +2110,7 @@ private ApiKeyService createApiKeyService(Settings baseSettings) { .build(); final ApiKeyService service = new ApiKeyService( settings, - Clock.systemUTC(), + clock, client, securityIndex, ClusterServiceUtils.createClusterService(threadPool), @@ -2017,8 +2133,11 @@ private Map buildApiKeySourceDoc(char[] hash) { sourceMap.put("api_key_hash", new String(hash)); sourceMap.put("name", randomAlphaOfLength(12)); sourceMap.put("version", 0); - sourceMap.put("role_descriptors", Collections.singletonMap("a role", Collections.singletonMap("cluster", "all"))); - sourceMap.put("limited_by_role_descriptors", Collections.singletonMap("limited role", Collections.singletonMap("cluster", "all"))); + sourceMap.put("role_descriptors", Collections.singletonMap("a role", Collections.singletonMap("cluster", List.of("all")))); + sourceMap.put( + "limited_by_role_descriptors", + Collections.singletonMap("limited role", Collections.singletonMap("cluster", List.of("all"))) + ); Map creatorMap = new HashMap<>(); creatorMap.put("principal", "test_user"); creatorMap.put("full_name", "test user"); From 26d9bdd280d5eb1783251854a1e49d97cd05db70 Mon Sep 17 00:00:00 2001 From: Jake Landis Date: Sun, 27 Nov 2022 22:55:48 -0600 Subject: [PATCH 088/919] Support SAN/dnsName for restricted trust (#91946) This commit extends the TLS restricted trust model to allow reading from alternative fields from the X509 certificate. Prior to this commit the only supported (hard coded) value that could be used with restricted trust is the SAN/otherName/CN value. This commit introduces support to read from other fields from the X509 certificate. This commit also introduces support to read from SAN/dnsName if configured. Any fields read from the certificate will be used to match against the restricted trust file and if any of the values match to the restricted trust file, then restricted trust is allowed. Only if none of the values match then the restricted trust denied. SAN/otherName/CN is the default, and SAN/dnsName can be used in addition or in place of SAN/otherName/CN. The possible configuration values are: *.trust_restrictions.x509_fields: ["subjectAltName.otherName.commonName", "subjectAltName.dnsName"] To help support testing, all of the existing certificates have been updated to include a SAN/dnsName that matches the SAN/otherName/CN. This allows the tests to randomize which field(s) are used to match for restricted trust. This also has the side effect of making this commit larger than expected in terms of lines of change. A readme has been included with copy-able commands to recreate the certificates as needed. Additionally, a CCS REST test has been introduced that uses the restricted trust. To support this new CCS REST test the private keys for the test certificates are also included in this commit as well as the gradle configuration needed to share those certificates across projects. --- docs/changelog/91946.yaml | 5 + .../common/ssl/SslConfigurationLoader.java | 4 + x-pack/plugin/core/build.gradle | 7 ++ .../xpack/core/ssl/RestrictedTrustConfig.java | 10 +- .../core/ssl/RestrictedTrustManager.java | 74 +++++++++++---- .../core/ssl/SSLConfigurationSettings.java | 42 ++++++++- .../xpack/core/ssl/SslSettingsLoader.java | 15 ++- .../core/ssl/RestrictedTrustConfigTests.java | 5 +- .../core/ssl/RestrictedTrustManagerTests.java | 39 +++++++- .../ssl/SSLConfigurationSettingsTests.java | 30 ++++++ .../certs/simple/nodes/ca-signed/n1.c1.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n1.c1.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n1.c2.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n1.c2.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n1.c3.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n1.c3.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n1.c4.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n1.c4.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n1.c5.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n1.c5.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n1.c6.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n1.c6.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n1.c7.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n1.c7.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n1.c8.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n1.c8.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n2.c1.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n2.c1.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n2.c2.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n2.c2.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n2.c3.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n2.c3.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n2.c4.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n2.c4.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n2.c5.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n2.c5.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n2.c6.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n2.c6.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n2.c7.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n2.c7.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n2.c8.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n2.c8.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n3.c1.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n3.c1.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n3.c2.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n3.c2.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n3.c3.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n3.c3.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n3.c4.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n3.c4.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n3.c5.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n3.c5.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n3.c6.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n3.c6.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n3.c7.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n3.c7.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n3.c8.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n3.c8.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n4.c1.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n4.c1.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n4.c2.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n4.c2.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n4.c3.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n4.c3.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n4.c4.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n4.c4.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n4.c5.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n4.c5.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n4.c6.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n4.c6.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n4.c7.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n4.c7.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n4.c8.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n4.c8.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n5.c1.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n5.c1.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n5.c2.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n5.c2.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n5.c3.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n5.c3.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n5.c4.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n5.c4.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n5.c5.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n5.c5.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n5.c6.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n5.c6.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n5.c7.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n5.c7.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n5.c8.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n5.c8.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n6.c1.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n6.c1.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n6.c2.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n6.c2.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n6.c3.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n6.c3.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n6.c4.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n6.c4.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n6.c5.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n6.c5.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n6.c6.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n6.c6.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n6.c7.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n6.c7.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n6.c8.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n6.c8.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n7.c1.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n7.c1.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n7.c2.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n7.c2.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n7.c3.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n7.c3.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n7.c4.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n7.c4.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n7.c5.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n7.c5.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n7.c6.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n7.c6.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n7.c7.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n7.c7.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n7.c8.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n7.c8.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n8.c1.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n8.c1.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n8.c2.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n8.c2.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n8.c3.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n8.c3.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n8.c4.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n8.c4.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n8.c5.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n8.c5.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n8.c6.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n8.c6.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n8.c7.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n8.c7.key | 27 ++++++ .../certs/simple/nodes/ca-signed/n8.c8.crt | 33 ++++--- .../certs/simple/nodes/ca-signed/n8.c8.key | 27 ++++++ .../ssl/certs/simple/nodes/readme.md | 58 ++++++++++++ .../certs/simple/nodes/self-signed/n1.c1.crt | 30 +++--- .../certs/simple/nodes/self-signed/n1.c1.key | 27 ++++++ .../certs/simple/nodes/self-signed/n1.c2.crt | 30 +++--- .../certs/simple/nodes/self-signed/n1.c2.key | 27 ++++++ .../certs/simple/nodes/self-signed/n1.c3.crt | 30 +++--- .../certs/simple/nodes/self-signed/n1.c3.key | 27 ++++++ .../certs/simple/nodes/self-signed/n1.c4.crt | 30 +++--- .../certs/simple/nodes/self-signed/n1.c4.key | 27 ++++++ .../certs/simple/nodes/self-signed/n1.c5.crt | 30 +++--- .../certs/simple/nodes/self-signed/n1.c5.key | 27 ++++++ .../certs/simple/nodes/self-signed/n1.c6.crt | 30 +++--- .../certs/simple/nodes/self-signed/n1.c6.key | 27 ++++++ .../certs/simple/nodes/self-signed/n1.c7.crt | 30 +++--- .../certs/simple/nodes/self-signed/n1.c7.key | 27 ++++++ .../certs/simple/nodes/self-signed/n1.c8.crt | 30 +++--- .../certs/simple/nodes/self-signed/n1.c8.key | 27 ++++++ .../certs/simple/nodes/self-signed/n2.c1.crt | 30 +++--- .../certs/simple/nodes/self-signed/n2.c1.key | 27 ++++++ .../certs/simple/nodes/self-signed/n2.c2.crt | 30 +++--- .../certs/simple/nodes/self-signed/n2.c2.key | 27 ++++++ .../certs/simple/nodes/self-signed/n2.c3.crt | 30 +++--- .../certs/simple/nodes/self-signed/n2.c3.key | 27 ++++++ .../certs/simple/nodes/self-signed/n2.c4.crt | 30 +++--- .../certs/simple/nodes/self-signed/n2.c4.key | 27 ++++++ .../certs/simple/nodes/self-signed/n2.c5.crt | 30 +++--- .../certs/simple/nodes/self-signed/n2.c5.key | 27 ++++++ .../certs/simple/nodes/self-signed/n2.c6.crt | 30 +++--- .../certs/simple/nodes/self-signed/n2.c6.key | 27 ++++++ .../certs/simple/nodes/self-signed/n2.c7.crt | 30 +++--- .../certs/simple/nodes/self-signed/n2.c7.key | 27 ++++++ .../certs/simple/nodes/self-signed/n2.c8.crt | 30 +++--- .../certs/simple/nodes/self-signed/n2.c8.key | 27 ++++++ .../certs/simple/nodes/self-signed/n3.c1.crt | 30 +++--- .../certs/simple/nodes/self-signed/n3.c1.key | 27 ++++++ .../certs/simple/nodes/self-signed/n3.c2.crt | 30 +++--- .../certs/simple/nodes/self-signed/n3.c2.key | 27 ++++++ .../certs/simple/nodes/self-signed/n3.c3.crt | 30 +++--- .../certs/simple/nodes/self-signed/n3.c3.key | 27 ++++++ .../certs/simple/nodes/self-signed/n3.c4.crt | 30 +++--- .../certs/simple/nodes/self-signed/n3.c4.key | 27 ++++++ .../certs/simple/nodes/self-signed/n3.c5.crt | 30 +++--- .../certs/simple/nodes/self-signed/n3.c5.key | 27 ++++++ .../certs/simple/nodes/self-signed/n3.c6.crt | 30 +++--- .../certs/simple/nodes/self-signed/n3.c6.key | 27 ++++++ .../certs/simple/nodes/self-signed/n3.c7.crt | 30 +++--- .../certs/simple/nodes/self-signed/n3.c7.key | 27 ++++++ .../certs/simple/nodes/self-signed/n3.c8.crt | 30 +++--- .../certs/simple/nodes/self-signed/n3.c8.key | 27 ++++++ .../certs/simple/nodes/self-signed/n4.c1.crt | 30 +++--- .../certs/simple/nodes/self-signed/n4.c1.key | 27 ++++++ .../certs/simple/nodes/self-signed/n4.c2.crt | 30 +++--- .../certs/simple/nodes/self-signed/n4.c2.key | 27 ++++++ .../certs/simple/nodes/self-signed/n4.c3.crt | 30 +++--- .../certs/simple/nodes/self-signed/n4.c3.key | 27 ++++++ .../certs/simple/nodes/self-signed/n4.c4.crt | 30 +++--- .../certs/simple/nodes/self-signed/n4.c4.key | 27 ++++++ .../certs/simple/nodes/self-signed/n4.c5.crt | 30 +++--- .../certs/simple/nodes/self-signed/n4.c5.key | 27 ++++++ .../certs/simple/nodes/self-signed/n4.c6.crt | 30 +++--- .../certs/simple/nodes/self-signed/n4.c6.key | 27 ++++++ .../certs/simple/nodes/self-signed/n4.c7.crt | 30 +++--- .../certs/simple/nodes/self-signed/n4.c7.key | 27 ++++++ .../certs/simple/nodes/self-signed/n4.c8.crt | 30 +++--- .../certs/simple/nodes/self-signed/n4.c8.key | 27 ++++++ .../certs/simple/nodes/self-signed/n5.c1.crt | 30 +++--- .../certs/simple/nodes/self-signed/n5.c1.key | 27 ++++++ .../certs/simple/nodes/self-signed/n5.c2.crt | 30 +++--- .../certs/simple/nodes/self-signed/n5.c2.key | 27 ++++++ .../certs/simple/nodes/self-signed/n5.c3.crt | 30 +++--- .../certs/simple/nodes/self-signed/n5.c3.key | 27 ++++++ .../certs/simple/nodes/self-signed/n5.c4.crt | 30 +++--- .../certs/simple/nodes/self-signed/n5.c4.key | 27 ++++++ .../certs/simple/nodes/self-signed/n5.c5.crt | 30 +++--- .../certs/simple/nodes/self-signed/n5.c5.key | 27 ++++++ .../certs/simple/nodes/self-signed/n5.c6.crt | 30 +++--- .../certs/simple/nodes/self-signed/n5.c6.key | 27 ++++++ .../certs/simple/nodes/self-signed/n5.c7.crt | 30 +++--- .../certs/simple/nodes/self-signed/n5.c7.key | 27 ++++++ .../certs/simple/nodes/self-signed/n5.c8.crt | 30 +++--- .../certs/simple/nodes/self-signed/n5.c8.key | 27 ++++++ .../certs/simple/nodes/self-signed/n6.c1.crt | 30 +++--- .../certs/simple/nodes/self-signed/n6.c1.key | 27 ++++++ .../certs/simple/nodes/self-signed/n6.c2.crt | 30 +++--- .../certs/simple/nodes/self-signed/n6.c2.key | 27 ++++++ .../certs/simple/nodes/self-signed/n6.c3.crt | 30 +++--- .../certs/simple/nodes/self-signed/n6.c3.key | 27 ++++++ .../certs/simple/nodes/self-signed/n6.c4.crt | 30 +++--- .../certs/simple/nodes/self-signed/n6.c4.key | 27 ++++++ .../certs/simple/nodes/self-signed/n6.c5.crt | 30 +++--- .../certs/simple/nodes/self-signed/n6.c5.key | 27 ++++++ .../certs/simple/nodes/self-signed/n6.c6.crt | 30 +++--- .../certs/simple/nodes/self-signed/n6.c6.key | 27 ++++++ .../certs/simple/nodes/self-signed/n6.c7.crt | 30 +++--- .../certs/simple/nodes/self-signed/n6.c7.key | 27 ++++++ .../certs/simple/nodes/self-signed/n6.c8.crt | 30 +++--- .../certs/simple/nodes/self-signed/n6.c8.key | 27 ++++++ .../certs/simple/nodes/self-signed/n7.c1.crt | 30 +++--- .../certs/simple/nodes/self-signed/n7.c1.key | 27 ++++++ .../certs/simple/nodes/self-signed/n7.c2.crt | 30 +++--- .../certs/simple/nodes/self-signed/n7.c2.key | 27 ++++++ .../certs/simple/nodes/self-signed/n7.c3.crt | 30 +++--- .../certs/simple/nodes/self-signed/n7.c3.key | 27 ++++++ .../certs/simple/nodes/self-signed/n7.c4.crt | 30 +++--- .../certs/simple/nodes/self-signed/n7.c4.key | 27 ++++++ .../certs/simple/nodes/self-signed/n7.c5.crt | 30 +++--- .../certs/simple/nodes/self-signed/n7.c5.key | 27 ++++++ .../certs/simple/nodes/self-signed/n7.c6.crt | 30 +++--- .../certs/simple/nodes/self-signed/n7.c6.key | 27 ++++++ .../certs/simple/nodes/self-signed/n7.c7.crt | 30 +++--- .../certs/simple/nodes/self-signed/n7.c7.key | 27 ++++++ .../certs/simple/nodes/self-signed/n7.c8.crt | 30 +++--- .../certs/simple/nodes/self-signed/n7.c8.key | 27 ++++++ .../certs/simple/nodes/self-signed/n8.c1.crt | 30 +++--- .../certs/simple/nodes/self-signed/n8.c1.key | 27 ++++++ .../certs/simple/nodes/self-signed/n8.c2.crt | 30 +++--- .../certs/simple/nodes/self-signed/n8.c2.key | 27 ++++++ .../certs/simple/nodes/self-signed/n8.c3.crt | 30 +++--- .../certs/simple/nodes/self-signed/n8.c3.key | 27 ++++++ .../certs/simple/nodes/self-signed/n8.c4.crt | 30 +++--- .../certs/simple/nodes/self-signed/n8.c4.key | 27 ++++++ .../certs/simple/nodes/self-signed/n8.c5.crt | 30 +++--- .../certs/simple/nodes/self-signed/n8.c5.key | 27 ++++++ .../certs/simple/nodes/self-signed/n8.c6.crt | 30 +++--- .../certs/simple/nodes/self-signed/n8.c6.key | 27 ++++++ .../certs/simple/nodes/self-signed/n8.c7.crt | 30 +++--- .../certs/simple/nodes/self-signed/n8.c7.key | 27 ++++++ .../certs/simple/nodes/self-signed/n8.c8.crt | 30 +++--- .../certs/simple/nodes/self-signed/n8.c8.key | 27 ++++++ .../xpack/security/SecurityTests.java | 2 + .../legacy-with-restricted-trust/build.gradle | 94 +++++++++++++++++++ .../RemoteClusterSecuritySmokeIT.java | 69 ++++++++++++++ .../src/test/resources/trust.yml | 2 + 271 files changed, 6183 insertions(+), 1761 deletions(-) create mode 100644 docs/changelog/91946.yaml create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c1.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c2.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c3.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c4.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c5.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c6.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c7.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c8.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c1.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c2.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c3.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c4.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c5.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c6.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c7.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c8.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c1.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c2.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c3.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c4.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c5.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c6.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c7.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c8.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c1.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c2.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c3.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c4.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c5.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c6.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c7.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c8.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c1.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c2.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c3.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c4.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c5.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c6.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c7.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c8.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c1.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c2.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c3.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c4.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c5.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c6.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c7.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c8.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c1.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c2.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c3.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c4.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c5.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c6.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c7.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c8.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c1.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c2.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c3.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c4.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c5.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c6.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c7.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c8.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/readme.md create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c1.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c2.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c3.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c4.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c5.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c6.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c7.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c8.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c1.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c2.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c3.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c4.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c5.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c6.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c7.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c8.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c1.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c2.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c3.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c4.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c5.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c6.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c7.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c8.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c1.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c2.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c3.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c4.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c5.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c6.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c7.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c8.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c1.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c2.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c3.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c4.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c5.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c6.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c7.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c8.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c1.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c2.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c3.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c4.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c5.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c6.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c7.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c8.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c1.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c2.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c3.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c4.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c5.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c6.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c7.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c8.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c1.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c2.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c3.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c4.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c5.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c6.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c7.key create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c8.key create mode 100644 x-pack/qa/multi-cluster-search-security/legacy-with-restricted-trust/build.gradle create mode 100644 x-pack/qa/multi-cluster-search-security/legacy-with-restricted-trust/src/test/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecuritySmokeIT.java create mode 100644 x-pack/qa/multi-cluster-search-security/legacy-with-restricted-trust/src/test/resources/trust.yml diff --git a/docs/changelog/91946.yaml b/docs/changelog/91946.yaml new file mode 100644 index 000000000000..83fb3b63382f --- /dev/null +++ b/docs/changelog/91946.yaml @@ -0,0 +1,5 @@ +pr: 91946 +summary: Support SAN/dnsName for restricted trust +area: TLS +type: enhancement +issues: [] diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationLoader.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationLoader.java index 4acee2dfcec0..0dd72f4f94f1 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationLoader.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationLoader.java @@ -355,6 +355,10 @@ protected Path resolvePath(String settingKey, Path basePath) { return resolveSetting(settingKey, basePath::resolve, null); } + protected List resolveList(String settingKey, List defaultList) { + return resolveListSetting(settingKey, Function.identity(), defaultList); + } + private String expandSettingKey(String key) { return settingPrefix + key; } diff --git a/x-pack/plugin/core/build.gradle b/x-pack/plugin/core/build.gradle index ba4218a0eab8..0b548af7da43 100644 --- a/x-pack/plugin/core/build.gradle +++ b/x-pack/plugin/core/build.gradle @@ -26,6 +26,11 @@ tasks.named("dependencyLicenses").configure { mapping from: /commons-.*/, to: 'commons' // pulled in by rest client } +configurations { + signedCerts + rootCert +} + dependencies { compileOnly project(":server") api project(':libs:elasticsearch-grok') @@ -59,6 +64,8 @@ dependencies { yamlRestTestImplementation project(':x-pack:plugin:core') javaRestTestImplementation(testArtifact(project(xpackModule('core')))) + signedCerts fileTree("src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed") + rootCert files("src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca.crt") } ext.expansions = [ diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustConfig.java index db8de5d36455..9afcafeb032d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustConfig.java @@ -17,6 +17,7 @@ import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.Set; import javax.net.ssl.X509ExtendedTrustManager; @@ -28,10 +29,15 @@ public final class RestrictedTrustConfig implements SslTrustConfig { private static final String RESTRICTIONS_KEY_SUBJECT_NAME = "trust.subject_name"; + public static final String SAN_OTHER_COMMON = "subjectAltName.otherName.commonName"; + public static final String SAN_DNS = "subjectAltName.dnsName"; + static final Set SUPPORTED_X_509_FIELDS = Set.of(SAN_OTHER_COMMON, SAN_DNS); private final Path groupConfigPath; private final SslTrustConfig delegate; + private final Set configuredX509Fields; - RestrictedTrustConfig(Path groupConfigPath, SslTrustConfig delegate) { + RestrictedTrustConfig(Path groupConfigPath, Set configuredX509Fields, SslTrustConfig delegate) { + this.configuredX509Fields = configuredX509Fields; this.groupConfigPath = Objects.requireNonNull(groupConfigPath); this.delegate = Objects.requireNonNull(delegate); } @@ -41,7 +47,7 @@ public RestrictedTrustManager createTrustManager() { try { final X509ExtendedTrustManager delegateTrustManager = delegate.createTrustManager(); final CertificateTrustRestrictions trustGroupConfig = readTrustGroup(groupConfigPath); - return new RestrictedTrustManager(delegateTrustManager, trustGroupConfig); + return new RestrictedTrustManager(delegateTrustManager, trustGroupConfig, configuredX509Fields); } catch (IOException e) { throw new ElasticsearchException("failed to initialize TrustManager for {}", e, toString()); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManager.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManager.java index 7c387017bcb6..3b93dc7ed429 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManager.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManager.java @@ -17,7 +17,9 @@ import java.security.cert.X509Certificate; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -28,26 +30,39 @@ import javax.net.ssl.X509ExtendedTrustManager; import static org.elasticsearch.core.Strings.format; +import static org.elasticsearch.xpack.core.ssl.RestrictedTrustConfig.SAN_DNS; +import static org.elasticsearch.xpack.core.ssl.RestrictedTrustConfig.SAN_OTHER_COMMON; /** * An X509 trust manager that only trusts connections from a restricted set of predefined network entities (nodes, clients, etc). - * The trusted entities are defined as a list of predicates on {@link CertificateTrustRestrictions} that are applied to the - * common-names of the certificate. - * The common-names are read as subject-alternative-names with type 'Other' and a 'cn' OID. - * The underlying certificate validation is delegated to another TrustManager. + * The trusted entities are defined as a list of predicates on {@link CertificateTrustRestrictions} that built from the + * configured restricted trust file. The values in the restricted trust file are compared to value(s) read from the X509 certificate. + * If the value(s) read from the X509 certificate match values configured in restricted trust file then restricted trust is established. + * If there is no match, then restricted trust is not established and the connection should be terminated. Restricted trust should be used + * in conjunction with additional trust models and is intended to restrict, not provide trust. + * The values read from the X509 certificate are configurable and the following are supported: + *

    + *
  • subjectAltName.otherName.commonName
  • + *
  • subjectAltName.dnsName
  • + *
+ * see also: {@link RestrictedTrustConfig} */ public final class RestrictedTrustManager extends X509ExtendedTrustManager { private static final Logger logger = LogManager.getLogger(RestrictedTrustManager.class); private static final String CN_OID = "2.5.4.3"; private static final int SAN_CODE_OTHERNAME = 0; + private static final int SAN_CODE_DNS = 2; private final X509ExtendedTrustManager delegate; private final CertificateTrustRestrictions trustRestrictions; + private final Set x509Fields; - public RestrictedTrustManager(X509ExtendedTrustManager delegate, CertificateTrustRestrictions restrictions) { + public RestrictedTrustManager(X509ExtendedTrustManager delegate, CertificateTrustRestrictions restrictions, Set x509Fields) { this.delegate = delegate; this.trustRestrictions = restrictions; + this.x509Fields = x509Fields.stream().map(s -> s.toLowerCase(Locale.ROOT)).collect(Collectors.toSet()); logger.debug("Configured with trust restrictions: [{}]", restrictions); + logger.debug("Configured with x509 fields: [{}]", x509Fields); } @Override @@ -96,28 +111,32 @@ private void verifyTrust(X509Certificate[] chain) throws CertificateException { throw new CertificateException("No certificate presented"); } final X509Certificate certificate = chain[0]; - Set names = readCommonNames(certificate); - if (verifyCertificateNames(names)) { + Set values = readX509Certificate(certificate); + if (verifyCertificateNames(values)) { logger.debug( () -> format( - "Trusting certificate [%s] [%s] with common-names [%s]", + "Trusting certificate [%s] [%s] with fields [%s] with values [%s]", certificate.getSubjectX500Principal(), certificate.getSerialNumber().toString(16), - names + x509Fields, + values ) ); } else { logger.info( - "Rejecting certificate [{}] [{}] with common-names [{}]", + "Rejecting certificate [{}] [{}] for fields [{}] with values [{}]", certificate.getSubjectX500Principal(), certificate.getSerialNumber().toString(16), - names + x509Fields, + values ); throw new CertificateException( "Certificate for " + certificate.getSubjectX500Principal() - + " with common-names " - + names + + " with fields " + + x509Fields + + " with values " + + values + " does not match the trusted names " + trustRestrictions.getTrustedNames() ); @@ -135,13 +154,28 @@ private boolean verifyCertificateNames(Set names) { return false; } - private static Set readCommonNames(X509Certificate certificate) throws CertificateParsingException { - return getSubjectAlternativeNames(certificate).stream() - .filter(pair -> ((Integer) pair.get(0)).intValue() == SAN_CODE_OTHERNAME) - .map(pair -> pair.get(1)) - .map(value -> decodeDerValue((byte[]) value, certificate)) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); + private Set readX509Certificate(X509Certificate certificate) throws CertificateParsingException { + Collection> sans = getSubjectAlternativeNames(certificate); + Set values = new HashSet<>(); + if (x509Fields.contains(SAN_DNS.toLowerCase(Locale.ROOT))) { + Set dnsNames = sans.stream() + .filter(pair -> ((Integer) pair.get(0)).intValue() == SAN_CODE_DNS) + .map(pair -> pair.get(1)) + .map(Object::toString) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + values.addAll(dnsNames); + } + if (x509Fields.contains(SAN_OTHER_COMMON.toLowerCase(Locale.ROOT))) { + Set otherNames = getSubjectAlternativeNames(certificate).stream() + .filter(pair -> ((Integer) pair.get(0)).intValue() == SAN_CODE_OTHERNAME) + .map(pair -> pair.get(1)) + .map(value -> decodeDerValue((byte[]) value, certificate)) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + values.addAll(otherNames); + } + return values; } /** diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationSettings.java index 545fc0147047..5fc7f0eb2709 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationSettings.java @@ -12,6 +12,7 @@ import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.ssl.SslClientAuthenticationMode; +import org.elasticsearch.common.ssl.SslConfigException; import org.elasticsearch.common.ssl.SslConfigurationKeys; import org.elasticsearch.common.ssl.SslVerificationMode; import org.elasticsearch.common.util.CollectionUtils; @@ -43,6 +44,7 @@ public class SSLConfigurationSettings { final Setting truststoreAlgorithm; final Setting> truststoreType; final Setting> trustRestrictionsPath; + final Setting> trustRestrictionsX509Fields; final Setting> caPaths; final Setting> clientAuth; final Setting> verificationMode; @@ -143,16 +145,43 @@ public class SSLConfigurationSettings { TRUST_STORE_TYPE_TEMPLATE ); - private static final Function>> TRUST_RESTRICTIONS_TEMPLATE = key -> new Setting<>( + private static final Function>> TRUST_RESTRICTIONS_PATH_TEMPLATE = key -> new Setting<>( key, s -> null, Optional::ofNullable, Property.NodeScope, Property.Filtered ); - private static final SslSetting> TRUST_RESTRICTIONS = SslSetting.setting( + private static final SslSetting> TRUST_RESTRICTIONS_PATH = SslSetting.setting( "trust_restrictions.path", - TRUST_RESTRICTIONS_TEMPLATE + TRUST_RESTRICTIONS_PATH_TEMPLATE + ); + + public static final Function>> TRUST_RESTRICTIONS_X509_FIELDS_TEMPLATE = key -> Setting.listSetting( + key, + List.of("subjectAltName.otherName.commonName"), + s -> { + RestrictedTrustConfig.SUPPORTED_X_509_FIELDS.stream() + .filter(v -> v.equalsIgnoreCase(s)) + .findAny() + .ifPresentOrElse(v -> {}, () -> { + throw new SslConfigException( + s + + " is not a supported x509 field for trust restrictions. " + + "Recognised values are [" + + String.join(",", RestrictedTrustConfig.SUPPORTED_X_509_FIELDS) + + "]" + ); + }); + return s; + }, + Property.NodeScope, + Property.Filtered + ); + + public static final SslSetting> TRUST_RESTRICTIONS_X509_FIELDS = SslSetting.setting( + "trust_restrictions.x509_fields", + TRUST_RESTRICTIONS_X509_FIELDS_TEMPLATE ); private static final SslSetting LEGACY_KEY_PASSWORD = SslSetting.setting( @@ -228,7 +257,8 @@ private SSLConfigurationSettings(String prefix, boolean acceptNonSecurePasswords truststorePassword = TRUSTSTORE_PASSWORD.withPrefix(prefix); truststoreAlgorithm = TRUSTSTORE_ALGORITHM.withPrefix(prefix); truststoreType = TRUSTSTORE_TYPE.withPrefix(prefix); - trustRestrictionsPath = TRUST_RESTRICTIONS.withPrefix(prefix); + trustRestrictionsPath = TRUST_RESTRICTIONS_PATH.withPrefix(prefix); + trustRestrictionsX509Fields = TRUST_RESTRICTIONS_X509_FIELDS.withPrefix(prefix); caPaths = CERT_AUTH_PATH.withPrefix(prefix); clientAuth = CLIENT_AUTH_SETTING.withPrefix(prefix); verificationMode = VERIFICATION_MODE.withPrefix(prefix); @@ -241,6 +271,7 @@ private SSLConfigurationSettings(String prefix, boolean acceptNonSecurePasswords truststoreAlgorithm, truststoreType, trustRestrictionsPath, + trustRestrictionsX509Fields, caPaths, clientAuth, verificationMode @@ -304,7 +335,8 @@ private static Collection> settings() { TRUSTSTORE_ALGORITHM, KEY_STORE_TYPE, TRUSTSTORE_TYPE, - TRUST_RESTRICTIONS, + TRUST_RESTRICTIONS_PATH, + TRUST_RESTRICTIONS_X509_FIELDS, KEY_PATH, LEGACY_KEY_PASSWORD, KEY_PASSWORD, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SslSettingsLoader.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SslSettingsLoader.java index 3c851dbb9da9..820107f78976 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SslSettingsLoader.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SslSettingsLoader.java @@ -25,9 +25,13 @@ import java.security.KeyStore; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +import static org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings.TRUST_RESTRICTIONS_X509_FIELDS; +import static org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings.TRUST_RESTRICTIONS_X509_FIELDS_TEMPLATE; + /** * A configuration loader for SSL Settings */ @@ -117,7 +121,16 @@ protected SslTrustConfig buildTrustConfig(Path basePath, SslVerificationMode ver if (trustRestrictions == null) { return trustConfig; } - return new RestrictedTrustConfig(trustRestrictions, trustConfig); + return new RestrictedTrustConfig( + trustRestrictions, + Set.copyOf( + super.resolveList( + TRUST_RESTRICTIONS_X509_FIELDS.rawSetting().getKey(), + TRUST_RESTRICTIONS_X509_FIELDS_TEMPLATE.apply("").getDefault(settings) + ) + ), + trustConfig + ); } public SslConfiguration load(Environment env) { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustConfigTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustConfigTests.java index 090ad015620f..bbd5cce7e757 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustConfigTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustConfigTests.java @@ -19,9 +19,12 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Set; import javax.net.ssl.X509ExtendedTrustManager; +import static org.elasticsearch.xpack.core.ssl.RestrictedTrustConfig.SAN_OTHER_COMMON; + public class RestrictedTrustConfigTests extends ESTestCase { public void testDelegationOfFilesToMonitor() throws Exception { @@ -68,7 +71,7 @@ public int hashCode() { } }; - final RestrictedTrustConfig restrictedTrustConfig = new RestrictedTrustConfig(groupConfigPath, delegate); + final RestrictedTrustConfig restrictedTrustConfig = new RestrictedTrustConfig(groupConfigPath, Set.of(SAN_OTHER_COMMON), delegate); Collection filesToMonitor = restrictedTrustConfig.getDependentFiles(); List expectedPathList = new ArrayList<>(otherFiles); expectedPathList.add(groupConfigPath); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManagerTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManagerTests.java index 1dd71e730c0d..f933216bd853 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManagerTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManagerTests.java @@ -28,17 +28,22 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.net.ssl.X509ExtendedTrustManager; +import static org.elasticsearch.xpack.core.ssl.RestrictedTrustConfig.SAN_DNS; +import static org.elasticsearch.xpack.core.ssl.RestrictedTrustConfig.SAN_OTHER_COMMON; + public class RestrictedTrustManagerTests extends ESTestCase { private X509ExtendedTrustManager baseTrustManager; private Map certificates; private int numberOfClusters; private int numberOfNodes; + private List fields; @Before public void readCertificates() throws GeneralSecurityException, IOException { @@ -92,6 +97,32 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO numberOfClusters = scaledRandomIntBetween(2, 8); numberOfNodes = scaledRandomIntBetween(2, 8); + fields = randomNonEmptySubsetOf(Set.of(SAN_OTHER_COMMON, SAN_DNS)); + } + + public void testTrustsOnlyNameDns() throws Exception { + final Path cert = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode_updated.crt"); + baseTrustManager = CertParsingUtils.getTrustManagerFromPEM(List.of(cert)); + X509Certificate[] certs = CertParsingUtils.readX509Certificates(Collections.singletonList(cert)); + assertTrue(certs[0].getSubjectAlternativeNames().stream().filter(pair -> (Integer) pair.get(0) == 0).findAny().isEmpty()); + certificates.put("onlyDns", certs); + List validDnsNames = randomNonEmptySubsetOf( + List.of("localhost", "localhost.localdomain", "localhost4", "localhost4.localdomain4", "localhost6", "localhost6.localdomain6") + ); + final CertificateTrustRestrictions restrictions = new CertificateTrustRestrictions(validDnsNames); + final RestrictedTrustManager trustManager = new RestrictedTrustManager(baseTrustManager, restrictions, Set.of(SAN_DNS)); + assertTrusted(trustManager, "onlyDns"); + } + + public void testTrustsOnlyNameOther() throws Exception { + final Path cert = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/trusted.crt"); + baseTrustManager = CertParsingUtils.getTrustManagerFromPEM(List.of(cert)); + X509Certificate[] certs = CertParsingUtils.readX509Certificates(Collections.singletonList(cert)); + assertTrue(certs[0].getSubjectAlternativeNames().stream().filter(pair -> (Integer) pair.get(0) == 2).findAny().isEmpty()); + certificates.put("onlyOtherName", certs); + final CertificateTrustRestrictions restrictions = new CertificateTrustRestrictions(List.of("node.trusted")); + final RestrictedTrustManager trustManager = new RestrictedTrustManager(baseTrustManager, restrictions, Set.of(SAN_OTHER_COMMON)); + assertTrusted(trustManager, "onlyOtherName"); } public void testTrustsExplicitCertificateName() throws Exception { @@ -101,7 +132,7 @@ public void testTrustsExplicitCertificateName() throws Exception { trustedNames.add("node" + node + ".cluster" + trustedCluster + ".elasticsearch"); } final CertificateTrustRestrictions restrictions = new CertificateTrustRestrictions(trustedNames); - final RestrictedTrustManager trustManager = new RestrictedTrustManager(baseTrustManager, restrictions); + final RestrictedTrustManager trustManager = new RestrictedTrustManager(baseTrustManager, restrictions, Set.copyOf(fields)); assertSingleClusterIsTrusted(trustedCluster, trustManager, trustedNames); } @@ -109,7 +140,7 @@ public void testTrustsWildcardCertificateName() throws Exception { final int trustedCluster = randomIntBetween(1, numberOfClusters); final List trustedNames = Collections.singletonList("*.cluster" + trustedCluster + ".elasticsearch"); final CertificateTrustRestrictions restrictions = new CertificateTrustRestrictions(trustedNames); - final RestrictedTrustManager trustManager = new RestrictedTrustManager(baseTrustManager, restrictions); + final RestrictedTrustManager trustManager = new RestrictedTrustManager(baseTrustManager, restrictions, Set.copyOf(fields)); assertSingleClusterIsTrusted(trustedCluster, trustManager, trustedNames); } @@ -117,7 +148,7 @@ public void testTrustWithRegexCertificateName() throws Exception { final int trustedNode = randomIntBetween(1, numberOfNodes); final List trustedNames = Collections.singletonList("/node" + trustedNode + ".cluster[0-9].elasticsearch/"); final CertificateTrustRestrictions restrictions = new CertificateTrustRestrictions(trustedNames); - final RestrictedTrustManager trustManager = new RestrictedTrustManager(baseTrustManager, restrictions); + final RestrictedTrustManager trustManager = new RestrictedTrustManager(baseTrustManager, restrictions, Set.copyOf(fields)); for (int cluster = 1; cluster <= numberOfClusters; cluster++) { for (int node = 1; node <= numberOfNodes; node++) { if (node == trustedNode) { @@ -131,7 +162,7 @@ public void testTrustWithRegexCertificateName() throws Exception { public void testThatDelegateTrustManagerIsRespected() throws Exception { final CertificateTrustRestrictions restrictions = new CertificateTrustRestrictions(Collections.singletonList("*.elasticsearch")); - final RestrictedTrustManager trustManager = new RestrictedTrustManager(baseTrustManager, restrictions); + final RestrictedTrustManager trustManager = new RestrictedTrustManager(baseTrustManager, restrictions, Set.copyOf(fields)); for (String cert : certificates.keySet()) { if (cert.endsWith("/ca")) { assertTrusted(trustManager, cert); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationSettingsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationSettingsTests.java index 305ad60217a7..0772eaeaca20 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationSettingsTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationSettingsTests.java @@ -12,10 +12,13 @@ import org.elasticsearch.test.ESTestCase; import java.util.Arrays; +import java.util.List; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.TrustManagerFactory; +import static org.elasticsearch.test.TestMatchers.throwableWithMessage; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.startsWith; @@ -73,6 +76,32 @@ public void testParseProtocolsListWithPrefix() { assertThat(ssl.supportedProtocols.get(settings), is(Arrays.asList("SSLv3", "SSLv2Hello", "SSLv2"))); } + public void testParseTrustRestrictionsListWithPrefix() { + final SSLConfigurationSettings ssl = SSLConfigurationSettings.withPrefix("ssl.", true); + assertThat(ssl.trustRestrictionsX509Fields.match("ssl.trust_restrictions.x509_fields"), is(true)); + + // explicit configuration + Settings settings = Settings.builder() + .putList("ssl.trust_restrictions.x509_fields", "subjectAltName.otherName.commonName", "subjectAltName.dnsName") + .build(); + assertThat( + ssl.trustRestrictionsX509Fields.get(settings), + is(Arrays.asList("subjectAltName.otherName.commonName", "subjectAltName.dnsName")) + ); + + // implicit configuration + settings = Settings.builder().build(); + assertThat(ssl.trustRestrictionsX509Fields.get(settings), is(Arrays.asList("subjectAltName.otherName.commonName"))); + + // invalid configuration + final Settings invalid = Settings.builder().putList("ssl.trust_restrictions.x509_fields", "foo.bar").build(); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> ssl.trustRestrictionsX509Fields.get(invalid)); + assertThat(e.getCause(), throwableWithMessage(containsString("foo.bar is not a supported x509 field for trust restrictions."))); + assertThat(e.getCause(), throwableWithMessage(containsString("Recognised values are"))); + assertThat(e.getCause(), throwableWithMessage(containsString("subjectAltName.otherName.commonName"))); + assertThat(e.getCause(), throwableWithMessage(containsString("subjectAltName.dnsName"))); + } + public void testEmptySettingsParsesToDefaults() { final SSLConfigurationSettings ssl = SSLConfigurationSettings.withoutPrefix(true); final Settings settings = Settings.EMPTY; @@ -93,6 +122,7 @@ public void testEmptySettingsParsesToDefaults() { assertThat(ssl.truststorePassword.exists(settings), is(false)); assertThat(ssl.truststorePath.get(settings).isPresent(), is(false)); assertThat(ssl.trustRestrictionsPath.get(settings).isPresent(), is(false)); + assertThat(ssl.trustRestrictionsX509Fields.get(settings), is(List.of("subjectAltName.otherName.commonName"))); assertThat(ssl.verificationMode.get(settings).isPresent(), is(false)); } diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c1.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c1.crt index 532b19692cf8..28b34827f64b 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c1.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c1.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAOycKYSPiBuHXuHsqaE5KyTLYTqlMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxM1oXDTQ1MDkw -NDEzMjIxM1owEDEOMAwGA1UEAxMFbjEuYzEwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAM/q6yVK17PHtdsO5pM6DNU6pnOY/FQO+c1JpD2cpOk6B8yokEtFR+a0 -QsdsqQewAGBG77u9jQVerJr6fkPW+AeJT7eEBl5rqYDx82XgeJS6dAJRvclrxsOL -BDDWsImDIMes0AZaE54P6LDGBooH3XhidTyFj2Gp9fozVY8PWFl5AgMBAAGjfzB9 -MB0GA1UdDgQWBBRLyFFKNa/ll8mohWv8TJyzCzyOmjAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMS5jbHVz -dGVyMS5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AFmuRJmgPi2dwMwhfzGdDB6d+FLxQOcWSqj10e7Iq+bEydUjYZubp1K0/9dbUB5o -9iUvvFbZirwhRhcYjB5s/sEJavXEOXQshU8zek+jBuO+uKdndmiA5oGbDcKAEcbH -aPbq4eJcLAT7RJWlW66nCZNvJNnSh+DmV2w9XUZBG2ryOlq5OYCmLJ57TBKgvK3C -6gOECdImj2mvZ86xBmjZXZ6JVK0ZbnClIFt+3b+R6l/IUkMf6t0VVN/cs4AB7bTX -c1E4L4WHUmO7enzMZi1LyOWs7BfdJjjx/YnsVyh6PqcJTt7896ohbnfORmtuMZNb -eO5el0aBOXZaJb/MiUC6E5I= +MIIDUDCCAjigAwIBAgIUYEfQjQ92+JVhB2oUCAsgYfZNyQwwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE0WhcNNDIxMTIx +MjEyMzE0WjAQMQ4wDAYDVQQDEwVuMS5jMTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAK7bV6N225wvhfQWP4JuX54H5L10N+OO952cdU57jcCdtyAIHt74 +XzvpHxnIvc7OcjfMG90iD8H60EIeXr1PrYqY7Jwra9KHAdxtqQzvFKKzKS+rrq8n +v6B5rmTCr2MofYf9GqG76JVHCqSUZcNqbwaE9ZA8c6pPrHOCdpL0Wukk0TxIwE5K +5oCLp0k5FB1QlOnfS4iJ0B4OkHo8t2cte13LPxAq/ip125aTCZD6wErS4HU04NUI +fM9kqCj2JvMDcRBALsITtCTLPgZrFzRyOoe2IomIrpgCpyX38mlCkEKfUImZ84yw +ECQYtXfNr08qtIqAxPKJXSDU4OHlH9QU+O8CAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +eAvx39OQ/kxxF3cxHvBTj/+sR7IwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTEuY2x1c3RlcjEuZWxhc3Rp +Y3NlYXJjaIIcbm9kZTEuY2x1c3RlcjEuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQCjCq8Pw9Uo/13KhyhowLdjavXYJQ7LtYMlymDY +as5ikj9Op2i5SnfGddH+8R9bhGTGhFaSL4DeDPqeiMNiliG89G7zV101r+Xv4nqX +wYUuOSef09UQqR0iHgGFL2jSJiJ3YPNsC5t1SSyGGicMyzt1iwFu/dRktJjhJXuF +FvxFBLLofh3g5xJPsS6M0h8HC2mI5nOZ7J2o2EkFsiKg/ryJ0BfAHX58lXGrSrnL +93St6aQ1xNBOCiNf/TfrTLO2hajDWXeVh2qXMT+KbXbcAC4EIbXE1hB1Zx8Ebl+o +4+G6mqRSGO8zJmrqLqxdf2hXRDanMgL1dlea5bFJ7d/LALOU -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c1.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c1.key new file mode 100644 index 000000000000..8c7b981da54c --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEArttXo3bbnC+F9BY/gm5fngfkvXQ34473nZx1TnuNwJ23IAge +3vhfO+kfGci9zs5yN8wb3SIPwfrQQh5evU+tipjsnCtr0ocB3G2pDO8UorMpL6uu +rye/oHmuZMKvYyh9h/0aobvolUcKpJRlw2pvBoT1kDxzqk+sc4J2kvRa6STRPEjA +TkrmgIunSTkUHVCU6d9LiInQHg6Qejy3Zy17Xcs/ECr+KnXblpMJkPrAStLgdTTg +1Qh8z2SoKPYm8wNxEEAuwhO0JMs+BmsXNHI6h7YiiYiumAKnJffyaUKQQp9QiZnz +jLAQJBi1d82vTyq0ioDE8oldINTg4eUf1BT47wIDAQABAoIBABBpKqj/WJjTMasZ +GXMoNmt9CdVUHoNfdtLr4mtFbPuVHHZHp39oEnwWIuKcdLVU/+LyZ50R+3uftW23 +LvoX4SLpu685OLXku40mHPXHTImr704iOqiwE4FABQOU7WQM4qHhjzR7D9b5eMC9 +4sJ770P5NO40FkD3mFOrp22fEcHhbc6A795Qi/xUF3+vgxGcLuPbY8yMqyd467R4 +qLg4mNfGYNaAGcZqOO6dhrXzdX4zXc0PZNLP/vlNxKoY0UCVGJIySvTC8OrE3v6R +PwtvRhgfb2Fy3n4ZN5DjrAXJJvPqiXEsRQxibzYF8xJL1P2mGUnyhqfOd5YtzorN +BZ4GmWUCgYEAy3BgQ2hDwjzf/b1ywcux/G5ewP7+IiJNBU2flGLnAnfUIr2aqsdx +I5FYShgwe6sMDZLeGthR0O2K2tpuhqfZGdeD7wBi4Bq7da/MactEqBjse9JDyo1O +1vSKMkppYwxya8tVHaLPJa9bc0uGm/j4rRO3FkxRF4+MsAQ8+/aBmVsCgYEA3AiE +oc4rSIxyjzPfPBFRxXcnrp4kYhxMz9o2DI26LmHheGRPe8ytxfaL0VsNXo+UC/2H +nqd9Tn6TfFU+y/iv4x3I7hMI/JbxCrm1OyCtCkaoUfJsDRLrrogZpXNWXiIvI9aC +yQaR0bw6vzYONcyWecBsBnDJu4N5s/cuaSzjXv0CgYBM3z3Autk/wDjzbG68g/fR +ZvMvAiuNQmDQLDCspWKdMCJcrkCPX6WExxsRhGptkIiRaHm8sxjXVasuX3N8Nlh2 +x1T3LrIt9Gv5YGBhaYmb6y8Q6rbrBtJPrz+5GR0RbFagHMkg1bBokQaBXnyS21U8 +MULxjgzmVjvXFwLkQpVeGQKBgFtpYojCWXMTfkiGGVkgLOOnpCHEa4MjFLpJgqjm +fJHmckCNFARvvUYALog0lrJRXgU5OA2usb1ZkrZVx/f/gjsN1bsqx1IA93UoP+Wn +ppwdTLJ2z3MDCE0DF/yHlTsYYpM1/DG9+/ixhYqIz5CERYOhcH+gff7cGUSuDWb+ +khuVAoGAFCieBT66xG+4ktuB6cmH1GKVfTY71Zj5K43CE3X+PDaSwtlZKBMZzx/X +HSWRZndTdSdekIv6tE8zSxwuNMRCv/+I0RbP+fTBv4ICOH9Jrb0Q9Iy3N0HURLPn +C0BNpXcWLOlksQbQ3CcAEXOqhIB3KIyz/6DClLvNLBPEhiRbwNo= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c2.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c2.crt index 680547f5beed..7fbd6cbf84df 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c2.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c2.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUV4XHn9tdtxmUFg8gitI7WNN145EwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0 -MTMyMjE0WjAQMQ4wDAYDVQQDEwVuMS5jMjCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAlC1TOrM0KvZxZ40BCef2HCQOGsFI/IUmFCVno62Rq/DZnQbpqs9eyBjD -dJ9lMET4arkqJ1Vvg0rodZWCg8TOU3+Wr1xV1u2v4oBTdwyfoMrZxKW3NtMGQRdr -suTkfQm32ouq4eJ8Xcf6ziCwApuoIKDPRZipu+9Jqmywqw45a1kCAwEAAaN/MH0w -HQYDVR0OBBYEFG4GgmhNwmrhBpwtZAMtv3bsH2PrMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGUxLmNsdXN0 -ZXIyLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -ZNWqkJ8Fl5DwSRgQfUAjOjEY7+PCAvX2qKY8I9Z3DiB5D8xFeIP0qrz78oN8K8JY -iJXFPaKFpYihejHywSK7/A+myvZlOUDbPx5rULtsYVeK0e7n2x8htTVD5E6M0nRP -Jdb9WittiGe4muO3e17wdwSLEOrRfp+YmBnRy+7Q9BOix4v5y6HWpYwGdX5Wnj8N -devs0ceL9Ymtdd6lSEmDQRxBGQ3xXfBU4+Wl0Ec786OjxfsE30hBRFH9S/eQwfE/ -vGxB9Bo49I1Jar9DRMxnWjdPuoJptvrFHeByejovBeGfjOmXDpl/+eNk6JcnXjH2 -9UTKG8M6OF9J2SVOWCzU4g== +MIIDUDCCAjigAwIBAgIUcyiHMSNG1nFi/Vd1b5ZdP7frbmkwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE0WhcNNDIxMTIx +MjEyMzE0WjAQMQ4wDAYDVQQDEwVuMS5jMjCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAI146YTocHeUH0uaiLFjSrxlvCtWJGBTmYqpxY4b0K0p/ZXmQBRU +BBdZ+EUpRx/IgQftzQPQ4OyvS13+ozBLoTwXX/+IgvVx25tu3gfuHphw3bpK18uL +YPRCM9gMXWV3Ffynws/DftH443YSsDYEMn1/yVobASdNhl5p7AIPfGQoSVYRbexf ++cErQ0zMbXpcdF0uf5iMpqcloPMp4XgqXwsZ5CCjE0JuUYshcU+4G/kAZ0AXDVHW +Z6CxQsrcC/PC0AgJgk6fITHHnu+wU2AcSC/9Nz8IaM/CeIuLvnIYMhmf2GZt/kgc +4gFASV5m3SJEJOdR6gB1fyt3emUYYoT3uMkCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +z9mVG2EsrlH6LJGbpSzRvjxrAaQwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTEuY2x1c3RlcjIuZWxhc3Rp +Y3NlYXJjaIIcbm9kZTEuY2x1c3RlcjIuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQBuYovGH3pKwxCgNxbwX8PRVHoXIQyxZx2+qY0b +BMnpWByGMzr72QdflvFm+OvIqthkt9kHKIA1rIGlB0dwUpo7RNLotXg9qspngZ2v +Bz/OT+v6hk3g53Tv8FncNs4tn/n/7jdEi7j4Ky7WiKlXGHkQv5LuJOpXJRcEV7YG +dGOiNGqFqKYDt1w3YPi9UtHceebhJPkYzD7nJlEhqpozAcBeCr9VVqcgdIlMUMvo +H7Cuj14HoUoYOc+RemZ2jZREdHTRIQ5PXntSnFvnkibz8fDhswRAAqDxQ1iVh8hS +NbH2Ho+pZWLDj7H7KrG9PGtzxlA+bvMQfbGKTtDxnk/j/72P -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c2.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c2.key new file mode 100644 index 000000000000..ce7bf1476abc --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAjXjphOhwd5QfS5qIsWNKvGW8K1YkYFOZiqnFjhvQrSn9leZA +FFQEF1n4RSlHH8iBB+3NA9Dg7K9LXf6jMEuhPBdf/4iC9XHbm27eB+4emHDdukrX +y4tg9EIz2AxdZXcV/KfCz8N+0fjjdhKwNgQyfX/JWhsBJ02GXmnsAg98ZChJVhFt +7F/5wStDTMxtelx0XS5/mIympyWg8ynheCpfCxnkIKMTQm5RiyFxT7gb+QBnQBcN +UdZnoLFCytwL88LQCAmCTp8hMcee77BTYBxIL/03Pwhoz8J4i4u+chgyGZ/YZm3+ +SBziAUBJXmbdIkQk51HqAHV/K3d6ZRhihPe4yQIDAQABAoIBAA9bI4XWBncvBc1F +YzcAqcO/YJMXpkUqbeJ7lm0GyeTcnPZLaC3iB2ZS0WcNxExH4vGYYgYwWGnN2+bC +VWD6+e+1iH8KiKSEYlZXIhScR907dbswlTHhLs9J7FS0KuMbmlsICVZXSF8Znp8E +sknQCib4hZjukMvSV2vMyt2tDqR/AvvGeauDwkRkUmjEOkYDF+uoP3oZna4mPcLy +/BIAMQ3C2gnR+xOSZBfAgyil7l0T+ZeQbFM1/8ntvK9yn8961Dqm3qiOMKrN0+n1 +e+oZKtCpq1uouV3Uz/aaJpmQVwKVAO8LoNuD2e5P4JeL7iXlWbcM0ThI1EDK8MX7 +GHysrm0CgYEAuhyi42tPkCHskJB13ptiJbMqyOqU0NKbX/4Csm6eD14WulJ0POtk +iEsEKIZG9sGT1/HzagJNSWh9Y/RzQp8ubp11Iwzl+qd6db9fbgzURJ7bTHVOpv0x +AIx6zoIlRTddEOCbZvTbUzZWGsmOIDRnKp8a6E1brUv/ioHthXHqvocCgYEAwpj5 +E0FgJM8xyTZiIerjZd9s3OvZu+dskU9Vc04nbO9dKYRuyj2N93oII55Tg/5+CZHj +67rWXsuAtGoEYp6bIfEbkHjD0ZZEbRqgHrEVq9AFPBU8SxTKPV7Y/nyBLG2EsGrS +92ri1fUJ3bQHVAh5xnt3HSdx/iaPWzQhjNHM0i8CgYEAmzNeMsA+bTedC0EsyiJR +ypaKy1m0GCBJKdetcBcHAFG6Kk6wWE0EXRQRt6mibAwpuh8umeypftoAFua3Z4tP +MTdlg4EyF8CqQp8AecmBCmhfAHeiy2bBAnAjySqG4h21ImnrrgPrEo1xaI1EVF7e +2ZAs5CP+m5mQ1HRdGrdQxukCgYEAjOOmSdumWWAtoomeSQI/+2zk65pSvSnTv+0Z +Z+8oukUfRUTd1cz1MT0IEYm627Dw3crjorNWK7HZEEZFwIUmuk9Pbq0Q0XijN/7z ++OSrVQa6E4MocQ+vUPmPz8ii2WC9leDxtzKK4u+TYhHK0HuGdz7CwbfiX2jZl+XJ +eKQZT/8CgYA/sAlesy6Uc2hGRmre4o6Nnz51EWVA1AKBldpuY/wmr3GMvqMJWoFx +CKkf6SbSODAi8kTxtHx2SgWCly96z38QcphhlY8ClQg2jPjumtnmCkXCBcQJLcez +4gQ/MmRdAsu1PGb+4Yi6I4HCc3+3eF7/aoGpxo3Nf9IkR2gN+BJaTw== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c3.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c3.crt index a29a86e3f674..ca978d7ec4a1 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c3.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c3.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUGqZyGvYnT/TnwhpRix5jqfGzMHEwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0 -MTMyMjE0WjAQMQ4wDAYDVQQDEwVuMS5jMzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAhXKEuM1xYezN98jNJftpBGSAQ4jM0W4nrP86NhflKhCCNnp6PSh651t4 -4+v8tPo2ZwhaBAkchrfDtHKBuT//6PIteyEbtsj/ej2OztQ9XO6h4+3bL6ccVgfM -vm6YD1y6TjTM9fCvpbtqrBMouEOYkPPLA6lx23JyW5mJ3Lwwno0CAwEAAaN/MH0w -HQYDVR0OBBYEFKEfz2T5jwYqWOgbnZOJL2mLkqtAMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGUxLmNsdXN0 -ZXIzLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -Bd8aXS7DvR5mNVz7aSR1qoTBHKVLqIrUju5gKFsIrkfm0mEgvhx8T9m60k5h+sgD -TMaoHatMIVssOaNoxYwvKtnnuHKMvrkJLb/qyaiDfn0zO2g0jvWMtmaZnShrE+jj -la1hFKCkxblUgo1d0IQaONaDpgS2PXPu1Iah7h4MPB4BZdFNT5afnKro3zAfiKA7 -lMhGmTwPKTDwkOETNJQ4ETFjZzY2UxFjLsRzbrXDeto+UrU/qJa8jzCWI8cI9jFc -vffGGldbRWoUq0ALZSbz6W2psf/xAo6tSkaf0MDSMYRZ9mFVmWBSREITn+12gp7g -5A8wNJujWbyAR0j5GXXZAg== +MIIDUDCCAjigAwIBAgIUerW/NwMch3xS2m7baNp82ZJH8jowDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE1WhcNNDIxMTIx +MjEyMzE1WjAQMQ4wDAYDVQQDEwVuMS5jMzCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAKtV/1tTyEHagrRwCVYybv/K4k22CGzJWuUM9ptrP0khaBFviw5l +iUNik2zBwKLhVGCRh9+1hzfMMe7dpyRCgNlBSdUVqaaQR9VRY7X23XWDj4RfIKtM +V+cLBX8RiHfjh5gVeWqAJ7O+JibXI3pp1pnyfhg9oj3oRpTRVCyiOXU62uemDxam +0KC0EQWwJeXG86c32jy1v8CqYztEDYBdXwsJK/kgOXPLmDcFB/bwft3Shzcmboff +qHWEic7JNPa1uMG6kBgRhm59uNjyKII0HCPhrABV1PnnrcVLpeFznp4qYLKza8T8 +cis5rHgU275NM+u/2J1bt/f6bGntgXVjIrcCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +grh2iD1+KGmc/z4w1S+M0rrZY50wHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRYIcbm9kZTEuY2x1c3RlcjMuZWxhc3RpY3NlYXJjaKAl +BgNVBAOgHgwcbm9kZTEuY2x1c3RlcjMuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQAfpAHAPMaYP7ufwL7tnpvUbJmLxBRpHYSJPEBW +0TxD5pRkjuPie0meGR0QHFo3EWwSX+RVEzXoq+2Xe1E0cqvxRpiVgBndNI6adE7N +G6NZXJoe1UJBCqxMSGhNGAdakkbPPeNvVRyml1SxbiPmYtORAAzlZ846fIkH90wl +EyvwkM+Q0Ge3YJxajbl/+qZOsDzQpPZkX157cirLg8m8ojdDIw1Du7Q6M7Ea3tGu +KbHm53Yfh8fxLTRYu/uVNTg709we5B7hx8AL383QMCtfzLC5UinFcDuQlqqBHEkU +S+wbBCnc77lf7xxxHOXLir7qi5eQ9RtsBQHh3I+oqcpyQiiD -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c3.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c3.key new file mode 100644 index 000000000000..750979d85dd6 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c3.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAq1X/W1PIQdqCtHAJVjJu/8riTbYIbMla5Qz2m2s/SSFoEW+L +DmWJQ2KTbMHAouFUYJGH37WHN8wx7t2nJEKA2UFJ1RWpppBH1VFjtfbddYOPhF8g +q0xX5wsFfxGId+OHmBV5aoAns74mJtcjemnWmfJ+GD2iPehGlNFULKI5dTra56YP +FqbQoLQRBbAl5cbzpzfaPLW/wKpjO0QNgF1fCwkr+SA5c8uYNwUH9vB+3dKHNyZu +h9+odYSJzsk09rW4wbqQGBGGbn242PIogjQcI+GsAFXU+eetxUul4XOenipgsrNr +xPxyKzmseBTbvk0z67/YnVu39/psae2BdWMitwIDAQABAoIBAFDu8wxDc9Wzr8kn +ISnv8eHvht3ZjnpA1ShcasM4snDLkHqn+4JF8UR89JHLpkDqeq6RICNK/wAa+z1P +w6vLpEy72/IFZRmSjvQTmauzXKItjqYjP23bRqyTVrnS4Ols97id8DggKGDuAdyY +BKDHDQG0e97cgl6G8YxLo1zgFo8qgvarVkaAQ4ZEGiIvB8j1pA+AC4Hg1xWr7JXG +mL6i+bqYEufm5CeFXNg5MTAKvUpwheVXlAJnAeVhymJ1bYMbpL1y+KdkfDqGm8hH +2Q9jZROPRwW7PXsl1ajYcdZfjj+kNsyAlwEjm2C00Vxk0ogzLGROzIW4Jb3Ddvtc +pQTu6kUCgYEA5B4hBkCDsWt/tnxDS2ZJAxZg+UhxzxLUJa/vj3l9Lk3pupv92H9U +1TsNbUwmx150ncu+SL76xGlC5o1qbBNfW8yMR1n1m4au97D8xT9wM8J6C+NlKzmE +NqHSW/oHBMfo92yKuRx1tk1VWQ8zL0J1LhNt+NQVE4Cjw7yVvSIfvbMCgYEAwEcm +ap6N2Lo7uxV3VuDPntzoohDlM7oty3IOqDLpoYqFFQDIhd/ZIJbKb/uRdiBdiGM0 +K+d9oej9cBDRaUkCI/j3aFU15iilLXP9kDg2wOPtFJuw+XtceaamgzVUxYoB/3DC +fO8qr9+geY4AWYdxsT67C7rMiFv08hPoPQujbO0CgYEAsydubEiSpO52OM1S1I6a +XNBgjKb2qthwwMBx9k5Fn+4U2DfWnwcxit176BDci8uE7sCqytsiesrofcUmhYaZ +Z7E3onnmSKT4GRNpEUiHSD8+NsP6Gn5rdbSVTyMIDBssvdUDBepyECCgHPexDTa2 +bN9ZiQiXHN/0va8IMqfwIb0CgYBznBQ4NT9amZeChISt4xxTC8aAc6IsePxCAEFZ +1srtC2m7WC45eH+H/cKYlG1A1nMGp4deAqFiccG42RPgtzonQPIZdGqlCblPXBeQ +2IvrbOBG7Nn7F7ZhmtcxuyZOZA2Rdk4qam+DMfW+QKhrLGoqxWoXwmhqcMgVqeyQ +j+fifQKBgQDSxH4PN8BJGcm74pOtqy3KFAuA8AO26hW7r4GNEe+ficWIFZzN0C1c +ilMCwUJLig7k7QxwtarT+1kMIQBpyb7NV2emaV93k5D+G52KSTWjGiuwR0c4LImI +RwjYOqWYr7kzJergNipzjZF5ng0ZWe/NImFpFRyMPcTD+M962S6nww== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c4.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c4.crt index e1838cc5b4fd..3b213d1af469 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c4.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c4.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVANq892kwQ089Hnpd0M4dSCmHpvARMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjEuYzQwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAJVhOQxK4DAxYsFDcm42dl0DdpRCqxYFecP9QmC2cniv+sMUSRLsti8B -eOKawlWL+NQK7CJhAQPuYnLEBiyhUIFz9dAVozHyOqBKFbf2L3A2nIPuom22UiJN -79k2YXZgagSCdCdRX8WTvTRbbN8WYRfE/wLO4SBxwW2f501ET6pTAgMBAAGjfzB9 -MB0GA1UdDgQWBBQIGCed24kXamJPxK4inO7BWteawjAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMS5jbHVz -dGVyNC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AE2ckMOf2Nrh+TfHQwvZWIWhENeIih4A4ivgimfcGWumpCho4tusNu/RIh2DxVqy -4kJSqQ6NggGADDDPmh2p2UTa5NR2RF2FQGmfNtkFwp48tE/YXWurts0IneidoGYC -RXM+mVXTNGlPE0BrgwS8tSKRRXh+lsvS9HH1wkLUSa8tk4PWqln1RpttiHYnImqU -t4d38vpk0AfRMalEV5atb8eoNkKVOfrDiJ8/iA/zJ7qh3PUaU24taJtP1MNR2mo3 -aXZH1GCA+WUqWAUDN8TXHv9TABh8IggmpK39weV17BAKFHsVslq2auyDaFcmoJk1 -tLXpMUUJweWmFTRqQguKu8k= +MIIDUDCCAjigAwIBAgIUSM8isttfREy53csauHlJheZFo6owDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE1WhcNNDIxMTIx +MjEyMzE1WjAQMQ4wDAYDVQQDEwVuMS5jNDCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAKv8CweNke4mqGmNcnObtTN0667yWfN0nVWzvANEUZyAWlxRMYoM +9hfCPv9kOxEddM+/pZ9AZ1v36L85rKGquPsO+m2nnBGJtNV2gup0EeC7DTRTfPlY +YRbkItMfPzktDd6PAQDUV8FtgzTBlnVgCzWS7A7MLjSXUk/EOCYl5Nuzfxqf/PM+ +25zkwkdBlJPoogs0vnCJIcl+oAu/KXpa7FMxnG4LNdkhxtDXtW21RIK09ZFWsGeR +ZgQl8JYk9UJPjKwMr4Yn1W5BKZPEUIZOzhg0IjfoykfiZPaRFPwZ054pZcygsrds +mjXklhely6PSOhbncXjPsKqJuC5Cih+hfPMCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +JL313utz//DGTX5n4betjYFR7PUwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRYIcbm9kZTEuY2x1c3RlcjQuZWxhc3RpY3NlYXJjaKAl +BgNVBAOgHgwcbm9kZTEuY2x1c3RlcjQuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQA/1fqWtbsrOs7T9UnGBlM03a9kRVWaevA0NvwH +z11+JW5TlU2ZVsdQPUpWf+gcbh3geXVnbyGuYnbtpn3lR3lAapj/7i5UE9ZZOWwi +XZH3QZ9yc7lCLGNgHR5fx72a5EXLsD0EewlJ153fCIn6cRyZPOGZ1R9orqRQv4bx +/2UN8VUazDiQVenJlrjSP4EsZKrto2GqS7bxrvJF61yhty+CYtbqxzlEgQVkaj/0 +yN8nRA7kJ567duvdD7JNmP1H6+QL9Lj3EFx1y54BcEBXtjubh8tUFxdPG8PHhSs6 +TsHmQdoPeRJcJztemEtkHAK7QpbLGXfZBHn1DSPtScfXhekS -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c4.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c4.key new file mode 100644 index 000000000000..6468d6c3d889 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c4.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAq/wLB42R7iaoaY1yc5u1M3TrrvJZ83SdVbO8A0RRnIBaXFEx +igz2F8I+/2Q7ER10z7+ln0BnW/fovzmsoaq4+w76baecEYm01XaC6nQR4LsNNFN8 ++VhhFuQi0x8/OS0N3o8BANRXwW2DNMGWdWALNZLsDswuNJdST8Q4JiXk27N/Gp/8 +8z7bnOTCR0GUk+iiCzS+cIkhyX6gC78pelrsUzGcbgs12SHG0Ne1bbVEgrT1kVaw +Z5FmBCXwliT1Qk+MrAyvhifVbkEpk8RQhk7OGDQiN+jKR+Jk9pEU/BnTnillzKCy +t2yaNeSWF6XLo9I6FudxeM+wqom4LkKKH6F88wIDAQABAoIBACb4S3+Aoqp18/9G +38I2bA6m1aiknaE+sU/0FAwhdOSjaNY2R6ViXnjvuNqh2Yh9RjS01lCJhWIfgIuk +A+v2BUhOEoy6R/DpZSJhYjTB7DMh71IGAPF3BzjqpMF1+Rt5jAT8HlwTwXbA29Qe +cXtTMfcHvZl0xpuAFlSgv6BVQdBOMnvBiZWd5/JPx9vKb0ItUoJCxbYbYWQk0oZO +OuG2obU4tRgxb047aanBhfoqNqZnZXFFazC/Yl79+Yi4aRAwAabPGyBLgDIIH++C +IwlL5YDPW5VFM5WFDUi+WZJF8YDMONdu+WYhI+RrO9hvXa/Cr5g80VwTgkkGIp8B +xrvdQqECgYEA47xCTCbjQ4nRJc5LGd5+bxNn0XsyX2XXEALN2yfXb0Edy6AnGypc +TQ5i1pauuRjFBRNOQpGKyKFwSakDSmN1l3SwIIbkyLKrxGjiddh660zFB4qK2WlZ +mv8G21CyvfvBZhrMcD/1ue3AZoDf1rUSfx6wZ/Dhyg2Sq7s09IWd8EkCgYEAwVRv +mJv1lLEhBTCfZ2zCarEZKD7cpYE7q0mlH5IrKmUGS3Y9EQAGH7eTUbn1DNWCGES/ ++4nI6gYhP8pPQFKlGw8lotUVmWCFd4QlE8AEG2VCsIrxu90twLr33DkduAxLE/Bx +z227c+Pk4SguWrohARUIFcFa2G72x2eQo+G6e1sCgYEAwiS16DGmwOb2k+JAB/ez +lYHXmbALC2TIaO/bCp/2evPqMLOBYxlDwrUm8YVXwpgpLeB9cSaeR9fI4CLjmUEc ++FcT50bYN3iKElDP7mL0ty+lMH6dTCcRqp8K+2DaYDgFccQa0P7VvwG+aiQnruSC +tjg1bwbdleDoTtqzlFlsgXECgYBaR5/FWC8aPIz663XEylkPkZv520EWWH8QwddG +Iu/dYEfMPW7O3X/+l69H3tq8H4gc5R3xzKnx/uLP8x2HIBDRzgT5QSBQ+23YHfFi +g6xnTqYR4xB3dhXDhovwXwutwq/co8/B2bBLsgRT0Hu1Cm4XH0cjQaO2pjq/a3Fz +1hlLOQKBgQDHRJ3o/NQxHT3AjkIg//+/JyXWfkmcKWI7+pUMBhu17utJzvA/SAcA +H1/gMuwlNrg7KTfteFGDIgWkDsDBWrF1HTbftPbvEWn7S9/Qm5UVBAJBiBVpKgNp +kKhIUploX0oSvopmUkdPVk2oPqFgUL3CyvVyom9cdXvM6Rd2FBn2UA== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c5.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c5.crt index a844d630a2cd..8c5c3b941450 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c5.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c5.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAJ+D63xrhVvDF8SCF0IN6olyWI9hMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjEuYzUwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBALGUvDdOnTe62apHl5sXV6Ys8GYqKDJ0e1cCQCKgxu3MyCueXC1xdhYJ -ceA3PTmnTRg7KqYhJLZi1sujBOfuy7vsg5r/7L6EhWDCM/d2QfF9ZUft5ljsEGYN -OmGakQnU+mFOuDe18hlp72tavC2tdPYHaTmd0t2f4J9ovxQznO+jAgMBAAGjfzB9 -MB0GA1UdDgQWBBTgPsBAr0vFIhwmOhBkC1XvlAGtvjAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMS5jbHVz -dGVyNS5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AB6nwwv8Pvzmj+/qxqyUu+doqR/DNu7J2ZoVJ4Y/kbp8Sya6GBVocN/Yaj+5aTv3 -phegvSvXTuw38gUuadGHSJ0R+GgaZNQ99HgMGCaePsFSf1Qya3vnK0308n7MSr8F -4pDnJCmOeOZQkY0PrMHx5KW+BdNWU3bvGALQubVo+nJEuZYTBPt52AkweMbtubiS -kS3v/SKi/mD0aKWS8amgAlCAFct419gE7frWAsEDXmQU0KDaSE37yx+0CbvqzlCe -EZRRpgg6R1px1F0WPU1SkCfQyhTE8MMz8Wj1dL4PkCJn7ku5CRF65VgA48t6Ecpt -4UfKfyiO1rcbZKA+UKovHjI= +MIIDUDCCAjigAwIBAgIUcfjfdI64Fb8CJevbBBisnOZsuGIwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE1WhcNNDIxMTIx +MjEyMzE1WjAQMQ4wDAYDVQQDEwVuMS5jNTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAKIYqHbLrUjbQ8smJzK7O9f6qeLxNvirAiFR9WUIyT77SPqeCiWz +ezUmBCb8jyLprtG76HiXMPvCnDbtpsbN1RKcZG0j+T7c3TtkygJk3ycfuWffRN01 +KPDqqUB8G89CQ/2W1sQxzkl/FBLjQj/DPCHy+HyYuyfVk1aFH2zH0O8y6ghwD3q8 +ZLa50Psf8fAmVLbUxTiZcD+n6bliYrGPPIqEfgX39U8tUXWrJ2a+/ESJZUdU6Nyd +tNiM0q8GBc+2Rz7aOwnxP+uy2sdrVdBbocFF0LDwZFnJFlt3vkSrtaNp7cpkduc/ +P1sM/5tv5zL8hOdcw76gBLY8Qw+UaNU2n3kCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +RSkxevULQPv85eNElA6ZZ615LiwwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRYIcbm9kZTEuY2x1c3RlcjUuZWxhc3RpY3NlYXJjaKAl +BgNVBAOgHgwcbm9kZTEuY2x1c3RlcjUuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQBMj1+uT/fYnNzJgDV0Fa8QhJiF8ekc9LiRPi1x +XXC3TtvEMAVYfOMWOWsMqIe16rW+2vxplfrEMSkp3EcejB57fX6XFQ3M87QnFIvI +OcGBWrGm5u/0mGHo+erkWXH6niw9QU74rBSgEiuh2gkguKe1+3Kkrr4uZnMB80zC +R8fXskMgfP4em8VI7s5UY2pEpFanoSyGysyYPo9A9BlSmbx5R9VBySMsd1x1GweC +WsgGYghTKf6yvmuRtak49boOR+a1dxz9XttD0xgZ2KFjV2QE5aLkc4S5gjx1iDHe +JZgHKM80i63atwvdp1UI01uYSE2Fi6zbU7iwj75w3HeyjX9E -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c5.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c5.key new file mode 100644 index 000000000000..add0c0189cbc --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c5.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAohiodsutSNtDyyYnMrs71/qp4vE2+KsCIVH1ZQjJPvtI+p4K +JbN7NSYEJvyPIumu0bvoeJcw+8KcNu2mxs3VEpxkbSP5PtzdO2TKAmTfJx+5Z99E +3TUo8OqpQHwbz0JD/ZbWxDHOSX8UEuNCP8M8IfL4fJi7J9WTVoUfbMfQ7zLqCHAP +erxktrnQ+x/x8CZUttTFOJlwP6fpuWJisY88ioR+Bff1Ty1RdasnZr78RIllR1To +3J202IzSrwYFz7ZHPto7CfE/67Lax2tV0FuhwUXQsPBkWckWW3e+RKu1o2ntymR2 +5z8/Wwz/m2/nMvyE51zDvqAEtjxDD5Ro1TafeQIDAQABAoIBABaDMjJ9iy2JNlsq +i1siYedXKu0X7ziOF5elGF3V4I1cvQABnwspaD0zcHQxs3d32Q3L+Td2WBk/KA8A +5p7Jy4PwMIpv6pfueNNc5il69PssDsX8XkYSsgLztFn+YqZgX60CnAVXnXzbp2AQ +LA1LAjj808IKPEQGB5aGmpyzC7OMuA9FjURlojCkGG6EmACOjyFEZcxD37xXV4JD +XXDciF+K+kFu7n2gpB2GO9YJXin45Cr6AvbSx0urGMS2zX02MQNigi2tK+hxHuHS +Ci4Lo9VlPHRMYNq0l5DPW2AlbbcwqhcohUN7O831g4UNQMvyrgTuuWw2Mi71EnGq +ZQ7KT1kCgYEA0/sFii83tRDyjwYmVlhL0jIfxuXyIALCWzatV+tkJgCYis1FDDai ++Y+vlx0SwxLjVA4LYf8X5RVVtd7LIQypMa4JoCBOJbsvwtu0itQ1XaFcpgoL7POF +DDaiV7smD3p3dhcru/CLxwh63MgQsRfZ0BJJLSBK4qU0K0+ES3D3kAcCgYEAw8HE +2GE0vIV7Zo8v/Pz6TPjSuHClEDe/XcByVhabx/QWFokpQ/t7nNufYrdxCpSCSZdk +BduBQHpNbpu0DagA0KtvakAAUpByr+KwGVc5u4Rm5IUBda5zrUool1I0UD8n3tBX +toUPLgmE/OV2j1fYdJqqq2e5IwvvBYEJrosFdH8CgYEAqIp+JhdSuIEIChV+p8o+ +RpqZz1+GelMXSsSXYD8E+n4gysNYcdSDEd9fYcu43icHXg5omHrk+bxT0G2aneVe +JOTWPF1TJQbGe6yJyJPe9lnUuyNjxfr8vvA1dYGUSlw53Ueg+yXPTOl+HkpnkxZ+ +lMPlMJ7rS3bEmub3LbBc9UMCgYBTlNJFSQvJSnzrx0tIC4ObhcoC8iWoDQVqNBcd +Zr3Q3AjqSloSKrgWuzcMYSKkz8/pN/h+/7/qFc4l4dk403i1n90MUpNQrVgqfPGP +gEyoIdoEzD1ZT8kYPkeihHjNoomVewNYbOuUToA3somynmFPOBKS6NCFhDzj7rqB +id4kZQKBgQC7kcU0i8Q4zvFjLOU+c0MRcC+BL9Zj+FNwxyvWg4N/Kjc01Nodf8ck ++WdW5q12UTdbAf5yFHO5IeHhBTy4ioVRv4hQ3ZBk6t++knuWVER9SZef3hEGoDjz +Arxf64h8HUv0YTHxcYnSnu0DWu18YIHSMf5O2W0JYZqq2I5aAWaWmA== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c6.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c6.crt index 9e3093aae54c..8b7aee7deebc 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c6.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c6.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAJHJ9P/gVQNv5i8q8tW/UzufE1cdMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjEuYzYwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAJGKOOMmMfmt+9VRZfYsorzOWLxWRtqD9VjRimbBVWBoIFYf+EVyEC0E -/sg8G9eWAcSEUJ2oDp5dnMuXzEZrA3aaYeDZjtOYuHF+i+PFQQFlpY9A+NapdhJe -9CU6cvL4ImRyatPxw4A6QLLetWHbDDIYA+6iYtmkKTkMlY6bboSzAgMBAAGjfzB9 -MB0GA1UdDgQWBBRs7k4fmkFw8RmVU1f+p9SY5qNwnDAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMS5jbHVz -dGVyNi5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AKvKMX7eiwShOetFoe8CQ0jls5aJ2119gp917eq75XZE8HGJhiWlBA5qcCCxCrtR -Cy1BbuHiMqPni/7KaWosw41dxyysJOrsPGw7QYnhda+ji3mtFVxT0Zert2DPKkxR -Fgt2VWbkF/keMKDGc2vrpyq7YVc0RwDjTzEURSLxI4p+t/HbWQXLAIGRjVXneoUt -bc+I/2/C5chV2qTQd1xC6OfnixJ1234enIL+0enypLx/8Ca6i8+Gl13FpyFpO/ik -yY3Xe7S4rRAsvWpHRDWIxqA/M+fzhOb76nLC4H4TGkuAJ6EavsnaHHHOngbL8Ag0 -zFhXT0Vj0GucUzjx2pjFhfY= +MIIDUTCCAjmgAwIBAgIVAPWiIWzwBeBTxx67S5Q4inI1C6ASMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxNVoXDTQyMTEy +MTIxMjMxNVowEDEOMAwGA1UEAxMFbjEuYzYwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCnyRePFOVECgYCEEbWK/hBjwYt1s/Y1uAXwlR9GRFW9k0//xBA +qS+0BDp/htkEfchZNaXdYb3DvARY+j03F17haagXhQz18GHDntspbYUjo6KZ/HLb +iT086jiPfueMwszObgEI7OQHk19IdCkF3oodBQBerJk8owCfEumQhY9uH7MvNSHe +WNaUJhVtqV5lOF5szFUJStWOkY+E66GduL+/dSVKjQnB60SYoQrz72XwwDetmCpN +ZiBwrZhIGJZ4D9AQ2EaHgJCZ6rMeabdQxcciu2WsruJxKGxxsaJ+XdUH3mlNYJiX +4V39g7hrT9YIvTL8I0AnZ/zaMzIkJ8mRQjstAgMBAAGjgZ4wgZswHQYDVR0OBBYE +FMHhSPHivE+Usi8rs6yyBPRQudd4MB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWCHG5vZGUxLmNsdXN0ZXI2LmVsYXN0aWNzZWFyY2ig +JQYDVQQDoB4MHG5vZGUxLmNsdXN0ZXI2LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEAGSQSbldCPH7pMwHn/GP8tG9eM7W5uss9zU8h +c+hXxPA3eBB2564xnnzw+pH7DJ/2KSTiAlkjDq3CBO7ClKm1MUt/t9V5dGwAU/C8 +y5sZQ3QHsKkmwVo8EH/rfWjGUi8J2c99nlPeqAZdaPjVZZKiZzRSIDoKbxTEjuzh +jjfpqePNOgwpg1vA0fkRprCDgj1VLOvEXmw01ASvIzAF5bb5xJOkYdlhRdn2AwVw +7tvZ7WRfjPX2Riup2NJKk7YBVAGBu27JbjCuTTfYFlMMDFRzftioGjCPSEWOkNSE +VEFuDfhl6eOvxlfjaqOg1MR9iAcShFj5Zv8ve89uNJrxpAhBcg== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c6.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c6.key new file mode 100644 index 000000000000..d3a1c2b4c3ee --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c6.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAp8kXjxTlRAoGAhBG1iv4QY8GLdbP2NbgF8JUfRkRVvZNP/8Q +QKkvtAQ6f4bZBH3IWTWl3WG9w7wEWPo9Nxde4WmoF4UM9fBhw57bKW2FI6Oimfxy +24k9POo4j37njMLMzm4BCOzkB5NfSHQpBd6KHQUAXqyZPKMAnxLpkIWPbh+zLzUh +3ljWlCYVbaleZThebMxVCUrVjpGPhOuhnbi/v3UlSo0JwetEmKEK8+9l8MA3rZgq +TWYgcK2YSBiWeA/QENhGh4CQmeqzHmm3UMXHIrtlrK7icShscbGifl3VB95pTWCY +l+Fd/YO4a0/WCL0y/CNAJ2f82jMyJCfJkUI7LQIDAQABAoIBAEj/e2l73NeCSOTi +8f7COItFT5nn6JvduLd9i8a1fk7IcAmUzhxjv2ZhpJXRNF/43Y66gYRUvvwGtBmG +HdX3LAeEMWayM1ZZFB/I4G8gwBkmW1sFc29CQUtcV+lOfC1In1R8laStc/Q70Ouz +5hkwngRMJcIK88xm2qFa2BVRlnqBSacl6nudtBQAIpJnMTLnalFIzO4aGAPcHRRX +2uEPqMSk4CuMBtvVc5dOAeOQoheg0o3tNSEnu5CUF9h5QyGs+DwGgdp0XkWQp5V7 +HSqLI6ORDYYmPD/A9g1790e6RTGKvDYjDJIvcB1OY3iI6reDvQ04zlXnn3iCI2fM +5Ifqnt0CgYEA2W5qso41FHOxIVbhDde6256pEFarbx4+WFjb08d2MO3EoQNhXzM5 +O71oM+QkHKQs10u3Rje0i81xweAc72tWMg4K0oWwYo4izWJmk1U9RO2zRCAK5X11 +3Iu8NmgToUD6OIqQBa3DWGV01gw5YGux0UN0f/kpFDU+QF3/MPmViVMCgYEAxYxA +1fU20nYJYzfjR756zCk/xdEBVcZ9PmeSJl+R+RfeGf0GhzFhbE2Hdjvh0zjsPh+H +0O4+dmwzD9XuWaOK6+zaT65UlhHlUXJUXvboGX7Dy8JjH/VrYiZHjKU2IJnPyO1q +K1jWnT/wo173sDYafLqK2/QMYlhI/dwG9cxuGX8CgYAbRXokgymtMwWYhOkdb12E +hlX2Mqi1+zkeo6aO3Ym5gVkUSPRBmI8pKwyS1Z6h6QWmy1xldj8y+cW254E5lmDt +Tk2VMid5dRJwaFMtT5eruZSuisZTGnVaADzPJiawJxI7XUXLMIw73h3VaxsXOOrI +sIlAM+QvIK56LFABuIxE+QKBgQDDWbaj3iGgbelKnHG8+LgmPIQ78jsHv4kKtRDp +sYiAcwHHYVebSVG179UNLppabefwZu2/xlkuckDsX/mb2wX91/Lq6NHstTW4HP8i +CbfrvleQn2G2sNLH9GK8yoOEKZY4c86RnwBb8Gbep4L4VghLX40BhYzZk8RahcK8 +PrSBsQKBgBU9PQDais52wBvXJW51bih6tob0iWKOMmUAiSw5UPs/QXezEK5bGJFy +0MkKCqBK3DWnbSk8MWcFI7KBBRheUjO4aQ9w3YYWYgTXZgsUuU1PE11eOnbJkQf6 +u33lKFoAqVdk3MFfM7eNDmc3DllK5ueC1cf2ZfLrFPsMVuB0XSnB +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c7.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c7.crt index 5301576723ef..cfefc3ef028f 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c7.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c7.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAKo0NbCgIbl24/edkcXtukrKEYx6MA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjEuYzcwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBANHSoRZvMsCdrQREwlLHR8xFRPrlW/qC1VcbMDkwSdvFOrPh/ykpPn+I -kKrTQ/kOBCOruPSh7x9/5KB4EbkMJnIo+XQlLgLbAUeMGSS11IKpQTS7S6noyxde -7xU0Z4u/MYk0PS+QgYqDU2ExoY8UN7VGxQg9zSzy3dublTUfVXPbAgMBAAGjfzB9 -MB0GA1UdDgQWBBTxpLGy+DAZqPn1o0uRboFaeT0D8jAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMS5jbHVz -dGVyNy5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AGPN3G8zC9wT+FPDUJzR5aiXWkBN6Bn8pYaDZyWuWFQr4l0m57sX91vOSBBvXtYv -0gcz/Nggkz2TpMokfXBOoioeHywYIycy+9F9obbgKy/f5ejCGv8F26FS4UH4WVqs -dIHaWy77od7SUuBqjlOYBAK13GNZt/4rkWSNsry3p+YoVxmEoUW0Y9BtXDXY2M/U -ewTEbC1oZHXeyNzgI6Njw5d/hELKMNFwq2RON6tU/XHXABXCdzFkIjHvrM+N8a4N -ju2xbQALjSgPWjYprjCiYHW5z+aB+lAKbCCUcsrMQnE85Bx1DpUs1oAWYo77h7Xd -uAX6yQ0JODD/TpNGoWktcUQ= +MIIDUTCCAjmgAwIBAgIVAJUhyR6QNxUckaeIj/pRCKLuAQxsMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxNVoXDTQyMTEy +MTIxMjMxNVowEDEOMAwGA1UEAxMFbjEuYzcwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCtAxYfnr4JQm+PhChak9VoPngeIoiYy8g4dfzj1SmtTd+SMANU +KEFnfGTRj1Efzwu0UgQdf0XkWau+lduN+MhDHVlxnFQv7zPFJa+wkf+brdfBw5vP +7hmscniIi+em8grcj6zAgyQR4IWrNl1TQucpY0mMVDsawuPk6fexWbYuTa3eMKYr +5SCs7jso0SDr/KEBoBbcVvlJnflRM2nPx0Otm6ck3IYUZ0rTlQcrVyqty83U+j2p +zf9Gu9PCZgQSUpnxEPtLjJmHRmsGCEmDAkQWv+3pkHvu81FE40CMlgmwWwtRaep1 +EQqritU1fIigH0jjxsrcf5dLkU4/LEKAqGrFAgMBAAGjgZ4wgZswHQYDVR0OBBYE +FP/Ec5xkXZ5pOoPxxsqKHzofGSzdMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGUxLmNsdXN0ZXI3LmVsYXN0 +aWNzZWFyY2iCHG5vZGUxLmNsdXN0ZXI3LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEAYyUAgy4qhWOIFHHH761LTxnQDeCnFEf8b74H +ns4Y6Ba7poxnkTUUOCi07jJP4ZuSMHyCGFBwaICPJGYi+vaz9fB7tEh0jW0FPtBu +VPSKhMxk6AXhWO/2CvHZMNDi0wD/ywnq70BxkFLIKCe7MPiNCGN0RO6AQO6bzKlu +naH3xUqU4k5wxOOAwBLP/a6RhFVEZdzlOrnMyorBSDcW/1GoIHQ4YI8Hm0Nuhjc9 +UACItH9HIVLljoewwBM8mfsn5Xt+0gczdbgnZvEkLYYQDIShr4R6gygWWwATSOL9 +O8tDNQSr/xiYUP/tXd3FTfOzSk9fJAydDWlAOq7itRsp312fwA== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c7.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c7.key new file mode 100644 index 000000000000..97717ee17647 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c7.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEArQMWH56+CUJvj4QoWpPVaD54HiKImMvIOHX849UprU3fkjAD +VChBZ3xk0Y9RH88LtFIEHX9F5FmrvpXbjfjIQx1ZcZxUL+8zxSWvsJH/m63XwcOb +z+4ZrHJ4iIvnpvIK3I+swIMkEeCFqzZdU0LnKWNJjFQ7GsLj5On3sVm2Lk2t3jCm +K+UgrO47KNEg6/yhAaAW3Fb5SZ35UTNpz8dDrZunJNyGFGdK05UHK1cqrcvN1Po9 +qc3/RrvTwmYEElKZ8RD7S4yZh0ZrBghJgwJEFr/t6ZB77vNRRONAjJYJsFsLUWnq +dREKq4rVNXyIoB9I48bK3H+XS5FOPyxCgKhqxQIDAQABAoIBAAf8hLysbf7M7Yvt +MJSvztHbUl2nv/D3tTQgBC67NAuPNPMRWtH7crt1c0z2JetHOjXWdGCtM5t9JS+s +yMAGvC3W8L5NFljJHLVszi2jK66yyWslHa/tQIuklBpPhP/AnA4+5p1TYGqf1+PF +pGy3waZ4Mhizea/8T5tNTpUF/GIqOdinX3Tzu5iqm1jA4YPNIaM96E3A90f/Pj/x +QlPmoCprKwnecM4S5hIXgfBxDIChwECYb3UBKALk2tLU7Xi9MmXD6myAPqgZ/R58 +XCUVrVgVyLNBb3vlrsfL3CuWjrXvYaANblM2hq9w89v3my5KT/1k/fQh2Mp0rxPR +MeluQpkCgYEA3QqHPMc97JcUZUnIwZ2HXQ47EIw2g7Q61Mvwbb/tZe8X2Z4BobPM +ofEB5Kb6nhk7wy+e5rUKCVj41NTBfqN/EZHGIqt/DDXTGZCXbfL4w7dXg5OoKEBU ++CoLIMI57FKJGRYNwmg9A8zmBr9MbA04pcNyw0Zuh9Qe6U7NL/KFFgkCgYEAyF/3 +Vp9CI/6lxWybD7nfR5mmbtPb6crodqaaBAYfoXKqhUdDmJOn3ple8S/ui+NYOP0t +eHid4TwRlyVYKtT4ivo/2cDkj5hEGiPN7gkv8lXK4+P0J0//f8y0towfdVoWx8dh +y0sO+VqvyCSducmWf5w3D0wf6v2NYrexyphyfd0CgYEAnQCbampC68er7v3noM6Z +lVlETs6o8geUOg3ZarsGjn6zPBllI0wor2YWaCjKnn1h25P5k/lHdbhCljnAbokG ++p3GVF3n0e5xURlI1gnc9HdHq+e3a0+2isaiPNR4fAlr4+usPkf3/rkNMgGy8JS7 +hdI7s8RVQhlDE4iracEjErECgYEAwhqkwVbZfYDrGQqSU3OdZgDkByxYNmyHMJlH +TWqZLY6kvSNBygYPckVkgYQLM5vyFJK9f5xoZI4KBD7639MbSZYOUGTZhHjeT+wr +StG/jqOpPYXPOF34hdbJoKSwvL+enbz5cTOut8Mv0VQXLqinj8rGU51MthkU43JL +WpS/JEkCgYAkeUZsKWDX1HGAnUEFroijGe1FyY467d+91vy1IANoN+WskiRZ6qSZ +Yfq1x9kAMzpfHhx4uYANCYDWB/2Ca+sB1sf81MFjjD5pUh2njmnQvWjsHAee+t7I +fV5yNhVO/sARO2f6RiL5EsZt8eTzgYPNbFkG+jJQDxjoJjjMyRsoOg== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c8.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c8.crt index 327d80885fab..3891086de81c 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c8.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c8.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUYMQ3IxuiB6pn0PD2FbQpl52431IwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE1WhcNNDUwOTA0 -MTMyMjE1WjAQMQ4wDAYDVQQDEwVuMS5jODCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEA0ExMUocWZ0c7F0gpF9RXw1Uogl79PxBKIc2Jd7YcDVNDWqnyMWNcueeh -5bBoM1SrPt0q2PDld/rpBeOoBqZqRMfNu1t2J+ij7bvq/eh/7111hiOObqfeWe8r -vOKcuMuP2XySO4SnStOCpRTbpWJwlWzvp2UewDQ7FKF1tt0BYnsCAwEAAaN/MH0w -HQYDVR0OBBYEFMZDNcpbk5GD5ynwpCQOY6VDM4gSMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGUxLmNsdXN0 -ZXI4LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -PbfEs/JWjdw5FUuj6+vb6hdpnkv+qCxX9ayZygmgzAd0mgYRpm51DQFyIbeBUYdA -vqHDiKMT56ODM4n4ij6gWwyzmSxLRbtCTcOC2oVyhyGlwmkYXw/9/JOUMj3oz7SN -8iRtsICqJwcbTGenvHN8Nfr1qMvl74K0foNE/hczXMcYNQq+LbYgen5dejFbOSmH -V2oE0e86w7pthriazNSmivWQEeDAJwTR51oOkfm2KMVA1adbqBB7BP8eA0uOO43L -SNAArh9Bzlm9/5raX/YTL6In7G3/wSFvIuJUYwV3JW57itzaBufZ4G5CCVBulGN1 -ltKJJgSsxo6InUTglO9VDA== +MIIDUDCCAjigAwIBAgIUbdKoOzTyTVynOAVFoXMXK7VeHdAwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE1WhcNNDIxMTIx +MjEyMzE1WjAQMQ4wDAYDVQQDEwVuMS5jODCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBANXbcH3OeBdsH37mxPgUxSfPKXNuMo7FdXlqCiHlRtGTv9S8VcOQ +sh4wBuTmXPry/cgdpESkNIF1lJG/JCWRJ8Qfx9CBiGTjSdUAnX5v0u/ubSh0BApC +PBt6e4U5gXqtXPHg1MBZj8AbhRsISKq6sVRtwRdNsBS58LXmgx92SThEUF3TNuHl +sYNGAPhhOhCPA2M1PLeYmkjh/dyfHg32LAOfRE8jdb9xvATAhgb7Qk8ts5/IRfiq +gYngjm1qLCFsUryEnSrOoPfVp5OtB5kbrsAQ3FDTBNVWOV6+qUBMMrZbhKcPJvhQ +JUd8B2rud9ehTIeyj6YhCIjHxxm47N6iSIcCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +fTBsfcn65Kl89gCzyClsha6qPpIwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTEuY2x1c3RlcjguZWxhc3Rp +Y3NlYXJjaIIcbm9kZTEuY2x1c3RlcjguZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQA8m89wtFp3D0gfSKa6yGoMyvPtUj+CcASi3KVd +4w7b7DR0e5mV2FLz9GKRxXKNQss00H9hpLSL4/uiyuHSYYfTjL+HzXR0sfyV5TDu +3zsTLE+xLZ7oQv4oDIbXXSJPoNZA90v92XYOMMyz/9sorwOI6rtVX2eFeVQNVTyw +zdrmL5l7PIZhk3DSU0t+ndS/QizabFJJ+qWKn4fp51xw0eT13Gda/WVF3gX+vYG2 +VBnRMpvxly0ZCFWVtNYMg2yDeT5oaZeFsTPPvOgW72cvQ9goq/Cqp/aMOrXJ3wTT +XxQcMO1IHVH7hYE3IQUlV8G3oFMCAUs08g7V9wRzGSlNHNhF -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c8.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c8.key new file mode 100644 index 000000000000..a21c17145c0b --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n1.c8.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA1dtwfc54F2wffubE+BTFJ88pc24yjsV1eWoKIeVG0ZO/1LxV +w5CyHjAG5OZc+vL9yB2kRKQ0gXWUkb8kJZEnxB/H0IGIZONJ1QCdfm/S7+5tKHQE +CkI8G3p7hTmBeq1c8eDUwFmPwBuFGwhIqrqxVG3BF02wFLnwteaDH3ZJOERQXdM2 +4eWxg0YA+GE6EI8DYzU8t5iaSOH93J8eDfYsA59ETyN1v3G8BMCGBvtCTy2zn8hF ++KqBieCObWosIWxSvISdKs6g99Wnk60HmRuuwBDcUNME1VY5Xr6pQEwytluEpw8m ++FAlR3wHau5316FMh7KPpiEIiMfHGbjs3qJIhwIDAQABAoIBACwFGWisAUhA91K1 +ycGO9O421D9wZXPB7WZqj19exG8LTKdLhRPWSvOvyxt+15DIR7jTcSa37h7fxw8h +Gx2ofVj/ea/PmfW6W04+7CxEdFTy7QEwsEw0Sg8V7SsFJUPNP56COwjE5rQSdhmU +YPDTsQDB/sb/NMlvZL7sjf0B6hxsgxJmaacrV13kDWLtsq0qn9Wo3DQ5ZYlO9dNR +VSWBD7h4Jo9gd5d9nPGdoapWtoksjT1JJeY+3cW8SRSnhHULVPZD2fUbCgKG8OCh +Ac/eOcVo/6r6OMSq2VMMYkHeZyuhD/ea3OHeh3Y4jjWsZRSIrhIpefAaLct6AG+j +ukNiAMECgYEA6G1z6Jj76v1b5fCYqpHehIPZDbTKZSoJWlQvJKR/zSNBfaH3CTbU +tzIHimLeCDo+bSy0RCAZfdJQoV5JGYeJsn4eu/tuQ9f1Rj1QVwHYimtpaNnf5W6r +x18BhlhkX8wmZhD0SyPG9dfDy156YtxQKSc6E6nJYYbeibpLbqh4oBcCgYEA64vX +gVdHZd5leE2FuyAowMFc+7bnreB6dAwT9wFGTCyK4kcr6YRgqeZfMoXu+3WyY5nv +jIDqgoKTyCZiN/mltahP8B7sZJwU/u5BnsYYysQcSoM/t/9S/9XOHW+Kb+4cwrmT +gV8xcsCs4M2DoPn+PC7MIhSheZVApkjXidem8RECgYEAhaO5S3n7DyhSGKn3/csV +eHge6ySSYwyCiC9zU8xJt3eYBlaEJgYi1Jqtf0JNi8mBb2CO3cTfI1s0BTSjL0yh +PcIKheeRhOy9GH/Um9Jx4VTVLPZpKBA0MNRNmiNkwzgss+73p+SQV3aSG3LdR86J +QMBPQlMTeUIE3ogBK0Uu5k0CgYAbUKw9I5H0DkZWHSdU+/W95K6TwjI7x7xrmLr3 ++f3c941jkD1XZzKOgVio0z9TieKWClPtR0XqvJnGrCzXsOu2lT4v6mYfhLoqlham +AvL9EKD3QyEwhTtqz7CHWVgEguhy7HJenltto0ppB2wsTtFcyVCC40GWh+hnYCse +oROLAQKBgHMrkYEyEis3sdi1qU1jFDfHlaznGboySgeIWKPaDPzaWBpCRKjh7W4b +gvSvraHjPQ3JR+aPI2+hzZcBFYY9rGIgB7rhwmD8oqdgCENLGvX04YHTmygRIm+x +GUYI685djyaznlsSJoBzXe64xWR0AikVHxSLwh9eKKZmdqQHo+uk +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c1.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c1.crt index 5153e2a762df..1fa6cac88da7 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c1.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c1.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUNzsUuUWlDaQigjdV2GBNk6NJfAIwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjEzWhcNNDUwOTA0 -MTMyMjEzWjAQMQ4wDAYDVQQDEwVuMi5jMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAm8aLIeL8o3DodD9VWa0KvotRwSUtWX+qzAFgM5SqHHymlDnwRJq6gutq -nCDCos6TGihA6bnlVw4eRnjq8usw7cyJW2iIIFPr4d36DN2Ui/NGdICP4RMClrgf -5sC+GE1jln0W7Jj3f76jB6HE3BbU6PCHdlAtgVcQ4j59y9dchI8CAwEAAaN/MH0w -HQYDVR0OBBYEFOMdmsBGJx8YHfiziaJxU93Fc6C5MB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGUyLmNsdXN0 -ZXIxLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -g/ZYdzwpQQQ70gIZ1jUSRkUUXAU8LNTLRx7fGuz7t0RNZ5MoUIlX/9GiJd3BPZOK -C3fxU6clHn2PA2p6ZyPfR+C7jW8akyHzqAsrawzXg0fqtlOwm1U0njNwt7xLS3Tn -/naLQoWYuj94rVzoV3wYjehcl2zJovudA2IrDeOGU/vAj2zwTnhagQ5eDQVf6A8H -kSItIg5kM+7XRYxp1VFgS61B1MGT8JWMATqQ6UyRXlI30HXWOxfL1rmWKmyNB73y -uMEZLibNAJkAl1vOuiM6W/OHKLPQ0htJnny70vX8nGNocWsAaQyDd7NfXpghS16P -fgttR3nz3R5lcceu3hKehw== +MIIDUDCCAjigAwIBAgIUC/58xkjfCrUBQCuvw5F69fSCeKUwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE1WhcNNDIxMTIx +MjEyMzE1WjAQMQ4wDAYDVQQDEwVuMi5jMTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAKkiRq1V2jX/FMHln7+vAWaB5ce1n90pKZnbPCeiTy169Zab7uvz ++MH+NIJnLrT3K9MlWw4ScGuTUBsI1TX1TR1YPaveJ4Pom/aTk/c8/Ag/RHNvdRsc +XI2sR1d9PmVGRYZh6Xb5BgILFszD1/48KdxscXgwW6brM4aw7mJA0X0yhDe53nsE +WqA+Hd4WBro/zxAK6g8jUAJiFp3gb6N8x/KdSr0ZeeNXkTOfH1jHMpf4QWnhpYV7 +nRx0BY5GeBH6jYKaCamNiAhFJSqwn9cap8mWCBywkArDXHK4ZMqIhaoYKUwIwfC8 +KADPm1dkb/Vvc6pKWfZ5ANpSabUdu7knE1MCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +vkfq/X/jJDH0OX4JOwp1EKAkL38wHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTIuY2x1c3RlcjEuZWxhc3Rp +Y3NlYXJjaIIcbm9kZTIuY2x1c3RlcjEuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQA9zWccNFBXjV8vsT117tu+Qpvd0gZkGgO1Vx3j +SKQ0CiKwxttSZvOBhqtUJc8AS1qO+64mDUW5P1zSUrLZ6kVQ/+YSvScHFHo9zDMP +4VA3PSLiyS85Z9a+5I0hrv6WTeJsh3Za9Vy8jy206nZMLrnz3spRuRdiH56D7aDQ +aQ4N2e8DAJhW4TC2bv2rG+vkTAu0fd9aGWjo4+7+6h/fMBy8PvwcpN2+MN9dvWbt +nauei+QnA0PHXOiDlNvHT9ARwZuczRm839xlglU/hbqkFjoE9lYys1usuU/GFjq9 +Va1T965h/cRNTwZ6DIDwERTUH91A35a0bv7Nd9/cW/1Sh/pb -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c1.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c1.key new file mode 100644 index 000000000000..967d61f2fada --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAqSJGrVXaNf8UweWfv68BZoHlx7Wf3Skpmds8J6JPLXr1lpvu +6/P4wf40gmcutPcr0yVbDhJwa5NQGwjVNfVNHVg9q94ng+ib9pOT9zz8CD9Ec291 +GxxcjaxHV30+ZUZFhmHpdvkGAgsWzMPX/jwp3GxxeDBbpuszhrDuYkDRfTKEN7ne +ewRaoD4d3hYGuj/PEArqDyNQAmIWneBvo3zH8p1KvRl541eRM58fWMcyl/hBaeGl +hXudHHQFjkZ4EfqNgpoJqY2ICEUlKrCf1xqnyZYIHLCQCsNccrhkyoiFqhgpTAjB +8LwoAM+bV2Rv9W9zqkpZ9nkA2lJptR27uScTUwIDAQABAoIBABegbSUboB744ipv +7E/3n4d1T+cFDS706BXRFWcn+k2NJq+CWu7lJvPakfVBernW6W1Wg5DErKWqb39H +TZl7Cem2gO74WqbHoLniRcI856wzQlmp0T8XSCqWTWUAXu4A0geCt/1hriAs8mNq +XErslqBvGa7Deponswv2vWZGTb1iTYk57NuLptB4xndraaf5swpEc5kixSz/nVtu +CCW19TIW5AKbds6VIubBv9AueNmdn+O4iRqIUTxDZnlUa/Z+wGl367cmzfswM/eO +qSphWJModroIVG9KiCe3FgHfSFihugS+HmIplnqjK+DNSO2AMgrg7wQ+60sggU5z +lSS+nxECgYEA7Tm8QF4UIvQTKOmQMkt6M1X0Nm2d+7dx8lhCQPvTsyiEpZWVxUTp +2iUUE1HMvqc5zf7CqCqUMAnL0RArpXkcYyzCgjiAma8nt/qxmoVc7bVVHXHM3R4t +CPRwV2OPlGs9AbMJGDnoX4RLfb14nzvbSRol/WI3QhMiD6UZeQ87mysCgYEAtoT7 +hKAuiWBmYTGnKNBTC0WNvkvcOqhBiBkePGt7QLCXe10pdTKiIcQdo+xn17zGkuxF +BilXkEozlE0IajbPYChdcCWCHqPh6s2pRDTlqLphPrXD8rCp6mbl05qq63zR2lK5 +Y4T/jhf4raqqleWlEY/rQ6/p9GjZCiVa/AxiNHkCgYBlRNVZQRAlj74yI+rbnZ7w +FUxog8kCcOBizUyTQy2veKePrFjAWtfduSBl0vCtnuOtTYk5ktup2jS47cdEH3x/ +1da/EnHLqZsyDgINbcsq6tMH+9GxvzUIcpkWpTjg6hqkBBLo1aeeRvKXHfBFQZ8u +CMyY63mdal+LADbsmBUg+wKBgQCnYhFfsZ678aIyZFzOOy7NWX4CFIPlb0SZ0Z8+ +/brMuqZRRkCs8EBsAvTDzhv1Z3QcbX1nhyZ8dvASgwrCjX1ky9/U5zELdHPM58rh +eDldoX9pCTyoJofNJYyDoIY66/9v3wmRUPkkkKtIaQmD6lSVZIz3SC6gLD9O5K8q +ExlE0QKBgDHiBZZRz5AQcyd1hiyWloaeUbNXlKR3XzP9hkvZX3+iZOQb76A9hIlO +Ddfz3dgFtspXEiS/nyC1RhwqLkW5yGq0SGsPU0rsCKZCeKqa9uFkkr8ZukXRacd6 +XbYOlhlZYgq2ShRq5q3zJ5ixuWdFhJo9tCprvjAAjMsDDFUB+bta +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c2.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c2.crt index fc88a125990d..56c860523b81 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c2.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c2.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUBC0p+NZGKecWH4gpX0rdVorODmIwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0 -MTMyMjE0WjAQMQ4wDAYDVQQDEwVuMi5jMjCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAwD1plbaw0HGBzX3FhRKZIbz+RncMdhwHTZFLrfLbRm83AEhvMyhrBDAP -1nyB+p/3Mnsk+eaTK+Fo/KBjdakbMLfS/Ewb9cbr+k9ZbTLGXIvULrRqefWvgFN1 -Fu+azjNrDqHS/qh4833+hnqyCM0TEEGxkRX1TIvrdbQsRw8j71MCAwEAAaN/MH0w -HQYDVR0OBBYEFLwdJ8K91/Xq8cEGOZvM9/4G1gvPMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGUyLmNsdXN0 -ZXIyLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -FdgI/IHCkZKV72pjgI+0Pz9nOvplX9SdBHYOQHl6gKdaa7RsBlqbY6VIFXG4ZNh5 -H/DWTixiEhgquNQJbum7LNan8ugMdsQXXo+WBWOgz3mYYHcPWvU7w4pyPUraXkC1 -QbsoCqY8senXl1C2RBq4psw0jpobVZWqQJxR4RRSV4mb8E6wMa46xwXTDqoTgYLN -qCh7ywhYYFkWIhfC/aS5D/kigcdU6IpV2hrNvFrFsNwCiNoyHmRy/AUDhGOE0BRp -8WjHLE2gTsR1hIF1jNIN7vebkdqq2igxs3wkEp9tN7af3mjwwIasi3ZCjKoQHVUC -EIwmFsFPsgQINx86HRWoeQ== +MIIDUTCCAjmgAwIBAgIVAPA4rJtPIHtQKLq3nLkTxzcOQUMAMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxNVoXDTQyMTEy +MTIxMjMxNVowEDEOMAwGA1UEAxMFbjIuYzIwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCrbO05PwPelGHOKJ0oSLhAtqB/1KSUPrpvTn6M1F4jxhK97SNR +eEk3sqn6Hq3wVyzxJLKMc3NdVb6dH1b9SsEFaDtwhFt73BZVo618q8Cn8P1jBphJ +DK0fKDlr70cOBJNmqFmbvqEXrkMsCMYpEFqsT12jZoIkTlmn/DaepRRWegxK98Rb +gp5JiqdlFC/JCzErenT2OPxiiTrPrsq0O7KKBvNAgF1YA/9k32YRHdcMlV8O2VAV +V91cnfpgMYdtr6qv/ERxPYmWSRx+ijutDz7vEc5cgpPCDyqOMD92jWPcQZUdtQKV +cjYObV/zMdkKi0DYJEjRmYV0g8vQ1xfmZEc3AgMBAAGjgZ4wgZswHQYDVR0OBBYE +FIwNH6lBMg95FcJBG64veXRzzp/rMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGUyLmNsdXN0ZXIyLmVsYXN0 +aWNzZWFyY2iCHG5vZGUyLmNsdXN0ZXIyLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEASS9J/2w53YlEkKVEvwrlkZtlBmVKBntydqLe +J0nQc5gT41mEAVjrybHAvuyh9Fa+YSVkh3wgkS+3/bJZsYJQq9ePI1iCnLA9Vnxm +xE39o+U5iDoYB+Y+3R+arcUlQGUafJai+nlljxKUAPB6xKMHHbGtdLBpntONDfKR ++MbcyBc/TQ28UHT0TreFYJNyVBfdD4meNKtz5MbyVH19yeIo3VZU9c8L3NXeWoXv +Hs0pWH93Q/DeRpuhT+86xF1BEzpzIPTyEFWR3aChgqaGSljAcN23BFZxViXNmAaD +TINrJowOD3wbN47rxTcgsPa4NpzH5+5sOVHG2VgQCmNnEYMDZg== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c2.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c2.key new file mode 100644 index 000000000000..2726de654f47 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAq2ztOT8D3pRhziidKEi4QLagf9SklD66b05+jNReI8YSve0j +UXhJN7Kp+h6t8Fcs8SSyjHNzXVW+nR9W/UrBBWg7cIRbe9wWVaOtfKvAp/D9YwaY +SQytHyg5a+9HDgSTZqhZm76hF65DLAjGKRBarE9do2aCJE5Zp/w2nqUUVnoMSvfE +W4KeSYqnZRQvyQsxK3p09jj8Yok6z67KtDuyigbzQIBdWAP/ZN9mER3XDJVfDtlQ +FVfdXJ36YDGHba+qr/xEcT2Jlkkcfoo7rQ8+7xHOXIKTwg8qjjA/do1j3EGVHbUC +lXI2Dm1f8zHZCotA2CRI0ZmFdIPL0NcX5mRHNwIDAQABAoIBACgtCbXqcl42eWun +MhnwXRpxktESxZWm0+vlQhyAYyXQNLFFhAsCfWpr2ZFwokRbMgG8H4pyPl3oDBnn +7+vCGtXJFXEr3AL91FAy4aR/3lGd1czhDUzFh0dvlnGY7Ra4dNFQ8FtjhH63zS9j +SQnztsR+f0wd6K9Ym4OWxThOKSOQ6RglGdZE2PWyUi11WWcl0jebXfBPwmCLJu1z +6od1OMcMIy8t15iQJ7TFG03olH1aK8dU924rlmRupInjtBY8Y1fyEjXQT3y9B2l/ +3SF3DN6tNCygkfq0wfxzrrvzIpcAkP28Cf+hEiHBD3PSXKFYsCNcPyy6FJ2crvgq +DS+UWu0CgYEA7cTgYsS8E2PO57SN4AiY0DleW4b68zXE4ef7vrxGrWkU/L+nHcit +ijNOBH4/1bGw7LyK/kssJFTtqKBUPz+RhcO98g1HHsXWFAGDA+oUv2R9SnAJUE/M +o1lbCPkEHXMZVJo7DjkOeudYdHJCi0pLiG59afd/7AIQ1AT7JI8R2QUCgYEAuJHO +HMn1HYKAH7rMrsvrHkLtevwX9vxj0dLtI/8YRo0kS0R4odkYInV3Ayudo2LufqWY +oltTQaA1tR5r+GdPy2XOodyVM2L6lcaeHt0Z8ZMYie0+oF6nwNRrKANAR8RQ4OFl +RmTjgYDytyMig7dG0iNCgiH5G/s/obAO1GeSZAsCgYB3zs9+FUHrx0wXpvwiWlKe +AXpRRlenO33Ekz9f4cD/WkbQAMZ1lIwVajqyuubxh8Lt1yd2cWyBtiW25WruH9r0 ++yf4Esa2c9umYwigbVAdDMaxPnkC9eLYoIrln4x6RmgKIFkHlqp8NZx1uKldRIeA +7KPXRHY59uSB4SynREwq/QKBgGj8z1CC9MH/fKAP4uQ/LJP/IBUIFx2wPZuaLrh+ +H7HqEU4bOb9evSLEzJjqE3ZgDSfPTH52EmrPFh3bmzEmmzYoyBw8XEZps/7Ehycp +P9uxf5DdefVBQo0mh3mwN71bB2KT88bSFrOQxP+1palk9I0N74QUjR4EkhKIEjdy +Xsw3AoGAdWS4ZGXZo/bePtTMaakba5ju8rCe9gYBldL7venkwpYEAbP9WKSqkS3f +xiaW8zMwXdjqVwAbdMNDz5Ci52g/mRkIyMl2Mi/eW0FslKM6rNVA9EBLT2YXPWms ++U/dnBqbaJ3qRMbqRNm9g+SfJAv2S41WjMUzln1dQlxTaHID1S4= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c3.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c3.crt index 14b9e54e29ea..b1e4a5d21624 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c3.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c3.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUJOYUNCOnak39xRysYs4KSzQALNMwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0 -MTMyMjE0WjAQMQ4wDAYDVQQDEwVuMi5jMzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAqWUXhz1gV5BnMS4T9gqVyod8OJL62oQ0nR0WPdyv0yODkm7cmMcvuWVk -2sPtyGHv1gyKcFteLsBH/1TPV1zJGk/ecrCJlmyfyhnJYKuahf0SN4U9EGmtar6o -5ja5RVWiEmAbNnpJQPGVrBPLI5zmhT/C5hvieY2sGwZP22e0c7kCAwEAAaN/MH0w -HQYDVR0OBBYEFIx2Qg1dSxouV+t8f1G7G4qw4OfxMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGUyLmNsdXN0 -ZXIzLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -diaaL/4cK+ycmIuyb9pf70PZ12/mNSOovYX0G/ZmhcuISxIOwig2LWz2DPlTs2wc -QBHsmiIVy0cvMM+cHgOgKz/gr1ZgkS8CoRblLh6gKHVrSkixk6oz8FABVrqmkSMw -McjMft3+7thITso2jRZJWRl2sbWN+cs0Rwjw7HajjHSLbmLu0/6rwioGTY8TpQJT -K3t0OkvBFCUQEsN1pkkXgncAoxeMQQg0fl8IJ0mrGyEjKgtqUuDkDLVdOY+y8oL/ -vbzpNHb93LbFl30Dl0Z4ugcUDPbp5tDzmeHMFbPGcKZd9s/TEInZbb/AfpGMq93t -t4MVFO6u5XaqOV792rc+kQ== +MIIDUDCCAjigAwIBAgIUNir0RrTNgwE1Tjq+NC31my7H20MwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE1WhcNNDIxMTIx +MjEyMzE1WjAQMQ4wDAYDVQQDEwVuMi5jMzCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAOj3BiCHYQRsBQ5WcuztaVmL7768COUGxdH29ksxhQ3Sq31G8MQc +Zq2l7O6uogfgiGxNSUvg69O0S+S42o1DWx/lazryNcg6B5NX/sm0YF4Msx3mZ8VO +VorCYHX3BFK80DdJ2DXgKAVMPsmBkrbBrvegAQOO4slf4Yex3T7diwKKx/OyzKRN +DzzxDwaC7j9FQOOfODBr+TCWRtwHgUh39BM5930qBHreecLrl8jZuUmzsBk7wr3N +cS0feXZxsnYuZ3QPkRtQcO/PRvr5RFf8Y4bj2s2v8j44H/8KY4/LtmxnNMi4CIyE +7IKUOVzeikVbgAtVJcrT9EpNGBQgzTkPEt8CAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +aRREEMpKtOXrMdf6ANSJL0ShhMwwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRYIcbm9kZTIuY2x1c3RlcjMuZWxhc3RpY3NlYXJjaKAl +BgNVBAOgHgwcbm9kZTIuY2x1c3RlcjMuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQAKuIGVAE6yViFDE6CMy8foDRZVxJZMxHu48YVs +upPUqPIa9puTS8HWB8iNGEcgUyEMMTUmnUNXMWvHQtRoCgTbJdm/Hx/IuFID5+67 +BuzMet0861vjEc0fhXsP/wh/j9TzvmQghSwgOsqSYgOR9VoQOWE+gST+u/u2XDQU +d181YRNAFhNF7lc3JkL6tHN4rjzT6o/iQlsCaId/7/uRvXXvNah+aJ7YYZ8ivVWt +XGI+jPmEWvV4F6zNGAsh4csg4TvE7WzDAKuoVZR7DsQ772F4I935iSSI1BxANzpF +2DPT8LPUjUlVnDhFn3uvCWu9E6K9zowPWVmfuUA9ii4NOAi+ -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c3.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c3.key new file mode 100644 index 000000000000..38e15acff655 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c3.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA6PcGIIdhBGwFDlZy7O1pWYvvvrwI5QbF0fb2SzGFDdKrfUbw +xBxmraXs7q6iB+CIbE1JS+Dr07RL5LjajUNbH+VrOvI1yDoHk1f+ybRgXgyzHeZn +xU5WisJgdfcEUrzQN0nYNeAoBUw+yYGStsGu96ABA47iyV/hh7HdPt2LAorH87LM +pE0PPPEPBoLuP0VA4584MGv5MJZG3AeBSHf0Ezn3fSoEet55wuuXyNm5SbOwGTvC +vc1xLR95dnGydi5ndA+RG1Bw789G+vlEV/xjhuPaza/yPjgf/wpjj8u2bGc0yLgI +jITsgpQ5XN6KRVuAC1UlytP0Sk0YFCDNOQ8S3wIDAQABAoIBADhIkgzi2EAivtf3 +b92pACZXxizdLgLzL3DqSxfgl2Y/CxqBvKKMwAvdBhCmVVQ03ZDY3Uf9R8yGhz1z +oa22Y7ohidYcdXzx9CmXfWGK+CtQwyeKpnCHXCY7DuYBL2Sz5FQvPZLKnLCm+pZh +fSo1QgnmeMg8ybLgPNM1WCAbkz2iBt3U9zolD+dFZqsa/ybJmPYhhZd3fofulPoF +OuZzbBZzrUISRtp74Z7+y/ViQfK5Kvj821VTwcR9dNYTWHRhtJACcKA6l2WurGhw +CpzJfuXUNL9+cLRuug++S/Y9MbHohRLnR16V1aVKxEYpqr9XBJduzSyWzw+L3p3m +mW/cJQECgYEA7QzlfFyMpu9CwoqsZJqPJuggLHOos0XmEMsmM3HHEyow0y7/566P +hjwOuA3DmOhTk7LuUHX+Kwusyh+vGtB6thnBXLvXTmalqOLvJlq5yZzBGjlp0Cqa +1Iy46HWxPa94kxtiVF83neKd95hbvsGfkjCjpjp4Oj9gj6IDOqdrqQECgYEA+5aF +cdwlr0CevOCSWAruWtqT9saAtV2eVrMsdJMotXwCSHg951VjGnIeyNmNUD8tfjkd +ch2oVtpLxU+gglusSeN+ONtd8in+vvhiBerkKuXsGCCFvF0819YBhHiMN2Jg3l2o +90Tx2xOO6p0iVByoPtFNI+ZXYhTmoQJREB2z298CgYATf98UxswvIzlbpWqe7/Pc +oHWdEnqLHLHbKcrfqnlUw0C27oPQfBSEV0Zir4cqedn+KVbfOYWJ5vD0w3x0+TdO +6Xl1dmI0qc7wwJIplob35bQgwx5IKGkKGPVQ8fV9fISvnS4OgLKjgzO5XUgWvfix +XtznXKHtMK9xwUMAvSrnAQKBgQCSdOpUAOrzHXQK0dR/akWdIBb4lk/AxLHzGLSt +XClblMgjddPbgiqB76MaGwMXjt+l1gvdLYScotFvGE6A9GrUTJli8di7q1S/yQoP +FdEjY9tPpEcWA1Ty9ILHogFGEIABkpZU846zkKwmybh9LH2IrADzABkoYLBtVFLq +RdA5EwKBgQCf9s9l92hfpFP58TCTmQrCUZsDjyuPnG3tgDi/oLfEQRnN8aaUW8sE +d5Qr432e50q+AOJQmyEzYLVJLB/aiK2Nk8v8Ltx2OOxKaOcimY7FX8bZXNRnY+zL +3vAgn/ZkCjFf4c6Pz8DnZjAPQtweAoyxSTwKl0Xf3SD8rPInQpshkw== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c4.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c4.crt index b94e8b5922ad..a225b3d73cff 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c4.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c4.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVALfaOUWIcNx5DoNpszbsieO27BDlMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjIuYzQwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAKMaYHZ4RiQN/cACOxN9GEyNtxwXC1PD5rD1nPCH0Efyg5SLcDzbBj5Y -VS+BGl5HIQ3hwX62vnI9pRM8GJOZXX4UGHQ4gdd3B+qbW3n9K29cSqnxulIZiE+A -x6LOXwwEzSag1nTEpJQo09LZPusyzXnicEJ/RpC0krAa9g6gnvNRAgMBAAGjfzB9 -MB0GA1UdDgQWBBRsXxUJ6YradZoLR5+cQBXgj69aOzAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMi5jbHVz -dGVyNC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AHe8DhnLXUPvfbKWV2h2NVI1x6MuMBFcDQ7HgukHwMMydnHclVZGffXu8ZcFfy4T -NxQ4N26irZlwjfpXSpZeE6b1fZ8x8B+3czXSZRGJIi8muBA2/YJjH1ul46z6wdgr -aR3RYQTYzEicM+qY2SfISEUqx0RGHxVhoFBwNaS/EZWXyi1XZfjD4XQri6KXJIgX -yf+zr5V0ZVbNSEci3gUR+TfjG5RWE/r8XiagZ+h4TJidAjgt0CPc2vONbgcgcb7k -mhRA92z/eT1etHaK29Ne13p7Y3NI2MxWHvyygGICBcP5DsthRVCNAbFFq7N8ym8Y -2ensWXgNScolc5SjzQLUwss= +MIIDUTCCAjmgAwIBAgIVALlGWZ7VBq1IWG4oiSXuXQqrfBqXMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxNVoXDTQyMTEy +MTIxMjMxNVowEDEOMAwGA1UEAxMFbjIuYzQwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQC0wzWsb0meqjHmuAYqRLX0HcK5yVRAYRacVIN2pqsDYxlAd/0/ +8yS69w0OScV+iiWESC4TaBdUN+BoSacPhYSe/3H7G2zuAJMg1eOh/spy3V7f+nwP +LY/LaWAHv0qZYKqBDbZYz/59ZVmcBoDJr6OELlS0hfRfPp9l8pit0zMV7ATK+pzi +OXm+huTsYsNbJrBzLAgkeR9WwcdpntToB3CaZbTQE216sVyqvedPJcKQv87GcDwQ +6uSD8CrmGthA7Sf+YtrI4GPMI6sO2+v5Q2/GScFZqPG3xkAgf41zwLuVYkhIpDp7 +lQD+UgJodNL6PhAUuidpZjzIJOE3tYzrzMqjAgMBAAGjgZ4wgZswHQYDVR0OBBYE +FGqBnUVwwE9FUmwTszJocWx8+6sUMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWCHG5vZGUyLmNsdXN0ZXI0LmVsYXN0aWNzZWFyY2ig +JQYDVQQDoB4MHG5vZGUyLmNsdXN0ZXI0LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEAVohcUq2ocIp5HveC8YBgGLssx2bNfGDSqbue +nZbjsmzWVEu2IQc5gyHzD1GcQWhOOy8k+coN90KBeVHLjm4uvQQHm5I1NjnzvJaL +iuRUpPJg+L3rvWUDmaSeBLCzcNjuNfxCmmHT2emyeotcPaQxjwxV1BLBXfmZylWq +pb8WGuR+uTlkMql29cLtqdEYirBpHiJTaqBTjMF90XaQLvng3W9td43D62S21AbQ +l5xyVgtJlWyTh+3brZOVnomaVscGltN1W2+eH8nN4H4AQhkimC+jp9e3JBCvVSHA +fJaGVIXZQ5ZX9qjVDBU96bqqGt2bsLy7dRqI9VlmoFBfX3eDGQ== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c4.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c4.key new file mode 100644 index 000000000000..d5d3271eaa1b --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c4.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAtMM1rG9Jnqox5rgGKkS19B3CuclUQGEWnFSDdqarA2MZQHf9 +P/MkuvcNDknFfoolhEguE2gXVDfgaEmnD4WEnv9x+xts7gCTINXjof7Kct1e3/p8 +Dy2Py2lgB79KmWCqgQ22WM/+fWVZnAaAya+jhC5UtIX0Xz6fZfKYrdMzFewEyvqc +4jl5vobk7GLDWyawcywIJHkfVsHHaZ7U6AdwmmW00BNterFcqr3nTyXCkL/OxnA8 +EOrkg/Aq5hrYQO0n/mLayOBjzCOrDtvr+UNvxknBWajxt8ZAIH+Nc8C7lWJISKQ6 +e5UA/lICaHTS+j4QFLonaWY8yCThN7WM68zKowIDAQABAoIBADNzdO2V56CP1RBU +xPx2/ktzxR9YnPCOMf9hVuII4pheNqvQ+3aeYJAPDdpJSTuRWshiOysu76T7GpbU +kAas/Gi9UNYmokTESRyJhPdxLHKxa/XYuEMhttIxNjI+Up+BkkaVfpec7FUI6g6t +Ey9VHPfcsCOKFVqe2oQMH0Uq3lmYj6U6LwDICDhaCmhg2JXBmnyWf2YNoJUqk22r +1XJvsZXD0qCnckaGjFceiWpQ6l49XcBI5+ylbNjpsIASgxYnW3Cv3BF9AnVcjSGI +uAlWZrc9+ixVmXlhNsRvQXhYZ1WorkQytBxGoxNmxw63MnKVZ9cTpydjPDzDSb6y +9F6afqECgYEAyCZdEbOEfFdGa2w8Hq+HU0ly1MXPgvxza2Rqx5Mg5DPBO1u3o08l +1fdilmWouxiU+ZKjDV5PYjOrmTksYQZtKLUiL2wSNyb3abmtxRflM5wZEd+/BItJ +1QSVL/61AjY7UBYS+IOWYfmBWv9+LhrqMW2jwV6WK8naM+woU5+csjsCgYEA5zPr +aIDepBW65/hFJrwBgwKR+pLvdIQXnyAVkgH3ibfy/tdjE/LT64TLUhuoRs061ax6 +UQNIoB3XCQvwlzkKCORdx86IIT/g/Pzwie++ku4nVYACmqdwpDtChtph8K1LT4W/ +HiL+fin7Bq+p99Igpb9+uOQbW/OfCurtjH8CGrkCgYEAhLlD8FAN4/JwBR5CA8s8 +PtOu7T+7as4Dr8xoLq33GrzjP3yDmy0KFwg0fS0updsYNyOxMlU+6Q0Altvd/P0o +99ydrbrJnX8ehCKYA+HjoMS7rymjXt0K+dJPKAOMfsn9rQ7mR5vi5ldxGynfv60j +beGZvMn/ZUw2gUXyO/nVDC8CgYEAy59PafdtnkfBGAHxLT8Receut2fnWs/nWe7Q +8Imo8HLAoS8jBHOtrtSCmSrglpKziXJhO65PrSz6sO4CE71ZmCFAcvPzsozm4wkF +9AXg6tiybbvTD0/+hbXnTotRDL4jAMODYdBiIM1qVyYo5Kj8td2khsPnUKGMZfoo +Ar/tI4kCgYAdVk33vS/Y+GjwhRPQhZA7KYK9DiNsDZFo7yxhlPl6bpXW41opl19I +m8lPeOST1dI9zwwK07Rnsf6xJ+Ynobs852FWMBV66AIb78fvHaZ21eLl6NejPldT +rXx5LrIal64JYemAAt2XvvVl3xFDa+eoIFr3PZ5FtXZkM2OBcau+Mg== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c5.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c5.crt index c7fcbf3c7d79..b99d8bc89d24 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c5.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c5.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAK7v7QnhK4+lR3ne9GSfZiGiriXiMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjIuYzUwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBANLAWJxq07CPGG6VYspUbzyEMn6pBE0z9Or69wrDOKM7CVM8CzrN7aw8 -zYDLuOZy4cEJxLJKvtw5wQfh/JELcMH2WNLIFNABl5+m+FpNLBGgFMC48NUCfPex -D7JUPAc7oiaZQ/CFCP+5YK+a5F7M9/SwVfQKKnsMPK3v6zyZkFO7AgMBAAGjfzB9 -MB0GA1UdDgQWBBTKJgGOBVnP8ZNUNlZXD0iNkRRDEDAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMi5jbHVz -dGVyNS5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AI60ZuBWX9IFBBfGSx66cDVV6lCiwtl2h39SHDQoCPdXz0uLdpJqLEhmpQwkf25T -j4g3bmfRMTZbsztEZFNDUkyX7hlgmTEFmxyx12Z9NtOTzp9xaZdhAr7fAwBRcWjJ -CHPjrfjR/5CVXtb5XcGdGg18x097khfFOdubyoty4sJ/4wT4zoaZzTLGskzzvvsn -BSMJdCS2kM7ITGxsUYC2r+LzKR2/nX00o0xefGzgJkPr4fdDwCxsobYWspgt2SkZ -pjHV1/Qf9qdpi4wxcvZtVh3+YT5WGJRwTsbBicZ8i/gj88qRi5cqe1XiJ/05MGrt -XEM5DWun3J9yjav9v0jfZyc= +MIIDUTCCAjmgAwIBAgIVAKBUQjzcNrPi1ZjAItNKdgM+K8OvMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxNVoXDTQyMTEy +MTIxMjMxNVowEDEOMAwGA1UEAxMFbjIuYzUwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDSlxfS27E0yYRF4iEY+t1vDkvWN7sgS0QaoJcBqg/XbDeZlq8L +6jsIFiMewikjNCx8Yk12SoDDORR46AJ/durbCLDRwXrMZJ4T610SkLINrzrUl2sS +jcji32xd2sJE/69eOB+pQ4Tnc8Xiqi2Y/3eSsLIH98yob384yBgM6kyb58RCWygN +EiK5YBMAFXlyeUBBGMSoqNcSxBXjBIdA18gnrMMd5rA29QBCtidVKjQGqpUKruMp +gW8W9B1XCuctYx0ZHzCIZpvkWzo8GdBusIaGCsiH3JJ5foNw8RpJN0g/Ja0OFYgI +3ZfJGotHPlAg87Edyja9PvnnmuQ5f7MUYBz7AgMBAAGjgZ4wgZswHQYDVR0OBBYE +FLJa8AzfXWJwVAZH3w2939hbVbm3MB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGUyLmNsdXN0ZXI1LmVsYXN0 +aWNzZWFyY2iCHG5vZGUyLmNsdXN0ZXI1LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEAdB5GmXYTCXhO6Vr1eRL7IVQqwWJYfNozY//F +a8LBgDOPPhlU1zC+siroaPirhb2oRyR5c76Ar7N9uC+5EJt9QiBTCiel/DJ2OUfA +XLa9seTj2z1csAgDC/dlsStI6EnE5JFmE4DmN/jM7SEtyS60F1lp3XjAcP4reCoV +M1Uwrj1ndJvZkDnYmhXJ8MOdI6cmQXY0xEsH+zo9+gGC6dyn45O4HWO3LTmPGJD5 +RDHZANZWbtHt6fx6oY/fFzRFROdRrc4G4NMlTeLCNIXbJhFLnFg2yKO5LfD7QaZ9 +/7f6k68enrdSUlRSMh46VV6xUxk8PG1zwqD5zdxn2UvDMyuaOQ== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c5.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c5.key new file mode 100644 index 000000000000..472e85b80c43 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c5.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA0pcX0tuxNMmEReIhGPrdbw5L1je7IEtEGqCXAaoP12w3mZav +C+o7CBYjHsIpIzQsfGJNdkqAwzkUeOgCf3bq2wiw0cF6zGSeE+tdEpCyDa861Jdr +Eo3I4t9sXdrCRP+vXjgfqUOE53PF4qotmP93krCyB/fMqG9/OMgYDOpMm+fEQlso +DRIiuWATABV5cnlAQRjEqKjXEsQV4wSHQNfIJ6zDHeawNvUAQrYnVSo0BqqVCq7j +KYFvFvQdVwrnLWMdGR8wiGab5Fs6PBnQbrCGhgrIh9ySeX6DcPEaSTdIPyWtDhWI +CN2XyRqLRz5QIPOxHco2vT7555rkOX+zFGAc+wIDAQABAoIBAE4swa7IWEWgwyYQ +FjpIMjheRb+9i40m856w263F/EB7jz5IRi31ozlSwnHSIORNI3B/K4NMj/UotNVX +R0obEn8CAGW1XckPVvFND41rQnW7BWtS1HAcSyURTJCguzpS8j5XNkWm52jmPODy +TJ/9wd0mE8jKb+JdFYy2FtuAhLTgzTg+i1vV5tg2MPxXFCLS0GtMiWxtuzN/quaz +x8+zJoKZCxttwJQUtjpuDL7gKGGNVOwUqOHNAQ9SrrcS23d4RXVQtwYc+sG4asdc +4nslvuw/EcqHhrFoq1PTpm0zhHnSojC+JYg0VAJOAj02kN0PyECj+Rx2hrEuOgaz +yGRGGxECgYEA+1P4M+Udvvj5W9DR0lq4VYj/yUE01PWjK7xZE9L3Q+9yFTFmR3JC +LS4cnk8ChUXJ/s5ABmVEt3C0FkXrvaJm1QoobjNvESnNQHFxrnAlL0YkPYFYuuJk +blOFiPXcQDDYmjLEGhCHxpuHuY7jh7v/2t73sQtjvspaWvtq/kRfGgsCgYEA1oFC +PN5nZ8vcKXMaYL6l/f16o+tU7aO684s7kERGsxnD/2aiUtn45ZPpBCJqIWEr8eu1 +MNkdg5MfZtoM/T/rc4bMlK++57nHg3eFXMSm0I9XXyUfPPX0UyQB8awlbWKiM6Ah +RRNNC1Mg7U7jZbuVdhh5uWy5+PTNzjwkNza1ztECgYEAhhB5sr0CDA/lfCu88TfB +Yqcswp9M3liVjMvwvFlp2sxMj6+FPpFdaZpSW8QLtSnQIWLF1mOIr02oUsVDhimu +LvA3Numq0n345epd2M4rl7cTt9dajH4dUpis/60eqwMjV8XCw6CTvMbUNJBbINmR +FqCfgcpwj6KDZkr8/Ntd25cCgYA8FvOuR4NiMVqYhfUSuvWXJEYR21/sPlyl0xA5 +htbjX0lnp6G7YfJ2pau2C8n9VdJQr/4PFBNPg4xdbKIM47Yao911nZH2KjX43yeN +6Ezuyejo72ZD1oa+L60XvWiyrtnPaoFHb3O6w2vK7rmHCziAX7Thu9KRKkrpBjID +AaGGsQKBgQDmPuOg9nfdmf0HA16EKscnhZJAZsosjDfmoBARGwr99YK2iAlMk5pz +HwJoOF9etmx5c/nQwxcFRsarbtPTJbRmvbnfvUVCs+Ae4W+uSwCOCINY/GyCaGPb +NkScTn1tu5Ge8lEjdPiSo63+DBELPNO9XL2Jn9ckmDxH2HNH6nyTkA== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c6.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c6.crt index 649acff7ce63..14f8dcabadc3 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c6.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c6.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAIrh/Q7uY55aIFW6/o/ToRCGAKLAMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjIuYzYwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAICwXz/hU7DraQWX8TJWd8hva4KzTKOpq8JjnbCd1n3ovAqbDP6GkgVA -JxY0eMFwP+u4jN2JZuTISWIUOmLGosyHpXsXQwbsvDLUC0JPb6v0/omkLFsrIXgB -t2xIy7Bt3eifKUmUGO3ePi+LbgKYq7YNy1uVaFiYLR3PRul+TCXpAgMBAAGjfzB9 -MB0GA1UdDgQWBBR8+r3Wp4Dc7QfmCXkTGgz3gyfP0zAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMi5jbHVz -dGVyNi5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AK3Q82RwW4pqllyno6FO3OGOM3Dr2I6RN9N8A4eJShZXpyIvrgSH0/WSYolkGz2/ -NobQQBHFUYcjWtwUdoCTNrYXrENbG6j+EmW0ey5VBZNHyLc3clufGMc76uJUvARV -OLeyeSuSjJYxUafwacIzjsJtJbmixmgweKDI8jmBIuvSohnn7icwjGx46nbagBpJ -2et0zT16kVl6Ti/GjEYw3Q2q4iZfWaQ7WvqVYvk0gidbDwoJGP197MexeU23M8tS -CcmTLGoj9LbW6mc5maU1pxD7h/cFoMjgHQCDsV7tyoex5ZdxrUVaBKqGT4CVrPXK -OKyAMVQkOvxq5/QsYT+MHiU= +MIIDUDCCAjigAwIBAgIUNahDnbmVJBMJpw9oTkc6UAzlebswDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE2WhcNNDIxMTIx +MjEyMzE2WjAQMQ4wDAYDVQQDEwVuMi5jNjCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAOFeQkB6+nO/5i846Io3oJrNUgx7cKSg/UFMYHGzRiBB9/OYN5J1 +WgkTMRGzOgGCSE+tvEtKi7jWhzufpOLAn9qKuiv+ASpoFzOLA03ncmHxVyMJIlIm +z1w8DeEVt7VlKNOp8ZOfJ+vViN+KJdvzakI2Rrwa3ZgDJ/CB9AqA+xQoFiq5RAyx +pzzlBQ1b41Zh2xg9/S5UutYPTOWamrap9/KYAkD5y6/7nrxaivrVAfOQH7Pbzcvr +soGep7iN3SanADbUBOrGW2p5W12c7ln7uNq8C/aGU/d24dGNvHyvPYCeYWTAzctr +I8k1ckHKTf6QY9E827pBdeQmxuJ+vvMBhBkCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +Fvb/fRTXlJ7vEYmB9oQ7en9NloUwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTIuY2x1c3RlcjYuZWxhc3Rp +Y3NlYXJjaIIcbm9kZTIuY2x1c3RlcjYuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQB8BYgOfCZPrzQG1oIe7uHoSu7Zht+u0VINvGvB +RQ6sL1ggr/NXaWYkTYH9RkDeJTdCVx4ke5sjCO2mKaaBDWxLvDIIRvmXn/+R2Jmz +q06g4mo5mYw/4TyLTqBNmgu67rw0apoDth6pWmxasfckW7LQ0Ao7sutaBH6c7S0q +VS2IixcCEOvRO69aul1k+un/tgBXVrizHOfm3k8K7p5m08qE1+1OVVeF8RwUiQXT +2TYhCPk6JJCRlGuscUDpXOyv0j9Lb0mkKB3VFmh4JqUiTIfKonihUPvkPpKXFJjL +jAdTIaJpkHaaCt3RTBxaHfQZDXuPoMjYo2abYnWLTUW+Q1pW -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c6.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c6.key new file mode 100644 index 000000000000..474b03448708 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c6.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA4V5CQHr6c7/mLzjoijegms1SDHtwpKD9QUxgcbNGIEH385g3 +knVaCRMxEbM6AYJIT628S0qLuNaHO5+k4sCf2oq6K/4BKmgXM4sDTedyYfFXIwki +UibPXDwN4RW3tWUo06nxk58n69WI34ol2/NqQjZGvBrdmAMn8IH0CoD7FCgWKrlE +DLGnPOUFDVvjVmHbGD39LlS61g9M5Zqatqn38pgCQPnLr/uevFqK+tUB85Afs9vN +y+uygZ6nuI3dJqcANtQE6sZbanlbXZzuWfu42rwL9oZT93bh0Y28fK89gJ5hZMDN +y2sjyTVyQcpN/pBj0TzbukF15CbG4n6+8wGEGQIDAQABAoIBAAa1anm2p3SxYFHk +yuhNZFyjUsdQF9/wsmVCl6be0C425IGF9RaJI4FcZsQo/oeruAkWKgIgQZVCnpMv +6wW8d+22njjfxmtbnqAwScyEgJ9Jgr9mXtUM9nR/jaa3AalWze6yEwDC1rqCmGDW +IQ8heXbn3HZuQs0q/+H9DlWPNhopEoXlPd+rvN6oCXjnD4kgWTBnKgPaBviJJMhq +iWt7SHtHGpgits5nCL8POZPQp5AVhiTK6ueXWzTcapw6I6AO74CbntbP4ow65SeV +6shUk2wVE37uMej9A8Tp0+5Bm2/gSc5eduzMPGogpD7aq1hw5yA3lQ/jUP0K4Ebb +GcwmbmECgYEA8bzvkjCZBU1GH6RSzlPSq8IXW0emwuncfYcpWMB7RshLs+AxBxC9 +jN3Q0LqTTLcBzTJTVoejmuoLK3q+sxQjnForLzmGN+tOIp2cMJSeQRdkNvtcV8hS +VbF5Akbel00Fba8V05XuUzETt4jRRpAv3QHtstfmRObTzkt0qSQbFGECgYEA7qoV +PnXgCYjBA4BKW3f8iNBrw9YsNkc4MpUJ2+JO6YSAtrwTOnpbXLx5E7k6+33cYvc7 +cd53p3++M8VhFmVOnS8y/KRHfTjO0qkxhj9bCjQplPnkcPtFMCno872Tq7Le61LS +Qay7XS9b7VeqYj1Q85P8RaRzhEToYD4FQk8kCrkCgYBgkg7OwvVOZelemKGv5VKi +5P25rG0VLqGzz/wXhYV56Gg9qpFLdkeS78YMrIxNuEpS9AjQ54zXRSpDK1zlY2U6 +f9cCnaNd0XW8ZGRSD/0MmdCiGlZwhwlFTIm//BnOLam+gJRim2HDADcVOeYIn29P +hga0TIWj/uacYg5vx4m94QKBgQCKE+TOiivf2Dqy59odKUH1dX+jgZl6naGpwgg+ +KqQ0/e/pyM3nGzsYYAI8owG41oxWxWqi6m6XfZRxJ1SQPHRLBC+fcvvZOrCNuE1q +ueTV9m/IPPspUnYo81ZPDfMNdyL5SqVSUbjkpK2ulvWAeLEI+ykMsaQTdjpq0/b+ +8gKMIQKBgHisg1KyXQPEC8PRZFMzvjcdKeBbC80qHJlCLzbY9oC7g3BGE9A87rJ9 +9w0lYJNRO+KdHt4vuL+86V8+fxweNn4VUKTDzu1UbFFxalT5UXi3G98kreD2BBkZ +sjtJH5fo79Fv/GK1zkKm5sLOSdQe6EY+MAz6McDKU6K/T7mNcIo1 +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c7.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c7.crt index 88c2c0403ed3..5b2888c9546e 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c7.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c7.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUe1qpohqiGcra4yw7HWh348nvcGkwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0 -MTMyMjE0WjAQMQ4wDAYDVQQDEwVuMi5jNzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAhbZCMJKpb1FJW1h/xfK694X12jOwAp0Oo3qv2f06lSuR2jMH+2C44+/L -7JJoc3/Xf0is/VBgKpqJpQF69RDNTU3v/JicVel0aI1SuRZfLrBNK5mGYGbevbLZ -fAmuzMbPn+5WlBzWePNmyw431SDabPEq9m+ziFRedfYY7K6PHQ8CAwEAAaN/MH0w -HQYDVR0OBBYEFItxCmYpuU0EV0BsB9Gbc4CDQJKSMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGUyLmNsdXN0 -ZXI3LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -JorZihqwU6V6qWUU/GaXGHBMCIUtQVO6NsLbV+6YQ0ljSDlp4N6f8M2UFZZ47wWT -t6nWpFjrm00nng8usq5OF5KM2vyHyQ4gGs2Dl2+R0+BGy8mvj6yLfkxMMytOPBHK -NYucWh0Adjk1HqSf6Hrb1IskZqLk1/sJOoHoFnUixJ2Hz1L3Fcwl/eG4eeJ85KNS -vTx+Yqnx3S/51euuRcIJfM+1+9nBkAE8aaS2MLpTuU0kWvtk0PBPuro4t/8iAZ/G -di7yoPhtcwwM3u897hlRBkx7mBe5nae5HSAt3tqIGkvFQz+BxXfM8cvYOHaFyPRh -2GrBdAXu9kFDKfrAWxIUzQ== +MIIDUTCCAjmgAwIBAgIVAIt129VlGKEzeEzFC9tYrxj87xTnMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxNloXDTQyMTEy +MTIxMjMxNlowEDEOMAwGA1UEAxMFbjIuYzcwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCTn/hDW1OGbbzoG2Y6RS5O0CBHutfWamZ/xvWcUnJvIJUWBTcx +62hYOOagq44thDXgzxWr65oKbafpTQGiOvzBoEo1VSmA1assCdKzWxnUKI6q03CI +eqLLJASUHH7lMAuWvafjn0AJW0ZyA5bipSTD/nAO1/RRJVlp9FbUfkyJYXWhs8FK +quC29MWMDg0Tays3jeCPYPCO9Hdl4xxDelsUC6Bcc+1Xvf4M3J53OYneRUr25ylp +tAg+WrPTvETnizT47CUepcuUkGOLReg5gO1p8nPqBQnlHeL5j7J5Lxj7+dwTq4Qd +ROM1U19vmQEGfB4h/rCwPUCDC2QFrMCYIMn3AgMBAAGjgZ4wgZswHQYDVR0OBBYE +FOSnZLFIIGB/ex0NPJtq3KQf1NxJMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGUyLmNsdXN0ZXI3LmVsYXN0 +aWNzZWFyY2iCHG5vZGUyLmNsdXN0ZXI3LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEALRz6bKbaTIgnZ8cCaTw0zc89E0xz8kooR83y +iWdNDoW8K1S3EPNxjvBAOlm2PpuqxqwpCHc8+YjaLm9rjeMpmv9OrFMBHJmFHNAg +2+A3/3gsGVzSWlzbb78aE5M67zXq7qm469QtwDo4hMEb8kRejDPhWzSKxOLwEX2X +zcNj92glYFXPVt97Gs6/s73E0kgmGpuGMsF4m/IGBr7FWGriRwtJoQ+5QgpW70D/ +66flNUCvV7MddcjjFJgFZyCLoWprScplUMjYm4N0ZuQ8tQCNxJPU586eTFuU7+40 +pu3KW/oHTcZIeVbnbWf/jtqLLWE3VpDM6QGsv8lo7umF5wfLhw== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c7.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c7.key new file mode 100644 index 000000000000..6e74922e1078 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c7.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAk5/4Q1tThm286BtmOkUuTtAgR7rX1mpmf8b1nFJybyCVFgU3 +MetoWDjmoKuOLYQ14M8Vq+uaCm2n6U0Bojr8waBKNVUpgNWrLAnSs1sZ1CiOqtNw +iHqiyyQElBx+5TALlr2n459ACVtGcgOW4qUkw/5wDtf0USVZafRW1H5MiWF1obPB +SqrgtvTFjA4NE2srN43gj2DwjvR3ZeMcQ3pbFAugXHPtV73+DNyedzmJ3kVK9ucp +abQIPlqz07xE54s0+OwlHqXLlJBji0XoOYDtafJz6gUJ5R3i+Y+yeS8Y+/ncE6uE +HUTjNVNfb5kBBnweIf6wsD1AgwtkBazAmCDJ9wIDAQABAoIBAAGqJR+FURzL8fxg +CrgPfaUv3Z1Rcwuc6Yj5Ul2v5FByepvj9Spf8C5GfftD0/eZbQ4a57CnmHYTpHgS +ATm2FpGMYCnXyv8D6ck2VyTkmxb0Wh99AODZGSrXPiH5bEjjUM4jV1hOvGcem12/ +wpzNpet73AGe18dud2KlebEtt2k5JrjTKJs2o6JDpo05MBTjFMqh9o1SnxRQXcSN +Z9K0ye0N+asaTAVS48JyCXlsB2ksmedFcupi/s90FQPt78gU8v8lydgu7G5ppfZP +dgKpanemGCytpJsWmE02rj27L2nHsa+c8UiH+tQc/ZuRkrB3SMzQah5OSsojiZD4 +fflDjNECgYEAzwr9qjdZN/E0xyjw3eCwYZG1N5r9856A4v5PvxyyONapPZ7rmtPc +t2VxVeFrD2dAeOET/iOmgdYJ05HI4RZ6BDhJCbH2gDGwnEu9HluoYczl0WsMwJ9U +hjTNYkcLMnKmJvjttbasp8FkDanJVcftgsq5ljB/HTCdg8WqI9BuGYkCgYEAtog0 +CRcLY96X35mXXOYdoIYPrQM47sI5WpTlVX3dvSQhWxect8yob2osnEIlfHkX2LYX +sb1ACIDy1dx30fhOIdK5oHUUPAcL/U2XOg/tocL11Sayjv6zv+jK0OtS9nvHmD7l +rgUpQU9HDxFEUjPTzCayl/9tY/O+sU7iwvhcZ38CgYBKjA2LtUQVhW7YauBXM2iu +qNUUAWWAy0hi8uhpyn5NQtVLD6aLZ88n5YmqP3DZ6fZnz7rwHYv9YcDGAc7PZNVo +IZACwqYbwEhz7j1Y6e210R3Kin3b+UJo1ChDQ4s/jdJPY//LFswR1usclE6e6U6W +ckHiVvHd47xDTu+U155c6QKBgCUjNSMcUy7wnQnbYBaAo0qDvDDBVRYO4khxvu9T ++od/EGJOPY6Pzz1L5uOr+aCHqrhm5A8/qAWS5tVoU+Cempb7HtVsvL8x19ijrr8b +eofM+ONzSiUZ4UyMeqdjGbAYoxVwxp5PYoNtM9H2+2WWMeN5hCC00XbiosvEdxKg +IXzVAoGAQc4YVnpIuLd6AxVZUt67gopnhk3liKm23VRx73Mh6OeX/o/REJSCJftw +aj99anNXBi0uPMXY/uMr2uVolgshuanaAg5nugqwBpn7yPrqXDFTN3QKT7ighEiO +N34RoRmXLM8D0ch/TTcbt68TxZTmJTfKbIrIEzNA/ck5h1iyoyE= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c8.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c8.crt index 250c5f5beec3..0132f2a30f78 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c8.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c8.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAJC+xJO13Zt3e/q5XOGbqT8JJmGsMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNVoXDTQ1MDkw -NDEzMjIxNVowEDEOMAwGA1UEAxMFbjIuYzgwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAK9dg+Pl7I8v6JIQCdZA1W/1ieuKHHdhnVkNYkI9y1Kw7cPyhZVsApOz -mjX5YccUCgu/zsNgo6K/Q/Mo9/2qxx/JCmBvaTL7PAUzFS4VPJFwiyM4rhebP38w -TUb4YXKk3Y3GZ9Xy1vFFbDMmx4HREFi+2PacMZyrs7pCIGtvpTlNAgMBAAGjfzB9 -MB0GA1UdDgQWBBTCTGp5uFyY09VJmeqTkdEVrElUdTAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMi5jbHVz -dGVyOC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AKG8Jr8rNglt/TS2LX3/J0Fsgob00H7fmV4DAuOM/Ui3VeZi1xdHm8su9s0R3dDb -JkU8t/cI+jrg2OI54nDuxaNRX0dLPWIfb5Q2Z3cXiDdvvy/9RIK1SvN1o4XERIfd -Vu0ZP5akVexuPgZWfEpwgeOrjlkhO5VrHap9+igvSsNU2mJtzK5iFy21nYobC2OT -/a+qxa658RdvojerNAiYmpC0mvKv3Au94loJ8emoZOnVIS6lQivCz97tKO0L6tnI -jOsCu7cW7IAxKvYOqtOSZxZzBHSa4QFu6HixpTITbeyJ6a8MaiejHG5fRZ3BpSIG -BmlsawZmp/Rrd6nLq2Aztqc= +MIIDUDCCAjigAwIBAgIULAq6TsXXCvEpwqFtAUo2u1hLBR0wDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE2WhcNNDIxMTIx +MjEyMzE2WjAQMQ4wDAYDVQQDEwVuMi5jODCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBALSKQJMbcL+N7OmSZYnajjrWaSIV2A89E6iLmyvyzj6ki6oGVJa6 +6Rnaome4tA6RX/hSF1oC1pH4ly/KzwCMlAldqwxFrMAmkAhcP/zpgs2w1wQO/QRz +/6WgDLChPlFd/PoAiNusAjf6/+cUXmfc6Xq/9d6DE+sirYiIuwHAcubgRzhplQQd +2iXtSII9X2eNf41rQEX+fJHCbd+d6df+6BORDgo77Ih8Drx4gc7MqqK/bPkO+lR4 +LeClwTaosp7U61M46bc30DdiJfDVMELd+hxMKn0sxtjX7WzOI3YNJIyY9D0PXTEv +Zxn3xNlLLrAJ2WOU+iRgbbU9xdaKA3Gt4UsCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +qrgI4mZy+JxtQsBRrd2WS7ClufcwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTIuY2x1c3RlcjguZWxhc3Rp +Y3NlYXJjaIIcbm9kZTIuY2x1c3RlcjguZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQBEK2gXBnPrGkEhmkWd+evq+8LtIV3vuFmfOUMW +bjhoyzohDY6LyCoC9jLEJCwevlZ47wrARDjoLn2DLxCQqjjC+gMVZ7Y8LrTeILKl +w43m3hAb+vKyXoNgEZnyRCEGp5n1sFveQDUIe8uh5yKfLzow77nvQiRJjlvEvg52 +g5JoNi7Guh/qdS5r+1I/pt6O+qzpCWR4j4l5fmu3NubiaC3iu2MELTnPCMFb05Bd +KYbZdrVCwyBw5/WCea9ZBE2GMU/MwzOzBffm58zWwYYjEY3rdvYXvC5N1+P8g8RQ +HdOlplW6aKnPRguaupNW5jWwKH4NtkFLrKg3AAJKFpZD7kD2 -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c8.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c8.key new file mode 100644 index 000000000000..c7d6027bbb76 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n2.c8.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEoQIBAAKCAQEAtIpAkxtwv43s6ZJlidqOOtZpIhXYDz0TqIubK/LOPqSLqgZU +lrrpGdqiZ7i0DpFf+FIXWgLWkfiXL8rPAIyUCV2rDEWswCaQCFw//OmCzbDXBA79 +BHP/paAMsKE+UV38+gCI26wCN/r/5xReZ9zper/13oMT6yKtiIi7AcBy5uBHOGmV +BB3aJe1Igj1fZ41/jWtARf58kcJt353p1/7oE5EOCjvsiHwOvHiBzsyqor9s+Q76 +VHgt4KXBNqiyntTrUzjptzfQN2Il8NUwQt36HEwqfSzG2NftbM4jdg0kjJj0PQ9d +MS9nGffE2UsusAnZY5T6JGBttT3F1ooDca3hSwIDAQABAoIBAA47L6fo69RNrbUj +sxlQvxFoQVVMf4GEAP6yMoMeWp+ZfODkKtrO1P0SvOaMKrVTREtgvTJ9MsjC3bgI +tpSGgEzUs1z+SQXEhGoES472ITdxjyFkxREKy8YUfauWqkDjtcNHM4KNZodfI7jl +ZCOvyD/CzfDCSFmqnB89F/ClaOlF9kvTaUoXNpLL9+CL36lTXZuJRBIkcGGbjRsb +rX/DRFmXcJdd4pQtx4T81Qj0hZ1nas2SRfS3lZEUsgJDBwvi6z15D5XwmQOGjfG4 +8COxAWFt/HslClOva+1SJo/xnjMXHmI3EG3FUehm/Pzwiwb0i2fZowtbaTM5Jb5t +jTSQENECgYEAuFeZZbAToFWwKCzA1E7Xl8LTmNWLV9QtdsY8OuADn1bk/Eqmb8GO +t3TcQWOHdawK6D/4TsjLUrS9tyZ26cs5p2MF04wi6IJ1tYHEMsmFAoaUtKMtMChQ +Pzb7r/g6N7RtQ1U4sjfkh5jpmvVYeFfqvEUuEfDH+qTciY3O5ocxXh8CgYEA+rhK +umdXZjbKUvQS+zqnfQF0sBdTNExm8WrPkN36Ftej7uvDYLnTr78eewDZP5J0SQKT +E3Zjsv/KJ0DOUBDboXpxRmKS7Nq0qrJRaw7Y3Uya69XCqZVi7hlfsCRRrd/vXABx +8M3+ebt8TfhJ7iry5JtT1WurQK9Vl3xm41PgP1UCgYB7+37SY8Bvf/JAswjmQjjC +e2ixTD6xzaTeU6nCt1P4YQsVnGegloAUZ2aUHArJW/LiDdRFuMr3T1Sh0B7+U47t +Q65xf7kixEvCzgpVVjA9aKKvc2g7EYIXYjWZPsx5FsK+HJ1LUEhF4tSLQiREnGzm +P2ki69y0eRlS8quLZLYRZQKBgQCI+vNpEFkB8WFtHQjZ1dv4QjlclAHTiwKbV737 +6itfG4XnXUwrpptvY73lejs3rm52DUAJ3B7whywNcizGNc+pU9pbmDOp0Hvw3uf4 +0+3MSrRBsVJfkMyyRY6wD8fIdZay71NRO1qTa8moV8UcqL54BB0zd1XjS2g6Ea+s ++ACG2QJ/cH9g96IaFeJCKU5Ik9gox6UppiNU8tUFHo/nWorIn1CrX5b/p2ZRmlFS +nCGRoghnv1qKly/OjE0U/cVBg/ODEC68ujjrqXSukv7LsEat8uZxU7+J+8wrDcqC +mcfC+0EKO1A3zlOnpN3IjOwnuF3PuzGjEDTwNlsodkja0edllw== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c1.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c1.crt index d1fa1d536013..1a34cab66f87 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c1.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c1.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAKd2+KsSsV/wqMN6BTbenGb0t7J5MA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxM1oXDTQ1MDkw -NDEzMjIxM1owEDEOMAwGA1UEAxMFbjMuYzEwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAIzKcPP+NTeJoVHNK38SvvMpHADXXN4iavSOL8iker0xzCV9EPpBCi6b -zfiCjUqtS5n0ySTCce75Y4yuhLstAZ3DhUyKvdV3oVZMv2T2Tqy2ybrT20YwhRjs -MXWKlFjIdWLp4kAVfMt4Mf+qp9Ou+RCUKCtYl1+caHt8K6lE4uPjAgMBAAGjfzB9 -MB0GA1UdDgQWBBRJ7Oy8e2RR2rHLjBcETcdR4DmY1jAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMy5jbHVz -dGVyMS5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -ACJxPS9L9+bStPqXjVGavLMH1OTT5Jz521oB3KgOnYb0pEewnnUpYi6Tty8cuoh0 -a8MoAF0O/pqqXYJQ9gaUBBdokg0raDcE1c0jObd7OR7IFZoG4ojVh0M8fK28ykFM -F+Jy2nGLiW5C2Je9pPELYHmwfuw16iU65WlSYkgPAwwc2oNNnq5mmvLgGOE1kcri -lFjQuKgQEzt/U4oRX74zHHsajC+ZYAYf5mTrz7qJO4IH8/+//HahimRUphE0o/KZ -NY6iyRppdhWWLQFQ8+VpTY1crmwP5qyUcrlSY1zl5zkldU9dfNmz2NdjrnWtMbl2 -VdTOSAtHe6OtvsPCAlrGFyU= +MIIDUDCCAjigAwIBAgIUBJaojgaSWGBz/bLezkR6z9wUVtcwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE2WhcNNDIxMTIx +MjEyMzE2WjAQMQ4wDAYDVQQDEwVuMy5jMTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBALVxnIZebpQ7pS6Vmwn07m1162CGGlQEy/IJyU3FR8CDs6g69KD0 +UoxN+W0jHTBiAlJhXx2gKQCd7d85kkSJkM0u1zoD6/WfX2dr0IBJ6G+wU0TACSw2 +b044SMHPlRSy8kQ+3uo2M9IKUBmgf7dAUqmIYsbwbB9M+iKFKslk6/SmU/47E4Tf +bWTtuqS3Qy7NOrTPu9rmAM+N19RmjlpnMDxcNpAisAJeCkkEg89YaOKAu1nh2/L1 +aDhJTUmFlKW0P8SVwY7iGPrp6sRlJJd/nP9mJrt1Xvv3CXEKfhqNP9tQhUvFEifn +zkdXlExhttZo95YLlXFo5PuIHeNP1cnyIdUCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +lK8wV3Krq7wwonIlfdDgcKEItPQwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTMuY2x1c3RlcjEuZWxhc3Rp +Y3NlYXJjaIIcbm9kZTMuY2x1c3RlcjEuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQCp45/Rp/TyZtRAmcSrTHVGUtp3f00Ivbhl5VK/ +Kv0jWX68sqZ4qvJZkARE5T3wjvRvDJRz5wzGObHJcOf0w2n0V62VOJ3gXornFyiT +p3gTkEOam9i3Ts3Q8D7sp+GjbKQp9WO7gUkdUomNKhYzDMMJnM/SLDDUEzjD2MKV +Q6k0ETVFTIehvpihtobRSeYPlSQMH5kfWVRt0iKUaIBrFB82q7d29fs1lIplAjMs +QsqpQFIg+5UTdjX8K7l6ytBsnYAY53NIUJ+Wz2mTVzksIZfIlco/8wBcFFRmmQMo +wHxeAXzGaM/HjiS8McvH41j0kDZofVoPuF+gLa1ySSOGEbNZ -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c1.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c1.key new file mode 100644 index 000000000000..db8d8ca60403 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAtXGchl5ulDulLpWbCfTubXXrYIYaVATL8gnJTcVHwIOzqDr0 +oPRSjE35bSMdMGICUmFfHaApAJ3t3zmSRImQzS7XOgPr9Z9fZ2vQgEnob7BTRMAJ +LDZvTjhIwc+VFLLyRD7e6jYz0gpQGaB/t0BSqYhixvBsH0z6IoUqyWTr9KZT/jsT +hN9tZO26pLdDLs06tM+72uYAz43X1GaOWmcwPFw2kCKwAl4KSQSDz1ho4oC7WeHb +8vVoOElNSYWUpbQ/xJXBjuIY+unqxGUkl3+c/2Ymu3Ve+/cJcQp+Go0/21CFS8US +J+fOR1eUTGG21mj3lguVcWjk+4gd40/VyfIh1QIDAQABAoIBAEijlDNjCMgFXOra +b/g7e1yNEc9RwKaUSQW8D0A4U7Jpx3n5KEhrp8SNzuZ3YU8mXaGXY9nbba7Wiw2L +4dfYbwUF1JpgSY3K4Qnpas6BSDG2mI90hfeST1s0Q0R8E0X3IfBabym6y2IySMEJ +tTeq460Fce4NAGxDh/xb7bY01hXZ+CU+lomBZkHnXCqZnWAzejmeU1fh79YBHbaw +bMmU2VjmDH0u4m6upclDW8ua4blEIWvswaLAtvb3Bf3x5ll2VR50YrYVgMxfvHdU +ZM0uDKnwgwdyrnZ1D2/j8HYzFhtZAvPVX5Hr4Oea3Cd2c+gk4zD9sbbhvrz9JGsN +TYtpV/0CgYEA36N2DkdNGKVQtj7AKiN0x0ZJ5URfEYxOL4qcfB6CzDe0ZAgWASW/ +4tjQx+JDixuiA4tQhvGuL6gGJ4DfoVhTxjjIBr+0mDEKDbr9RMw8sO9vF/0OPT2c +o3Bwb4uHFRAYcjsHQv1VnpwFKSRELwm1gEEofcHKjWKfj1fYDDkyj+sCgYEAz7MT +MohXfNQCeNTeKxtRytX+ICLGR/xQbsp3U04DzMOrLNKKzvycZKfqE4lWMAnYTQEA +QZ9IeP5X4dC6cAtw0nw06R9ISavYp4cM/oYLkitVFBJT1Tnk0zcfFVGvVbEqh035 +evzppE1fPe3KYrnkIcCUHFql8l5qQ+Z05zXrZT8CgYEAvF/w2jge7aqaYNhcf2Ic +RJqlNfQwbBSV6x/Djh0ZKbGmRjg1aMQGbKsaubOSF0OwZXHc474Bbe0HOhEozbwb +3LUJb/3cvfFYuZYPLfsnCBa2qisi6lzSay0T2DFcGFbVcKreBARmMVLUWgJNWJ3B +/4Vi+QLafdtb8Z8ZwboQJ/8CgYEAxQkBAUNOm+EUp0Rpr0MviNkRR7M1nzzBoRj5 +slGkUTCextrFTC9gvix9ZyM2XEQ6qnJ0p8xPmFjRTiwRchGoakXie/VtQlaAJg18 +Skkn2Pa6s1vfo7xUU/tKxZLEXuq/ibRzJDWiAHhWDg3znUz0GwDygSONoivna5rI +9D62nUkCgYEAjL6sErhbM5HtRHw8F+eU73pb9bcJogFxk3Gf1cUp79kbwnI/y+qv +sIK8rC4obeuQDAes4em+Exyqzo1Cz24nLPK7rdmaz4bOBqPgz9JqN/WzpjiXwqTl +KB1FvTtU22eLhTpCC9S+Yx9RIUlSlnZMxyuf+Yien7s8V2XTIczSfRM= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c2.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c2.crt index a6b388dfe60f..96838dd096f9 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c2.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c2.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAJfn9TW5KLMUqkzAi+zsAHqbcTZWMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjMuYzIwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBALEnh4sMjt8W7vwIeW/PirZad6tqKbiCeGyhSWNL07VGDKUsAfF6RY6v -oyGjBow/fPxQ7OXjlw5BFEgO3DxlTbxLcNk06JV7d4RrfHhCGF3R3x1L/icTg+rB -+QdAaMdHAiU6Fme5qGUZg7k2C0JO9mTnJO4tIsL+frPGRFmlNZyzAgMBAAGjfzB9 -MB0GA1UdDgQWBBT9XQWzCcEEuYHJXQVr447M/HqFtTAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMy5jbHVz -dGVyMi5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -ABd6sS7YCm+KYQKxvhM0I9k4BS3rkd3L9fRajM6gciPQhXpfO4X9lKxXtGfG1LcC -13U8k7nV8o9VXCcNSUXtLt3jk1+qprqPfAPDWiYkaYgRImotgU5q5IxM2uCQXZP7 -BTlzBODi/e79jgzuxhbOCzO/6D7Eji4vQP3MCnikzjXJ3MMDIV+u4M+e8UuS76vt -5HwsWQrAMl1hfxuA4a/9OhbfHRrVaYOr3czULqr+66IHetsDsUZs7JSL7qPXFldi -oMrUQnqECwZKhqxokiY0i/mrC8frajxQbk9Y9GVSBsGess9YXwNXW2VdbyyZFwkw -rIdSfF/kY4BNf3/c1FqVjNc= +MIIDUDCCAjigAwIBAgIUXLvr7qOPTXFguQJGr8lY7+4BwNEwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE2WhcNNDIxMTIx +MjEyMzE2WjAQMQ4wDAYDVQQDEwVuMy5jMjCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAKyLoDjJimn98kRd/QVZmQNfGthB4aKi7Cku5engQ3+VHrO10Bdy +iOivVQXUupIR5knYoAZyrLW8gjSjsNiO9Gu/fcuYOcw57Oh7ODgjnVPyNtudDHv8 +rbkBgy6qUjtnRL87wqxNEjDXWA0uY8/NS0Fp8hH7ZtmovhOXADbkZ8KJ0WY5GFft +HrugaETe3zuOUQisZTtORFpIBNmA41Jzt9Q4RLkyck3ujJOpzXkJo/JoavFKtukf +k2DIVnjmL2ntXgI15th8DKe7kvwwhisrlMScU0ksX3dXTdnptiDdHldPJWKPFy9g +4Qg9tsalpnpULrK9oDCmqqQZmaMupj7MOS8CAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +WU2KLwZebRk8cUl9ygl909D1eF8wHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTMuY2x1c3RlcjIuZWxhc3Rp +Y3NlYXJjaIIcbm9kZTMuY2x1c3RlcjIuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQACnbIqetl/9+thIq8QhzAMGI5faSrx5eAiK14B +2Voc74zEt6UlCypZ2r5A2v7yF/Qy9eW0+JJAfS18azfIYjrqftgf7Wo7nNlbXWyi +EN2hzVgxYz2bc3YRILVYjk2qh1uI5bQToRmIimdc4DKTIUSBGowyx3aLrBSm4uri +kbz16klJ2G5JKDvYq4kDR2EIaqSHhyCplrdje9zzaiu/HOawB8vNYkfwzuLi1D3N +/9e11DEwQJTfBc9rnBk8rgWT3IjW3nc7xL9LK5pdGFE7FBBc7IDn//YEI8fv+dnT +XqG6+CBRyOaa4ztWcGptGe/9VItqF5V59HTd6UAbaO2EORoW -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c2.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c2.key new file mode 100644 index 000000000000..b2943ab38048 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEArIugOMmKaf3yRF39BVmZA18a2EHhoqLsKS7l6eBDf5Ues7XQ +F3KI6K9VBdS6khHmSdigBnKstbyCNKOw2I70a799y5g5zDns6Hs4OCOdU/I2250M +e/ytuQGDLqpSO2dEvzvCrE0SMNdYDS5jz81LQWnyEftm2ai+E5cANuRnwonRZjkY +V+0eu6BoRN7fO45RCKxlO05EWkgE2YDjUnO31DhEuTJyTe6Mk6nNeQmj8mhq8Uq2 +6R+TYMhWeOYvae1eAjXm2HwMp7uS/DCGKyuUxJxTSSxfd1dN2em2IN0eV08lYo8X +L2DhCD22xqWmelQusr2gMKaqpBmZoy6mPsw5LwIDAQABAoIBAALXk+6Lh0+iorR0 +ld/aL3aKQP9x6l0K8PfmsVvOWV5HHwfXxVM1jELE7JIZhG82g0QRIOWqSuT9c2mZ +jqaqiCUkNNa/LaiLBbd4nB70mzlQQo4Qgq2iJlxnRXllzc/zrgK2YckDxfpu+aDR +9EvudkBgFeAs828/Y84fzR2aWokuRAP15VG1jTDwq6YAnJ1JwNRUblkrUrQZEQF5 +koQY9Eng61qYOSwHWzFLX7l1uDsCU9nBi20pUZTB49XOc4zLqEa0WXU9vQFH3kJU +WB8b0vmGJmiAYpsOwjEw/0gUrWNNaUiOTLzUF2pjz8lmwOV/si878/SZBaO6GxXA +HQ4Fk80CgYEA5szrHqBEMzocE9w8f+BrTSpNmiMrGQcF0F/OLlbXdex0GpTJJfZB ++QMM0gWW+VZSGTL3l0xCMKWye4hK8b7slXcoNk7aVradljR0b4G8ahKvpIO2PSds +wnXQR2jRN3PlVeNfTyOM6lHOWO5YfHfsaQwsBgWqAGIkA3N4mOTU6RUCgYEAv2Jt +GAwdYfZ2jeA2j8H4qBK3zLsEQHM1nJKy0+PnGrJ6PP35qEuxB5zPfXJk3eXTeig8 +UGqIEumxC77bL54l4T09UU07NHSGy//p/PFNeN7z7ockDkltRZAp661EHPZuxTBQ +yzefQl3s5Nl6RRpV0vKKrhODyFD1v/VTcfz5IjMCgYEAzzZVNYbgFDnssWv+qTR8 +QahkSamfbPXgI2eQfgwbn1TuHFtOiJWiP4Wcggbgt/Tm+pbGsYCWwIRTTRNdbdNj +JwH3CWMcJdQIga2Qh/grh/RE22ghOqBlxeTuXN5fT/DuioD7tdqODmXp4/ABY6Zr +ixiRgcqeCYATI0INmP+Bo80CgYEAmb1Rca3ZtKYuGk5wxg7F1w4iQ/4/rh5ehMZ7 +jSdYsOOKupAzC3SyYzEepOFOEcrblRfZS6goYzkDFjJdFOVv/H4bUcI+uyaYWivc +JWsaFuzS+1voJOnrM6RZaI8pVjLITPNBE948uy2EbVoelIBRTUG1EZ1vkW0n3OoP +/FOBNm0CgYEAsmx3NSEBSpMSvihuNmcNvvYK8Y4cUWYToVzPOPw8AHWMxDeGpMjW +ihjSNKI5MUCZROE3YoopKbtQelmzCHEAurvsQ8nnbWrNgb6HM7muKUUPgRhxLkec +Qu/4XpbcU3Os73fdor8hbe2Is0F1H1CKfG7IdcckViuiNpDnqzL7ys0= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c3.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c3.crt index 60abe22ce3a2..9bd11c1ca822 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c3.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c3.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUHMEbpWTGffkry4rCbr+g0f66LGwwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0 -MTMyMjE0WjAQMQ4wDAYDVQQDEwVuMy5jMzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAlMorfk1gUwZci3ta792qR6j50VUKQ5AqZrvOXk74eYzfPZiE8TJwnphG -dfMm8Ru4v04+4pf5tTzrl0L+dKebSLvVWYE+VPz/So9jyb2gHT7/I1ypEjum7iUp -SiVzM09exmSyMrcyRpWSRQcrMomC4EK5OwDctUv1EUqAlgbmuqECAwEAAaN/MH0w -HQYDVR0OBBYEFOkY81GYZIxcO6x/wKKrfPah0ThRMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGUzLmNsdXN0 -ZXIzLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -nKuZp21gDBXwZV+u3/QXpsR2jLp1T8oWh1d1GuJhXkHqT59DTLgaDxNt+8gxA7mA -Kooisz68hOjBP3KGGvz+WUWM5KhwPJplFyQi605RJOdczYf5+6lRqbeCQnC0kaR7 -iSvfqyLQXc41aTL8MxVBkJJhr3N2Qui6H4H+8nHv+R9ShE2HYC9UDUcifoabc1ME -B6wKWM9TbGSqozeH65v5yDGcC/17LMe6oKuI/kti8L3Tbcp7E2bRyt0CeYXRwq7e -/vA83WufkhfXcZZzOvvgJBxKH1MCj7D9tIOMv9Wv1vu2ECBaYPIcClTqcSjGrguw -XHBKbhh0L6a0jcsolV7v0w== +MIIDUTCCAjmgAwIBAgIVAM29K8aZdBEwDdTEdx4fbHG1OYmNMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxNloXDTQyMTEy +MTIxMjMxNlowEDEOMAwGA1UEAxMFbjMuYzMwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDqYHMyjLjGNFPYUwDI51w+jLytzTK6gWKQHX6ZziyfTUilDea8 +ZyvHaZ5Q0gZCxKgFHn6QB6QgREAlqVnWRXbD7ypFlsKC/kOEBE6h487GuyCF1B7N +lbT3yN/6LEVYNtiwr2IRAZrgVMnjMIaZA9BPULsUFmbm4ArBLxwvaS261VMcXXCi +Pfx+BxtvM12b0kZ69TsV85lh0p6deDGvevDPPLsxGuRevxGj7iklRppbZQFg6JdO +lddi+QiYgIjAlN2NrN8n2GXsa5OQO6JVe1F5Mab1hKizhzPpq93wi8wMGXLFkhkO +Q2FoZgIejo9Gt+64Xh2w8rmHhAKPbeGxi0jxAgMBAAGjgZ4wgZswHQYDVR0OBBYE +FB362r6yAWeg2JFXTAk2CoJrWERvMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWCHG5vZGUzLmNsdXN0ZXIzLmVsYXN0aWNzZWFyY2ig +JQYDVQQDoB4MHG5vZGUzLmNsdXN0ZXIzLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEAVny3LlCab5oI86tfO6oOOsonw6qhq8P9t99j +vz7wUEt7v6+SK5JouApSxpBTVs4Wo+1+0RGg5z5KJefdovHKdIc/omxhlG6lWPBr +2M3YXkNuRTbg9Lon/EqzNeI/VL7Xev/uDMuUG27oSn3XtGVVuJkjit1uZEZVS2Ez +W0/MhguBauyweFZJbL6i5xTZQiZZTor4WA7MKawy9/lAAPWQ5cG0t0PLdT8MmhdB +h4o8MzW+kAR/uCnPYLWNh1bPNtAHMXWYkycgdbs0pGx++r+Bsz028fwsVLb7EDBB +FvQvhWx2zMxhQ271j+EapduogbU/C8fWAICiezR9KuYu5zfWvQ== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c3.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c3.key new file mode 100644 index 000000000000..c645b339e6f5 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c3.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEA6mBzMoy4xjRT2FMAyOdcPoy8rc0yuoFikB1+mc4sn01IpQ3m +vGcrx2meUNIGQsSoBR5+kAekIERAJalZ1kV2w+8qRZbCgv5DhAROoePOxrsghdQe +zZW098jf+ixFWDbYsK9iEQGa4FTJ4zCGmQPQT1C7FBZm5uAKwS8cL2ktutVTHF1w +oj38fgcbbzNdm9JGevU7FfOZYdKenXgxr3rwzzy7MRrkXr8Ro+4pJUaaW2UBYOiX +TpXXYvkImICIwJTdjazfJ9hl7GuTkDuiVXtReTGm9YSos4cz6avd8IvMDBlyxZIZ +DkNhaGYCHo6PRrfuuF4dsPK5h4QCj23hsYtI8QIDAQABAoIBACYG9KHmwaMNAe6X +FwlBuo5+hK15XenzTLPfyTb4w/Yr0qXPEdkPL0P5T8HCtdfW8wDzn+rMBpXipPrH +VadlmV20fAoS6ifZS8iLb1uma9oEKgR0tHO8PsK2QZrkXHLx4SoXpqRmO4P61c/K +A4innQCFc3kQom0CvFP4Sg0qNkx5+S8Xn5g6dwdwAd3Aw9qUrXgCvmew3zZbzObO +jxXYUtw0wNEY75mPeP3wwxmI5gfpdscfp0MQXWf9C7sCIRSfnzsm6s3050wjBIGM +2wSz2Ks2t4s9DNWSH3JVeRy9nghw1U8Er/g8uAnZF7oCrpqxbbNOOysDkaxSbTWH +U6TSwa0CgYEA7d+zWPqGl1jAGX2U5ZKOLMk00BNiFs+A5CCFKvtzTdYWc8G0Oe8E +cXh1PA6/W6tuuxchahjAUZk+trg5/dDkTp3ll8gS4j0SM1N8DSW+/dsocF/vs7vK +gJ6B1iMfEB71elkOgT6XXQ7afUvuzlhvecFN/cps3oYR77B03dSCsuUCgYEA/DyH +1zJfD+vELjG3d/lEY1k7tEqKjfjEEYSl/MfMRhJMsmTR3/lomXRx45z6QGhD+TJS +/1RPfUr74MGjDnE0Xmfw+n4F+GCoM5HaMMKtX3R49xMMdaRVuSzRrMrNDy2A3AMv +JIo3QCVAASVQMxyLDVUzFS/hiUWvW2GX0xCfoR0CgYAU9+AmWsccdj1QweorJnUT +65SVNvAxlI8HDPrQJCQw20tVXuDVLoWUvwqOhbpGiWEUauCOKqphRwHpN2odPpmx +eGexrnhDqyB8l8JaolYoqHXzGfHWRRp/pflJ+ASX9jKrW0hVCkBrepC19MiTNeZ1 +tB183qgkIWtZVpOmrkHrJQKBgAfc7zMjK9l4NQ5nmFuSFrKuJnFRXY5dcedF5lLB ++7k3etKY1/4ObVdTTPWWgCqf1XagPfwdqbizLLTt9GJP3x0OSnHUmeULRlMNOesJ +NpRiMh8ppdrI+68hGYw0aj4N4hV5t++Aq4ZBYwRmIGDng+IvhL8lVfeMh5Hmse/v +dbp1AoGAAVCFNQGtHnBqce7hjAQ+5rbSYjlt1QbfOL8DNUZpylvSGiIlp/D0v0DD +3FsmRvZtiAzpnEdX8CfPWgQt/Zr5J4YPNa6R7WXxUUiZgCeKXqG5NKx0+nJuOi78 +nix4yDoZBGSyZ9tXLJIbvP7TJl2nSCP4OtLCC4lwbxSFoTu3SZc= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c4.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c4.crt index db1714af7132..2b70efda5a59 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c4.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c4.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAOcJa1E/SlLyIMeH6LDKqwCVLxS7MA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjMuYzQwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAIwFiuTI0adVuF91ltRwyCzA2Eb9fV8JUKmlVXfjElP+XGG2CUerk8H1 -Ps0nPDFBCZB+0VM5BzjrtsEYmuEk7LJDXzoEj6L2MiYKIVia+qxzLvSfbcdJsGNX -DrKn47sKDrUJbBDVodI43xGCplWyyhf0MS6hMe+7ttwb+LIZzTFLAgMBAAGjfzB9 -MB0GA1UdDgQWBBQJKDvefckZz3in61z5pdM6CpIwYTAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMy5jbHVz -dGVyNC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AG6ie4xkvfaHLWhq5BtFYVQO2+z6siRz63hBySKwyCgoNvyk1h4xHbmyB8gWG7G7 -TUjCMkRovx0DIhCwtK3Ahj4APh/QyqY3d7KV4i9jQSLXfyaY7ZdvLt18RE9i3MjL -QR4kGSPaTKlhAOyZ/qr3NdyQH56cvUEto3hgBOb71JcqpT4DBc7w+W/wNeSSPDb9 -/7m5Oq5ntS9DzJBpGcfZ4IHtrgZu9x34uYLkYCblYiUQ4QQyyQae6YyihfzH9QsD -vt5ja0lueTLuA+LAUv7OxDQm5USXQeRmXYT+Oq+MBKoJkMznftKYnWK3tchwr5qn -2bQoMq2HNQtemjZAZ3IoQYU= +MIIDUDCCAjigAwIBAgIUceiv+ma8IfROaLErkki3f8MBciAwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE2WhcNNDIxMTIx +MjEyMzE2WjAQMQ4wDAYDVQQDEwVuMy5jNDCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAMvbCax0XEJ5GCy0rDwuYcRbXMGQCmPboDv0T/+KqmtOyNhDjNaf ++Q6sZLyehbuuQL64wbK7RMQfMlZMy9o77LyZQhF2cJTPAAFI1nbSmZsRD2G+eEqG +8uOtTTkpI/uaWV5t2DjY7WBfudqE+c/vtfGaqp97wky+9hPTUSCtOelWqQqT/l3i +ancEUAdpGubYRPjWAQIIgvso/Z4Is+n3wOpvJXSq7R9GCi2+PLoAqNBxlxHrkYAB +ZfH5B++ZF3MtVC8W6lAKRn8Zk79Ek5RF7BNqv3AqtzexuNc+Pc2+fPA6O5UuQotB +NI8dd543pi64DDeoy3RW0C6gV6+sVY3z4hkCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +zhMgSZVQezwsPcFm8+ujjyi9/NswHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRYIcbm9kZTMuY2x1c3RlcjQuZWxhc3RpY3NlYXJjaKAl +BgNVBAOgHgwcbm9kZTMuY2x1c3RlcjQuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQCVnfWFIhsxteeJRKVzT5cOUPxNOofxY8ulM2rM +KJ+ii3sTybPtPonsUdTrdATUtPzXohQgQV62cP633tm5FIIXTBwvxbKOjFz05tJr +oZ7cNp1nmlJXSmOuGtimPHdt9Jm6Is6GuAACdl/RmXDtYwTHKB/h2GeaHtnPugBT +6RNImWjan92jey/Or97WtcL+otEcu0O1bMZ4GHBm9N50WdlnC/GeLW62qaKZuXje +FBgB9ZrO//SXtrEyk1VMreS2MarO4UUxcOtfkc+e66nPwwbofN8T2eNm30CMjha9 +lGC0rPdli+H8S06YJQDVUa/nnTZxnIsqu/Ck3zP7YdXuUBTv -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c4.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c4.key new file mode 100644 index 000000000000..2cb84a699356 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c4.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAy9sJrHRcQnkYLLSsPC5hxFtcwZAKY9ugO/RP/4qqa07I2EOM +1p/5DqxkvJ6Fu65AvrjBsrtExB8yVkzL2jvsvJlCEXZwlM8AAUjWdtKZmxEPYb54 +Soby461NOSkj+5pZXm3YONjtYF+52oT5z++18Zqqn3vCTL72E9NRIK056VapCpP+ +XeJqdwRQB2ka5thE+NYBAgiC+yj9ngiz6ffA6m8ldKrtH0YKLb48ugCo0HGXEeuR +gAFl8fkH75kXcy1ULxbqUApGfxmTv0STlEXsE2q/cCq3N7G41z49zb588Do7lS5C +i0E0jx13njemLrgMN6jLdFbQLqBXr6xVjfPiGQIDAQABAoIBAGMAmY4bZMMTtF3h +0NPZYrnN9Ro8Lg+ZFhSH7mIYofNDa987zyZcwsQRjUrdYe1oDyGQ8OEweZEV5Yhr ++4SdHbfqZl+8uahLb6EwEpfaZ6cbffDmY90t9MAF5j8gb809hLGLfzr8fywS0VwC +wJ/vttkDWGHj5NUI2NGACyJnfnyHL8545kttAHRDSXr7xmeVgZKBrx4ngX5h33YL +L9xjAHABrx0YRD48j2ATaBsIgogPWgywj8pB/9aClGi5QG1gUQINgzQInMcA3nge +SBXYFiLhJI9M2NXW7Ho3xIVD0mk94Vir2Ybg4I0WwCLJ/gI8LGIVmWDoO8BeTIzU +2zhVRxECgYEA6v6qg9LVlziTj1C1Fo/JISqc0Cm9d+uEoPtOQNQy10Dt6i2Uvji8 +HauZvWDwIoCbXMyR6FLEcq4PtNrfqpyEezlUPDf7/BbFPKPh+13subx/JC79rmkD +JvAruAjaNsZKqF9QfZXPCZ0SKcYfhCAJU/BlbK6kIXb6DHL82BjlNHcCgYEA3hPS +JasZmUpQeCU9QWjRvp8g0qTT2GfdhLEOX5d3qK5IETzL2ly0FvB5LNY+lW212D1Z +sGzUXxKaYYAB90HrCktR99qjoYC0CvpWZj8plLNTqIGbYu8tst7mAV6DwgVikovj +F6dHUWiTKRcampuEndii/hKLz/J6NafwUW9kEe8CgYEAhTRYOaMgObjzQ/CuxZw5 +mVhLTV/rq3dQVeQ95leifgurEOxHfVAxqevkmMnDNPuLlZ3MM1H/UzZBZvYOhOp1 +2leT3RKq/ql1ojsPVOpXzqz/sOYGHIAgLklDru0trJDYLvrgaF+r3Tow2ocYxE9Y +mkERbPrNxL5pXPctRd1Ox18CgYBdaNzltzhHP/jk8FT5pEtE8M3vjCEHT3jfjVHM +Z3uPWe86QOZF7ORbGb7+Wwdr9v+b+wI0sTIK41pIVH1/MRzZ8aCCrlZRYj9Ytr+M +tliZEWzI3essDAqeyB7ayJSHEZjKp0hX9KaM2X28tNkd2vhHJI46bDaeuxEobIEq +372pxQKBgQCOSvmHsolD39e+VV9vZdbJbMumZqbsdUB/EG3lI3hu+Vx3vIqax62j +woY5KSYL5bwKPQVINuEzd4fI7q7/yzzFEL3l96lD5Oxz/++aHprC7dK5SNVVsW3w +kTfU9kt/VsiBZGMXxbksiy3uwTKjSqIiAedYytWkRdsSRzOZrVzXBA== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c5.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c5.crt index 7ee49509b0c6..857d55f2dd01 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c5.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c5.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUVSG6oHWDOX3AILqaPWoYx+i69V0wDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0 -MTMyMjE0WjAQMQ4wDAYDVQQDEwVuMy5jNTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAm+gpSNeO4opovT+vEOBd34pKfiCaIqoe0D+vR60gB35glziDShITOGMs -04qUkM44nnv+d/Qib2BdkNlze+l/Sv92r+0DQp+MRzdNE9GkuV4B8yF99hpi71hJ -MED1lkrYRLUiEFUjmHccUoO6fF8BbdQwvD3rBTpAss1nDuO4BncCAwEAAaN/MH0w -HQYDVR0OBBYEFFT81Tir0l9q5Cr5n47pDeY9pr4EMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGUzLmNsdXN0 -ZXI1LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -BMhWQlbyMG7OYN7Xtt6d9sOduI+peTlDKju1DQHS0P+jvCuT46Tm7u/4A7G6ommW -Bc7hLaaGi1gFyiR424FSjiQJg6ye5zdix09H6DQipzSB5GwfyQsB3YWsUniPQn1R -ZSaqQvcuD4vCXANyo9LE/3VOJKqXB9sLdU42nNU4M4vyB9myTLTHodFoKuZwGifP -/lv63dd1PQXfeQ/u5FtWopSDdk6fMigoKvo7f1SViTUtI+PmK19sOqOK3qyrCagx -I0+krf0TsAhdGo1gwpu40JLb15pWU99IhKIAkAu1kjmAxrpkbztAH9wjCm1/jS+V -nHe7wxiLzStz64Ld9voqPQ== +MIIDUTCCAjmgAwIBAgIVAOAwJDjlILMIPxSxVK6E36unw3/tMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxNloXDTQyMTEy +MTIxMjMxNlowEDEOMAwGA1UEAxMFbjMuYzUwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDda8Hyy0SR0qK89aVAmERnJzLRXr9JuPzPSeNN8u3ivvyU2PHj +Q8W3xCRnAThkvW6XQB21Cy2gwyn6kbEAU0BwchH0DoNJ21O+h+RW9GOzQVE2fnvc +VOUxlHxuroLlmNuMXj5II6qT3yGdAB0Alih8AZXNw55PLU3Mf60ejxPJ1iauZggf +Za90ZNQCvjWwKBn5o5Wj0SmD/IDdO6qPtgopN6zgmpQOQKrpnjwFIm5gpPFQmTPX +WOKs/VYdtHhlcJ+U5jR0iwteRzmCB2a6zYGi0pSdzyRL0YkwU50YBxTXPgguuKK/ +KqQA9C0bR+KUoDXN4QGuRFpiR2RBXtVvN9A9AgMBAAGjgZ4wgZswHQYDVR0OBBYE +FJGYg2/QdFmOMA/chIJ+7wtY+odDMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGUzLmNsdXN0ZXI1LmVsYXN0 +aWNzZWFyY2iCHG5vZGUzLmNsdXN0ZXI1LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEAI7fEpgzcOdzyC/9hMez2oM6B46xrxzFSxzic +Qw0pl02VZPZ1fv129lB5Q3h+FJndpYV/96VS75H0oiwEwLta0OqjnrQtCS0wCcZc +g7nqtgcRbwr5hJmMXlr12xEo5aaW1ekbykbIo6ACc55+7Kq1/6v1LGDDCp6cA20R +txNuZlzCPiTSS9+oxUGYgDVdQV3udOll7FGxOrr1cH5AgJQyeMyTccqD0U/GG4pc +qPKbL5i0fghwWnnt/2gnwdTp5ZGKGGoOss/jWzL13LdMQzsD9OVPSH4AmQpRqM7v +r/nYgnSrPbV5O0+38Bs+j7IeXqJNms6TCG/zBcCGBrQ4kRS2pQ== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c5.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c5.key new file mode 100644 index 000000000000..45679e7ec2fd --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c5.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA3WvB8stEkdKivPWlQJhEZycy0V6/Sbj8z0njTfLt4r78lNjx +40PFt8QkZwE4ZL1ul0AdtQstoMMp+pGxAFNAcHIR9A6DSdtTvofkVvRjs0FRNn57 +3FTlMZR8bq6C5ZjbjF4+SCOqk98hnQAdAJYofAGVzcOeTy1NzH+tHo8TydYmrmYI +H2WvdGTUAr41sCgZ+aOVo9Epg/yA3Tuqj7YKKTes4JqUDkCq6Z48BSJuYKTxUJkz +11jirP1WHbR4ZXCflOY0dIsLXkc5ggdmus2BotKUnc8kS9GJMFOdGAcU1z4ILrii +vyqkAPQtG0filKA1zeEBrkRaYkdkQV7VbzfQPQIDAQABAoIBAA87BlBJIyJUttUA +oKlcz/e4fopgVhAgJWOs1XHbL/nU4VLryVe0pbkKSOa88sAbN6w3wNNfPXg9dHnD +48T99QTHE+0Rwn4mFBCbEF3a2bDO4WM6OBLj1BAhAp4qat6YMSHqqaYUrfsV8h32 +kAgmTAqboU5SEmWn3B4g+iuL8/SCIbiDb6Nma/bPKMfgwiHIu4fXAfVvP4AGeNY+ +HZb+mn8PX3n/k9vBOeoP62G1E1LmT97J6pfw2pNmBNSjzMMk2jGY/4A0I6iTz9OM +mNu52dzO+QbtbIB99tDX/yJ6AHYV80DDPOqXUtvDq3xi1QSZer/40a82h60fZbLD +7V1aeBECgYEA3aHUbQFu6GubBOrr6NHRPFVRcDcoCOd2BoGq4udeg7Ac4HySJDbO +csPIHOFwRWQ6FemchfUAFmBzlq9Fpa7/FrkEu7Q/5KbTYs/ZLLZ5Asi4IYTJDhOt +TTM9pvNUicUD+4dWdzEn6bYbflFFY/DSawYysG2Yo44D3SeiBLfYE0sCgYEA/8GL +AomEVEC3qPx50viGuFtC0yugcUqjgSGziywUNCZQt2o4Qq0iZO/lto6oj6j1t3Mm +2dicWKoXQBNPK1ygeqLpXA/RLbe1sJIgzTFhIDSwfh7KYNfqk7ykYwklxP1rQ+zA +HZg4BqrJu7vLcbl39zglQDbkHGK9kz5xnuW47ZcCgYEA2rvkktBfTTMmA2x294hc +IFdz0HQfJuqJKVK//MCsHvupBETtVqBgkBN5ZEd6LoIEcavltq9eLrdyXL0O174m +M57WdQooJNogtdy8UcylEYJXd309kI8K8q4noqV0QaQXkh63z1rMMXRxHfHHsiLz +CH1NeaIwivqWBaYf2Ng4gJECgYBUego/cUrLRYEkiRh4LezZ0hMVizB4m028ZnRW +gN2/h/RbUPN6WpOkwXSfwyk8InPe/2MSy7CAvNXnLTmWOgcr2sm3xeWy8x5JT0sU +W74YFC9DyUnRiSALYmDt8WSPV6Fpkza1z8SZtynhH5uIWdmdR+dN+ZOJKLgzfYms +EXbuywKBgQCTIvVwnNcdtcUJktSGgNKPZTZj5tE3rJMvdQ2ceiJEh43+Ma7amRtf +0+MqRpTYk2sY3CGi9OTpBoKn5t84ZI1phuianSnt9irQRdWsx+6MOlsK1nvsuO8O +qTLjo5E0j5LRHPlf9t/hBT/wHALQYno3jUnGiZ06HrBIFT4pDLI70w== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c6.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c6.crt index 35613a36735e..fe08e8790a2f 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c6.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c6.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAMK8wla5/wXpbdq+VeKNBkL0pv6/MA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjMuYzYwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAI9iaWrpHwmXPBC3YhfzkVe10A7AKjRzdo5uv4ra8xQmVzsdk3lQ+oQD -YMDFoP7rfxtRLzB2Z5l5rChFbkLav+3G8DJ09Vdzs28J9Kxz7N/H9tEiTmgvzdPV -BWRyA7D2WPPiaHBeIIrtgzfZbHenyxuDxfNtvgvSgUZMEjM7Vg83AgMBAAGjfzB9 -MB0GA1UdDgQWBBSADI6qBveWvxd9C8xucsFtogIDRTAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMy5jbHVz -dGVyNi5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AEb1kIeYEWrqqCHC3g7eM0r7sCIvgL4fLNeWNLSMnECM2nBaDTpC7JQhVo+nnMKE -hgYVZ1/Waj7LRQTR0tGZwRVXWKLB1suCszp+6Y3ZCeGR7ife9Nu+ZFucszw3IfgD -q4DmNB/mVc4Uab7ud5BdpEVMLlbExlasiJ6pDMf0ql1MpWaRfV6pExxbeNf+FQhO -pHWIzNk1Py/i2sTOOU6aWajQEY+2xFjlcdKzNFb0Dtq6ALsD6NuHnSIqAaDH9cAY -LmMPzzBy7s/4HAO2LM0xyec5T4WI5+W2f2V9xpkgwG8gp/ypdABJfw4/WyeI+bpW -ki8XWEjW63yhAUDTsAD6pXs= +MIIDUDCCAjigAwIBAgIUSnQ4R5z71NqC7t4zA/TxyVfhtNIwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE2WhcNNDIxMTIx +MjEyMzE2WjAQMQ4wDAYDVQQDEwVuMy5jNjCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBALasyt5jgxI+cYn4cd7KFSUkbpldhOZKtYWrSqCq2VcW9l4nRugN +s3W/TXir9oit69uv3Vj3fKKl8w9iNqnPBbQSlJ16WgWnDBLaAeu9C6rMTNHdodo5 +nNkyP8tuZpNuaflK1wsHilj6himInmGDvmfk/qlTzB4GUQjnsLGCSbEFvKi76pZ+ +2eTSTagggP2+TkquvtBnAEUVQWIhpYspTXC5608gP14tZIZ3SFUXOWBjq0U0WoBE +xBOsvjaF4X7CJ2QUOzSVGNu4BVBeibcF12t/8djdurzYB+VxF+1yY7F642Iw+fFg +0j2/b/Ki3tr1Z0gsaE7vc3yVoI26BYtFRTsCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +mrhKR4ll3OD2TS4pu6aaAILHSQAwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTMuY2x1c3RlcjYuZWxhc3Rp +Y3NlYXJjaIIcbm9kZTMuY2x1c3RlcjYuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQBooCTFaJJN38wbSRlwF+rQeNH4YSpaQpF4lLv+ +0DrfpKkSF8a9YivfoolrYAYe8sLtHADtcxu92b6KIWmd9uPShN/7fnNmnKLRgZ+x +q1eZp153S3HKa7aGaPLMMDnu7iMJetuZGQx+Loa4cIUL01ldHVx9xsrfqg4aVzhQ +dfhNDdik9EWtFDWuN3PdxZcueeaV5JnDB3OGm50u8+hhSQagmY9C8zgOjuep/WWl +xTK93NoFm1vczdY8WKEaA82ExFcXlYgnUDNA4sEOFes+eAkhgunvKZtUjejthKmI +/XVqn7b7WsbF1gMSvDLr9EWDkvStJdEeNrzN2J3hPdp/iwuy -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c6.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c6.key new file mode 100644 index 000000000000..1753b6c42d66 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c6.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAtqzK3mODEj5xifhx3soVJSRumV2E5kq1hatKoKrZVxb2XidG +6A2zdb9NeKv2iK3r26/dWPd8oqXzD2I2qc8FtBKUnXpaBacMEtoB670LqsxM0d2h +2jmc2TI/y25mk25p+UrXCweKWPqGKYieYYO+Z+T+qVPMHgZRCOewsYJJsQW8qLvq +ln7Z5NJNqCCA/b5OSq6+0GcARRVBYiGliylNcLnrTyA/Xi1khndIVRc5YGOrRTRa +gETEE6y+NoXhfsInZBQ7NJUY27gFUF6JtwXXa3/x2N26vNgH5XEX7XJjsXrjYjD5 +8WDSPb9v8qLe2vVnSCxoTu9zfJWgjboFi0VFOwIDAQABAoIBAATYxHsuYxG5WWmC +j4GMlWS1DkArHzW9rApIt1lYSchJ7Nh7rKHHN0iQ/lJiJvo3ucchkpBj5J6AzlhN +T/ZPxAQj7WXY6LQ3W0Emyp/sFff+pVfOIcIN/u+I/0X7qboQrGbNCbPvtnJ87HwD +9XEl6IfjfK7rxmx82L/mkdAWjQj8JVSCkNamfIKqFKxBUv7CxW2qZpDfUG9yXXKS +HCXAx2ff7gIsaPGW77EUcDNS7yL3KVtIkEwcoWPixBKAr2rbhihzNp+c0MA4KbV9 +HTQEYyDqGX4+Y80jznCX4xpeLXvAEtuzktfRTWQ3IXp4AF76XDd8PlPVGEzjsn0s +N2jF/80CgYEA4YAWH3FlWZ09RjDysh9axpVaR2+hvTojTsNCPZ0TdHhJOlEGpZDD +Kk9H4cVHPZ09r/ijPQQ+dWL9sfhRgp3M4nUnzNwzSeXIu0mP9udwK128RDQswQ/V +cMcPBtbforg7IYLq3Q6jDyW/NaZd+02vqZmm0Rh4Gz0hWLrD1xQZymcCgYEAz2Hi +bGw8MrG+GrokbCRIEb2/ODPouO6Czu2OWddTSmjMFhi178gufHbqHU2datvDwnQo +WkM7HKGYQjXlw5+fjrJojjh3vZU+ukm45zNdjs9IZA3HOWU41L9GzRsxrkMZPfFF +GamrjItS8JKSc3erfnJcl2wkJJlDDTkbDKE3Ug0CgYAok9TnSY90RcAt7kbt2IiM +LXn2Wtce5uRNmwNDdQ14KuD6dnQcvOTxqxyDOu2r36ynCrXNGANpOivup4F7YIvq +wCwuR/2OsaOiWAVo55Keqxpip4TaJNBCn/9LOsU5Ua+KQzWiG60p35/9tnc47mzK +txR2kdGjDSgAAHOXM03ExwKBgDnJluSLjIK70LN15yxTN9npu90XaeQOYljzDVWv +kMANT8FghWIZsApywG4F8LWUI4qN/6iXT2N9DAZ3Kl1Gn5mN0WDAD22+psoWPPX/ +9DQVQqDOzhMKR7nQyxPgXgmnHOOL1at/AhSZxjTPQwv17s2/ZFm1gqTIKE0I+qac +TeQJAoGAWvzYUHkXVcqSIp2cudsXMIiQQ4rt18GvYwYLaEI7qtshfISbQTwSPzBR +iViFqw4Ne53mlKrYusMbZPJ5gsmsrKuKJVaMyil69CXh6NXxVrmaiAFYJabWKZtd +R2ncCgdPUBuHBoA8msR6h9XRZQe8CPuIlJMgpPXRfQYyxbMsZcM= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c7.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c7.crt index 8b9597217242..6736eeb6ea86 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c7.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c7.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUGKYfqn6eHAmevff9PPzxeULwjFIwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE1WhcNNDUwOTA0 -MTMyMjE1WjAQMQ4wDAYDVQQDEwVuMy5jNzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAxbb/mneg3oKdKk7tMpcpC+KDBsWWvgx9tKPqHFBWIrklLEeHVA87wuWo -XVoEmoPRmaJkHCuNSUOmNUsw4ZAigdCHLI+Fxgu3RXE5YcOlR/VcfIdmeTOkZTmf -kAVl6sRIPMARTeQkqaaG93WtXpSFQ+hO3RBIkJ3t3vTL7m7dUH0CAwEAAaN/MH0w -HQYDVR0OBBYEFL/uRR/fiAlWZMNJZ8ZcDCDLLkCaMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGUzLmNsdXN0 -ZXI3LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -r0hXtDPClFrZFUu8WF6h7kLwc2A1Dp3lFtYUrih9e7e2t9ANPMAfYbUN+bS/QCJ7 -1ric6GBn1cyLagWV9EyxoILtkd8lGkXUC9tuS3uk2roq9KBOFgmoIZ3zvWaN8dOT -juqD2Z1hOQDUTYdiggONDTRXUc+PELS34571oFXq/c5/ZZlwww3J+Vxwr0a3N9LM -l7hcobhUZIkFz1GFjU6H/64WBDtb81A29ZVGDsRSqsgZJECjigMZhmq9XRPjVSyq -VK2bCg90lAIlH3408XZVXNahcpaWz6VXq4Rg2QAN9yES9u7t3qkL35AxLmCpmd27 -xp9v2+CIHghzMzboSiZ1Ew== +MIIDUDCCAjigAwIBAgIUTcFvgBXm16tCqDZXc34HoyfR0AYwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE2WhcNNDIxMTIx +MjEyMzE2WjAQMQ4wDAYDVQQDEwVuMy5jNzCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBALT4AM8T4lJiPu2zNijce7y2o7mNH8WV4UwegMfyb/JBuDenTACZ +V8DoYtug0BL8ugum/I0esLPHSdcuyo7Pn2FXHgTJ/9b3btQ/aKArvtUscHIxMS4f +PhOU0LyfRXErRq8NNqL36B3RV7rG6vxyhw7VLbouEn7ZyH2P22zHjZMWIesvk+yw +Yybq4d5byDuMHhL0uTRXG3O05Eej4fz1FYADAh+ZbOnVs2U6Y4ra2yZIHtNnM5SM +MHG4TnFwA/nEbPOa0Khgc37IoBZ+eDehTHLVLESxN7a1JZEbarzisW6bpUbCKRf/ +ycKT0TUwOHPKIu980FNk89dz840Z7L1sRI0CAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +bc4DH6455vgp/RtqE115RYRhjl4wHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTMuY2x1c3RlcjcuZWxhc3Rp +Y3NlYXJjaIIcbm9kZTMuY2x1c3RlcjcuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQCoEhsgNWIwwj+ACbJj+lEPCOpGmPmYWPyMKtnI +W5lxvdp9DGDFYtzpuUAsyKMxT1fCGGrED3f8sWnIujGQYdBNkrhhsgSnCKqmlGXk +M+YCbeoEhNcaPkcf1ZWfZvCkOoIWt6Q1E1ymziSaltCEG4A47jfSAbr4cRbIoxpK +Zb52G9aAPdjbzIfxW9NVAlFelD8peahpnVWN3+ghNU+7SXsiP2OjUY2K6guur4mH +LxXinkDmtsZT/k3HJRujjA3hlDcvV6RzO4GkgxV0kBodWQ0V9LJFzp3RLSI2PPBY +edguNIMyNxzEU3DlW38U92iHp/qwxkvGFCIM0g7T+xSu4y/o -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c7.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c7.key new file mode 100644 index 000000000000..73824963559b --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c7.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAtPgAzxPiUmI+7bM2KNx7vLajuY0fxZXhTB6Ax/Jv8kG4N6dM +AJlXwOhi26DQEvy6C6b8jR6ws8dJ1y7Kjs+fYVceBMn/1vdu1D9ooCu+1SxwcjEx +Lh8+E5TQvJ9FcStGrw02ovfoHdFXusbq/HKHDtUtui4SftnIfY/bbMeNkxYh6y+T +7LBjJurh3lvIO4weEvS5NFcbc7TkR6Ph/PUVgAMCH5ls6dWzZTpjitrbJkge02cz +lIwwcbhOcXAD+cRs85rQqGBzfsigFn54N6FMctUsRLE3trUlkRtqvOKxbpulRsIp +F//JwpPRNTA4c8oi73zQU2Tz13PzjRnsvWxEjQIDAQABAoIBAETz9gLw9ZwkWQvi +6/D3yaHnCU0q638yk4yYVsNYqbTpu5JhCUF1LA2RvhEnHgEOEKld7/6pQfTOK2F1 +L7LogzbopcR3Xyb+UllG68uTv3ukGhg4jB+hwdpeKoakPSZYse8DHt1LjkLGQCIn +puOXk8RgD7NP2LZCrvTAun+hLV1sPNhBRlxcZjvE0HINJ3Pw5djU9EJPMwkYVTWw +t+2YbTbMLc1ZC8z0XYZGDtNdtCMu1KuBzwz121pOOgDlTfNEIq5s48kTR4EsOuaB +7HIngX8P0stA0Luoe+UU+MMOftVtENr61BkGVCy1ARaBJQug74ndddrHd8I6VwsA +tloJd5UCgYEAu2K8H2dw01/fZ5PQvysqGhsH/TYf9yi/+1RUW3NFR6yFuLDbThVb +xan0AToqTIYOAUHzab0KPW1pU0kSvI4fZ+EdXblfICrwOOyUY40DA5oYjPQgDGCH +N29rJbOupQjSghDbKU0Afaph5p/r/LfK0ZdrJ8NWbXP2vw7D68Pd6mcCgYEA9zvB +X5xBiL41DANRKyaLOvO+d3TYeOYNxIZ08EhrZ8Qopr1qz+FR+NvJuFMATSsxFciV +AlUFwTDjV+KBO+Q5+vfmqXa6tFtnWChbnvQVd2UH2ijoEICpOVqrgQ98Lo7GFwAF +ie2vbtZ6gHiEkY4mrE0Ed5ZEHcgMg3M3FJl6KOsCgYEAqHmGIEoVc8YwC2Un+qjp +ap1BGpUOOl7l9ScFzU8fS+i+Naa1Fkg3wcki+/N60mB4uQK9wiTXzxDO8LWzZdn2 +PKR2jay77SUN5HA4niTLP/LM/fYD1nz3NY00bapWbEU5Vv2mvkRgG5wT95FzUD1C +2hxkmFSVIUVw5Kh22EO7AyMCgYA69odh1BRVFnf0S346OlEOWD9eogFWpBc4fZC6 +Fat37fJ8AcTNm0E/hruPOExkGx7zwQi4q6pZrt0pgp2ruPyS/DiIxgY1KW92OPnQ +6QmNlgzPXN4rwWOaiCNK/nRxbh+UMK8hfAxQ9Nn2dce4JxzWDvA3zSClsaY6W2oR +gaD+zQKBgQCDYbtHAkMjS46xItF7iQGQtwwtI+LVRXWb/WJerRdwU/2/brUlcrwZ +GRWOfgyVSn1WOkM+totVPYioRhkRnM6Zm2WJTy80nvH4EjMZFzGpQ3f4dTn1LkU7 +etQA5wLzKfGOTEo8nU/c2VzHhK3E1e7FyutFS+o5gN+sS19eIJ/OhA== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c8.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c8.crt index 7da427b21a97..7ba0586ce650 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c8.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c8.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAO4k/NUJiy5g41bmdZi+G1mdw9DWMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNVoXDTQ1MDkw -NDEzMjIxNVowEDEOMAwGA1UEAxMFbjMuYzgwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAIr6JyOARsRn4zJ//TSQ3eMigJDnDTz4qloEcGRhdsEmEkiVxmawSx5X -YZeqvQIz69axBViHQZEa1emNrgktmzR4swV8wUip8tJhcic7iegSYk40VG8w5YSD -fVhCa4JEjTF+H/0B61j+Gq81Xzqw6g4m8kJWqWCvICNDo5SKkBSzAgMBAAGjfzB9 -MB0GA1UdDgQWBBRYXtwMkHRZ58jOv4Naa7AkihBRijAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMy5jbHVz -dGVyOC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -ADGVl3fvVGFLIRBK1+qQK1LHGkGrNpSQ3R9q7uDSHBIP7YYp90SF9mwNah+Ma+0e -ZqkmpD85QyzYas9lbXJebE/Mp47W422xwbVqqCqU+w7XMpLLZUbY8LM0PKyuepJv -GETzjI58H2Z9kU99xW3aQ2dti8UcfBeoOiFJgWtyuI4uiqa0u/5+by1uBVW0n1Cv -pyZD+bWfFt6Aoe+8xkQMnVZSTlfTfgpUzWM8vDmNi4l2cSOUHa39zWmWRH+QEl64 -EF1rbGwxaYEZ4/8JAdmWD8yzjjUtmJjxjn6PpLOQCDkhTySlNFApB23bShr/ABD/ -KyiL0JK8z8EBOkUxZiXztSw= +MIIDUDCCAjigAwIBAgIUftkt4nygxwtYAIlYKj2Tox8h0BAwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE2WhcNNDIxMTIx +MjEyMzE2WjAQMQ4wDAYDVQQDEwVuMy5jODCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBALvDJBvmBX5New94tLP1NZPKJHG/GYmX9fIO+568xGcaU77Ohfgg +K9rtXJOLLN2L+fJlmbhVAJ1VHdNchQbHzEmZpZnA7KLe4vQ3Dxg64z5vEu1z8x4F +VKw3ngkTAom/ROTd9/UOhFe/Wi2kEGgWOsuwKQFAbMJSlsFoMs3YaAWNg8N00/AP +/6P6H8Zd+sXwmVOEshzaQ8mr9vYMVv19AjHnLRFuy2QcEHK3BH3iuOVp+xd9nPbd +pbX8tuDeVKiWNSxwkDbEcgfvChjQZoHShKRuQkk0W+5UsZVLuSOGiWLg/4w83Ral +F+0BgDbP8y+feR+lnBqz3d6xxMy1pLLQleECAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +ajzsQjTUk2i4HNwpR2liVoPjiTEwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTMuY2x1c3RlcjguZWxhc3Rp +Y3NlYXJjaIIcbm9kZTMuY2x1c3RlcjguZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQAqYrP6cjb8xGFMzMRrvaH4BfAXDD9QoC+x96ZZ +/JubC7nrhr0KVpJDaFyMkXB3mEMyvLgJ7FlVrl8gVkrfZMGXiBXck7tnPXpKhzcH +tvqtmVSlSBwLW77t2IfHLcwVse+wseLkiF9RI1AGIz+USI3junl0Yq8z7PE1U68R +GkEaFxC5ZBKphfB7ctc+8bAKXlNNxxoCoN8AaXN39mU4PSqDKt4D7bPfNJyftiam +3iEvCxqcmMqjXiGiPHbtYmYo37cwtaIl9lSwSaCHpESj8OO3LXuya3hgi7aDO9eL +wQH6p6Rg1SQz658KOMfBwNoM6uVrnWT212QWO36MedEZLo2m -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c8.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c8.key new file mode 100644 index 000000000000..546cad7dacb5 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n3.c8.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAu8MkG+YFfk17D3i0s/U1k8okcb8ZiZf18g77nrzEZxpTvs6F ++CAr2u1ck4ss3Yv58mWZuFUAnVUd01yFBsfMSZmlmcDsot7i9DcPGDrjPm8S7XPz +HgVUrDeeCRMCib9E5N339Q6EV79aLaQQaBY6y7ApAUBswlKWwWgyzdhoBY2Dw3TT +8A//o/ofxl36xfCZU4SyHNpDyav29gxW/X0CMectEW7LZBwQcrcEfeK45Wn7F32c +9t2ltfy24N5UqJY1LHCQNsRyB+8KGNBmgdKEpG5CSTRb7lSxlUu5I4aJYuD/jDzd +FqUX7QGANs/zL595H6WcGrPd3rHEzLWkstCV4QIDAQABAoIBAAbQ7xgErg0fXLDr +d7rjM1RXxaIr9u496h+lD9kbvvEDvYfLGUWwTEtEq+ROzHyH8vVL3yxYCJ9NillE +je84tFe1KFP4Mxb4CqrlL0r4w0xeTaR5XblopeHhHH91lVVhEEwTK0fWBLOnRouq +3a4bpXuEu/5Xw4L76s1fXh3HJgKSiB4uJ+71kseKj2cUgF0H53Tgy07sJ0XQ3Dsu +NWh8yVt5k6GqBntJgILeBLxaw/F0DFHluX8mBqvbsNv6Ssuf1PI7OBAvgN22Mi3u +0/VH2CuH08f57uFLaV8ZOITCNkXWF1l7TvsOzTjUdQmJ5T2JXgk/lsZIIamdjTE9 +n/pk+SECgYEAvNy/vaOIWA6FdXPChFlbDJaz3Fk0hGsP1a4RvPnpP0suZEDuUzU/ +A+tYb9SHuqOcISNFN4JSu1Tmq2x3rB4x/YHwpuz67577fZAS9oqCDjDIO8AoeDd2 +lql/nZpkVlrlNOWREECGGyTLy7Zlf2xs+IPITUchfAzgfncSPoBUZ3ECgYEA/oJI +4WZeA32xW/WfP4sZ4Blk7g8igVOX3HcFa2oWisWWWoXV7jrWBc3i5NbRQbosBy4J +17q5Vqqze/m0TfmgGqkK7dTNcd6s5IaWTwO5KlKrK5yzgmrGTp9/REdnaUxMhwBl +B1HAwFJ5KMe1tCEtMfPNfOTuziYZ35U81JT5PXECgYEAnQfCmVAOfoDccJLmihqt +vKLVO4IkS3GlQdqwvvrf+BNvqEXgouij04NsDdedXw7IMzlbRNMkLMRj4uaV/lNA +53KSAIkMAA6lNnqDeeAiUOA4rI2SWJptDMoz5HvJr492AmxaGDQ+DPWkSWYHsFIb +rOoNsktRSQ5/3kbssXEMPNECgYEAxHeQiSc7xdgetM1sTGI17E/OIRNf4JPGNDNQ +5LV1h1S2F1UfaxLSKpLf5yWJTgLIgBZjFLyY3DVT7o05ScQ8ergprqTKn2906ciX +eZF3PBuq9TS3QiLoyMLJPM0eICP9I24Dj13lvNEtpHReaRn/SINbCmGzk0HIUVdk +KPqDWJECgYAwTeSbX3vfNtTc/XbKJXjN3Xe31xwd3CKV8LCtA6GKCPJbnS5fZnt3 +MkPgvP+hf0szCK3LLZUP2I0njfAP5wZeu6KXpSygeG9yGVzbg1Z0FClwUujjzL/U +K8F2jO3nh2O8spC5RbRU8JNyBCGLwMs7DdF0K/tDitvJerA0i5Wy8w== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c1.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c1.crt index 0229d50acb14..01a67be3e16c 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c1.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c1.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAO20mZ8U6BS1TKy9C/jR5Fiz5Ey4MA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxM1oXDTQ1MDkw -NDEzMjIxM1owEDEOMAwGA1UEAxMFbjQuYzEwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAJbrB0ic/oGENs1JDhRT4o23j3AwbeUE/CEKZqwHUIavjvIHcD8Vay/0 -TpAJrG6EoriqlqNN+m+Bm66NENnyuLsj4X3q4LyX3WwFimQt4LQkssfv9+q1kvac -UgKyytoZ3ummHI4bhpYc3Bq4SrOQchLd9EZBCvURMKdDPjxsaAJrAgMBAAGjfzB9 -MB0GA1UdDgQWBBTLdAFCf9QVjWvJ/FUvgZl8vxkmGjAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNC5jbHVz -dGVyMS5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AJtTgYL5eBaKTYNEWgehivxg5a4uV//zasVQHt6DX1WfJO6mYuDQl+OTdwjgKPKz -m2ICAKjZ2PV066hzazrVULpjgMaUeRNxzNJi/5QqiJorNSsFVwSYkMjDB99KgWPf -22QTx0mV8141lUZY49iOnLk5u+h2tAhTF9U+S8MvXha+vyKc0SQpJcFurNxEZZvc -x0LCfLDixW/xs7QcfiNO2/Jn5lQf6hIOkyOUHRfsGs9QXlPyjnhvys45+CQb5Cve -v3vEDnE2lRe6lQn2k2ZOWrL9cyYfROcJfQLfzoKpNdr2PaFEd1g5Sthznv7KrAQ6 -dHr6b9xgGbf9I349zliuhnY= +MIIDUDCCAjigAwIBAgIUKdYDDACrr711ztIDYAPYtIQDDgAwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE2WhcNNDIxMTIx +MjEyMzE2WjAQMQ4wDAYDVQQDEwVuNC5jMTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAPPiQKn+A39l9TS5P8KJfu5hoh5pFsaDkMT5qSrVJkBSa1DLOYzt +umxQr9Xwg7rg67zvP3IIsDFXm+vgXj2z27JMcVJwMECen5WFeRdedI7Ph+Uk/J14 +q2Ew/Y+LLG4SL2nHZZpMxKvti502M2fpPzjpZ+/0ivXXAQS52LePYZrK+nMJq5oc +u6aOuJo/pAV91Jr0mUIPFPl8trNaZfol3OKBDHkxYKlO6R6bR2bLIUkyVD26XnYQ +0pbcEjGL1b0uJ0JavvYclUIiHdxplIZynO/qOSD1IXd9JEBPYJjlDQErfh7D22l0 +DMQrOILqkDojlosM5891H7JQFOxFTGkEpwECAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +NgJLwqdmEv4JM43jmYqNHe0yGB0wHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTQuY2x1c3RlcjEuZWxhc3Rp +Y3NlYXJjaIIcbm9kZTQuY2x1c3RlcjEuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQBbPl4px26ZSsGn/DTwYhf6XYug7XD47S/SlFLP +ji492jLW2nwecIVAwWnYkwiy0+T96zSrQvyIn6YVGmrw61/VOYI/3YT6qSQvSLid +TxyISqCl5hvgx2zJ11retwpg3w5Ro6Ex0OKKpTQBg3QDsIk1hdQ8Jr/jwriG9zHr +E0Gz6hUTpUc046NXCrFMkK/WkvCW7vqJG6skixcGsUmjV2CKmQv7QLqurIMNOXEQ +uXmjs1NMxPObVxVvi/eWhZQountdpOr//uIWZvGcPJvX9tQurt86+0t5t6fC6lgm +cSrGJifA40LRM7RDGC1m4EOm2ozinWOzsAun0H1XsB8xQDFC -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c1.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c1.key new file mode 100644 index 000000000000..009119dcfa82 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA8+JAqf4Df2X1NLk/wol+7mGiHmkWxoOQxPmpKtUmQFJrUMs5 +jO26bFCv1fCDuuDrvO8/cgiwMVeb6+BePbPbskxxUnAwQJ6flYV5F150js+H5ST8 +nXirYTD9j4ssbhIvacdlmkzEq+2LnTYzZ+k/OOln7/SK9dcBBLnYt49hmsr6cwmr +mhy7po64mj+kBX3UmvSZQg8U+Xy2s1pl+iXc4oEMeTFgqU7pHptHZsshSTJUPbpe +dhDSltwSMYvVvS4nQlq+9hyVQiId3GmUhnKc7+o5IPUhd30kQE9gmOUNASt+HsPb +aXQMxCs4guqQOiOWiwznz3UfslAU7EVMaQSnAQIDAQABAoIBADuTFCqyUxZmyIw1 +iF60CZ6fmOOG1jpt3wpO1kURRkPGHlLhIdDMLZLR7vH6y6L12IARjVUEzE1xUjQ/ +XaYC7nLSe4ggwZi3ZtI2hYrw1tYf2dlevJqW5H5cYkQTQiotJQnY3HfhstUwtL8V +g18pufjkAzC5csx01dnasBa+3p/Z5VcqpOhQxOsJVi3ccpDi4zEe1LYziIlelEbE +acnXLX/8rrbVTbzw/spGCDJyKhmT1G5VOsJcwDDPD8DY+ocMZa0cbDDsU8YxwhHa +rZs2O3ZdTIRFNA6ocTERGuCUYTbLFWJ+p5tqWc23LSPI8IqeO7KKbZ0+Ryy0Dwnd +lH5lFXUCgYEA/b5DG/A6t5lwIm33Fvbfd3j64L6Hucf1Z0tE0ER9NlfeDe1Te2up +0lZX4LxT/dNcI+ne6sY8EuudNQucubUzrECpv+q+ubd4DZktLwDpTZId8AzkgUPu +ZAnU8c37vDgEN6W6hJ83mqAWkOM2fsz31mzN2q5J2FXItJWhKuSt6rsCgYEA9g2K +vNa+KQaLCL7QoKw3/PZylV1zQn0qBjnoQeWNaZTJvsxHPC4S62T2RNZWw5UVj52I +gU09BGfCbcnMUrQ4AJZbsga0eD7DMnk92Cmz3gdSjD9baRUmlnvwPUodoB1G/6Ov +EXOtb085iEg3IAiG8S6ik6rKFCiWF5i42XpVz3MCgYAD2TjnWZNqaXdb7lMLFB97 +jmgvtHQWi0Pblzap+f/0LQz/JRRZEN74g7q9e93CQcxp9wgcg3kxyEUQIubzoh3w +hOKOOJ8/7dpySlp3Fgpz1L2DgI6Or1sURVnnePpSdn8J4QedL1UGkVsGQ4v2DGHN +7MSKho40K8jphEN3hRRmNQKBgGK1Dgoxj7m/kTkh62n5jMBjUZZQFfP/aEN1KZJ9 +91v6v3BWXOFsjlR2fUEiR05Q2YRyqxdMGAVTZrFKejhXafv6VbC2deGjNAVklAXK +vQh+IYR3ykD+pitKbxb3S6k29YuKa6nxoQr9WlQsvEiSgyfwEHJy2nx5x7SRpN5B +8Hi3AoGBANjS14zMpLAMVJZr++kWrqBPZ5mzjhb9DdPSy/CfQL7Qxq8lPPY7Pvvs +tG4rsmnIWDoO3ReX/QD0x4FQ2IjXrdDN28okYZewnWy6r9bHpUYraXXyhJSw6mkZ +MPm4GPHHANNoanWZvg6fmPRH/VRiGW1/0OxEnUG2a9P8InvrgYep +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c2.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c2.crt index 38c9f20e3512..255f5cce3918 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c2.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c2.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAIIj3+gGiW2qnF1Y0DJuJBBFfM5MMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjQuYzIwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAKj0cnL86BiN2VBhvrH3XDplcwd8lFAGxAzAfP+Sx1Swqd6y4hZoX2Vk -J43AuYvJY7aoMK4xIQJ4lWbrvCa+5epbOY1GGg82Z+/Od7mfZppfajCKTAmAIXfa -SW48V/fRZyYl3KDfP4h5AqJV02wAJ4g5r7Olm74dwAMclLF6FqwfAgMBAAGjfzB9 -MB0GA1UdDgQWBBQ9SpGlYooWMcJYNuTND5SQk6EPfjAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNC5jbHVz -dGVyMi5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AGtA9aht6J9uVrrow+ks4IxvSTeDK055rdM1ryCkszKf/QPzsDfj0VONEjJM2GhE -Wib6KjYz6Lki7mEa6DY1IxPZHzMjjkfeLYmnE2+zgdGH0X5D1bgO7cK7tWmcuVAO -eN0YxO67av/PZT/00wLYva1ubH+zY+C5AZYOxJpPcJ8JNpwO73UDFZwbnUzvpK6b -mFN/eBqvB7XWViJcHgoXEHkYH9Fetnpt8T6EYxWDL+FYi6iN4/L0a60l+emeleha -fcbjwU5Ax630EZAEecRB1D2wYxwOIFk+CGLeXpT2bSgvtmfU/pIHPeSJ2lSAydui -bVeYBdRoOZeUpX3LXO2YuLg= +MIIDUDCCAjigAwIBAgIUUW4V10o1g9DeT8YquG+IwcE2/qYwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE3WhcNNDIxMTIx +MjEyMzE3WjAQMQ4wDAYDVQQDEwVuNC5jMjCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAMEYP3CxCDOvSxjCS/Qz+9icBl7mrBi2xP5Q5166pSOtaoq0Gfoh +QvTpa/s9eP5Z1SOJhms2PnL5mzK8u8531EYRkkAiCyo8akNcepVT7FQf8AaNeEX3 +iD0PSP3eg4EpuJW6E0YTG/UMWWbrYoAS15b4a1Mm6komtK46bnOuvSHvSEsAntiv +ollE+CNZBN4XbAMdWq5bLdI+Py5ym2HlaVodygr6VRzLbSaL4sLmeHBCcXswcSuB +VGxZfV9oBSns5pWRrGqXKTHrR3+E/0UH93KmsS7MWsj/gWyRLuVgqf67fyjH1iP8 +zI3J0xw+WS0SH5WFCR+8tKQsM7WSALnW7fECAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +1t7irljwvS+iZyIUmPEsB7i1d4UwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRYIcbm9kZTQuY2x1c3RlcjIuZWxhc3RpY3NlYXJjaKAl +BgNVBAOgHgwcbm9kZTQuY2x1c3RlcjIuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQAVIuSIk8J4Hl3YQK3pBe3zMgHOxmZyGgO1LSLK +cii74UlCxgF3Xte0mPLlD3kbjgQAcqRhnF927U/4GyEiJedUFpXHyrGpVqX5/QCs +6wvLQZKk/qA3DwJKCRRr0MexTjPnAV6ok3ZJUUTmMxP1lFHsWYcNssnclBKH+BAd +/1S6QovlCeKN7bVgSDpaFF3EE2D7tPywQ5O1t6w+c24Vy2Fi52r1fMK6/LGWqSQN +8lNBh1oRzdXu9DffS9dTjd33o+UYackt0Cm+fn9oaptRC740KWkDOP2oWwr+Z3P2 +9JI21Vu5nG5+NmH2wVikLTUoDA+3xPD/uLcNBx/9cpQykzrk -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c2.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c2.key new file mode 100644 index 000000000000..c5eb9e9406d5 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAwRg/cLEIM69LGMJL9DP72JwGXuasGLbE/lDnXrqlI61qirQZ ++iFC9Olr+z14/lnVI4mGazY+cvmbMry7znfURhGSQCILKjxqQ1x6lVPsVB/wBo14 +RfeIPQ9I/d6DgSm4lboTRhMb9QxZZutigBLXlvhrUybqSia0rjpuc669Ie9ISwCe +2K+iWUT4I1kE3hdsAx1arlst0j4/LnKbYeVpWh3KCvpVHMttJoviwuZ4cEJxezBx +K4FUbFl9X2gFKezmlZGsapcpMetHf4T/RQf3cqaxLsxayP+BbJEu5WCp/rt/KMfW +I/zMjcnTHD5ZLRIflYUJH7y0pCwztZIAudbt8QIDAQABAoIBADohRBv3V5pDp85s +07PRIHy9v9c7B00rXJOZJIYql0Nt1fiqrnVUJRWY3daVU4Ly5Uzc/nfxLQys4xF7 +quhFv8RWTtp6xTSyWmUsBoVE7g0HTlmSpkYd9kg3fUhb/+fW+QguK9Y5M33qeVGY +BB+9TGTZJWozeW3YSCV5Vr4BuVNo8PWkb9WROmQTUpVzCCnNJFsqNG2QUf9kh6B8 +BSxb03j2imvhXT7Qb8fvnJkFryaXnBDSoK9CSxMbgYr2qqGsE41+qRyf9YWLEHIh +G7IsVlq1/CKyvuP+6bk0wgiATXvxafmT3KpQQEwEF3D+LOiQVr8J+y3L5rcN6Coi +nKcBzRcCgYEA72NpKoiD848Ay6MXwzvJ11OgI+wYgwzrF0yjQiFAAwjFPtAmo4OF +2s0eDK0DJjO1N2rD7WCueM1ojQGaWixglDq6N/Y2XET5z1exQ69bruHMVDKz7/hT +r9d0tjWhpvqXbBXzb6OXlNtwY1BhKOHEo7TdpEcNy2VgYu2WgMpeuy8CgYEAzn51 +gsE8/8XQHMpm4Jr8xhyWFjMoLuxmcZAviljMy6O/H4VwVQ/JnZTOaZUq3AAQIm/I +5nWbfl4GEvWgfhWWyj2yJiMmjt7HhQz65su/d6WTsbZcMY7AcL0C13trCNoaBQIb +PKjn7L2CfeFRRnJbDwjCGDxJ4JWxAwc6VhnUIN8CgYEAhWH6ovxVqUuwwcYp41pY +6GVsARf3wG1eQCkvfhVQuWAVpx4LqWgLGRLY/28XJh4BhtMyP/1yuVSaQJb4De5U +EcN3Jt9FArxcBK9z0abPiu29D90AbWEMIBoIqc3QOF3AKTVB4p5gQ0zRnkSXHlRN +YHnSVUpR4VanEfgXt8MKbXkCgYEAmekIDX2aycnXZJ44MBFVdxS3ibR3rwHlGaAu +zx+BBgf23LfJEp3B2QKhF3FnkLM7U2+efqgKThYm0Nrj1RaiXfu+XU613qeOb01N +ddlHzRZND5is/wM3zNAU3ifPj7mAy1rWklATDhHywYC/45I47OvKP+Yv6oWoemlY +wfReeGECgYEAhx0cP2atYt3kzz0UGzmh61ShdJ4xlJfNTOeN1xQA/4zc91r8/Ht4 +3CxjN0CZZ5BTiTygQ88FEv4y+bmmpkIq1ztDJF8dH7C/8WhtGmgFvphuKKkzHtUS +sSSpR19bUsBoRQin95/WoeeDF5zPXLP1ax9i+u3PfZCP29e2ZUaTpvI= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c3.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c3.crt index 2a75d1584f92..eccca7f58adf 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c3.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c3.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUf9E+9p3WuX54FsOh8veaBlZx8ZAwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0 -MTMyMjE0WjAQMQ4wDAYDVQQDEwVuNC5jMzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAlLG3qMUCPi2usnc7jsxRFHVGjbf5+HWY9nWaboIQ1vHfn0Z3m/hOOCvx -waNY4RF75V1ZSeRsAKzGeDD4NC1KC2Dxa81elMfbXq6nLDIfh0klkK8lSw6snzpe -8NDMSJ8agNdr6zLUgL9e3qh2pU1Fc9ApR6+HRXA9rcnqp/xm4XMCAwEAAaN/MH0w -HQYDVR0OBBYEFCz38GxHKudMx4dyBlrAcIB+8XC0MB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU0LmNsdXN0 -ZXIzLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -PvCi4fiQ+711I/HQqE1YLGv6Z/Fys39CV2Ch2f7VVgb0o2O9UVpvs3UHxHwNOJpt -7l3yap/x3Av8lheE+w+qwlf8M+JZMrdDkMxevc/sTT1C+Pq28uet+RfaOdDulL89 -/mFD9LP/kJUhZ/nH+/OONWJ3j3oIkniMNZ+fUktMp9b/efDGD1xb6BDqfOS6JJ1s -NA2+gncc7uOkPZzOOq/cxZptlU5jN+EFu/o5RHpkjA3S6yQBav8SgahDgRGeorUn -WVaOiv1OdX+LuJAfZbGgOOxBi43zKT/TDhITcTTHCjmNKX9rfP1lrUR6lmqmFlwK -L9pdLHQ/cPPNgWa8C2Cffg== +MIIDUTCCAjmgAwIBAgIVALtUuorY5wbj/mFSIND3oGNl8ChaMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxN1oXDTQyMTEy +MTIxMjMxN1owEDEOMAwGA1UEAxMFbjQuYzMwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQC/J5elXGQOlkp/bulJ+IF2eIk0ZaQRQT8QYttSbacCMSLFnmaq +BAeEosGmVdpQc4u6mjREh84XJmFGTQEyXf2BQWajfoo7FrVkaN4wTfwZs+1+wRdS +z3fBKkfQoFHIehQSOKE+7wIUkkBllsw7Ku0TwEGtPtnRK1KbY1VQGSURn8ISjJA2 +d6xuB+BCYU9rPhDRdOrObvp028yr18trWniDuXpALPaEMR8fsXeII3BnB/OLMvyk +jigfe3GZv51lutASe7XEjUaBC1AXDvns13RLUvSRvaAu1B6ftQPohkU/EPnTfvhy +xNuOap/5of263E7/qM8UhbM64edMVaEb4aSpAgMBAAGjgZ4wgZswHQYDVR0OBBYE +FINeMlqF+VVaJAOG4bEiybLgcK0NMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGU0LmNsdXN0ZXIzLmVsYXN0 +aWNzZWFyY2iCHG5vZGU0LmNsdXN0ZXIzLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEAmJHlZEe0RmQERvMDnG+lP9QTjz5KUbKoxGXO +/4xoKQk6JsHbFGFt7yzFHZHjVgPxuPxD11j4vW8Acdw/x3ieWEMBVObQfYrOZuyg +pc59al6EyDgBGZdd8FmqWfaty5fKNX3mtfhVYzKcxOnOihRkhmmB5EYDipNL6lwa +pBBJXy9mTeTXUnn/kmiMgCP3CUhW8Bp9RAoRhpMko+3yjfmWUaGd4gVaotS5OABr +Mg/T9UCrxFI4oQPshttCNYtzc2aFC/1h49nMAA0wx2IfP/w4czq+FSBcIeE8QtKe +TGoLpdXRVwiS3/2rV1SKGHOa6cLLziC+uS2+6r6rqHhekP6w7A== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c3.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c3.key new file mode 100644 index 000000000000..90c454c17471 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c3.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAvyeXpVxkDpZKf27pSfiBdniJNGWkEUE/EGLbUm2nAjEixZ5m +qgQHhKLBplXaUHOLupo0RIfOFyZhRk0BMl39gUFmo36KOxa1ZGjeME38GbPtfsEX +Us93wSpH0KBRyHoUEjihPu8CFJJAZZbMOyrtE8BBrT7Z0StSm2NVUBklEZ/CEoyQ +NnesbgfgQmFPaz4Q0XTqzm76dNvMq9fLa1p4g7l6QCz2hDEfH7F3iCNwZwfzizL8 +pI4oH3txmb+dZbrQEnu1xI1GgQtQFw757Nd0S1L0kb2gLtQen7UD6IZFPxD50374 +csTbjmqf+aH9utxO/6jPFIWzOuHnTFWhG+GkqQIDAQABAoIBACiiFucss4wyQFXE +UszJiWOQjcHYlT6huvEVwn9MFcMuhLCzUilzv9Fe84ETINwXI62f19p1b8aV/vl9 +pvJ47klKiLHaVTHoSti0Pf7QpPUlvSdDjlypA1DJVwiu+5N898VDymrhTXTn/c5a +dsFhB5I6PPvBr3V6Gw4y2FC1d56+E8H2x01sfH/TgGOHhyrhgSquI7wQYU0hk5sU +OtCKqAxe8wv3OA/WXlbdZUpdbFShIo/Yp1DzV5cIpquFtzv8Gyl3/wgpb9sIZ9MS +dHVa6zE0hOCJdmOPLzKEgbvZSU/+oPoanZBU7KP+JrmINVirf5EfBlAjud70vBEq +oEc7LnECgYEA0IhiKPSuBs2si1bfh1I0Ags+I1XFjvomJcgATTmoAXEgsWYYZ6jk +GuXzJtKEJK6d5WspLbAAriIh89RB8LRHvIurQwiloyUBVqYMyfXIcszGHWp/TCcW +VDwitYyMHTHlnbUZ0eCfKLH+HdQpmkO8dWD75FidOZ5y0LzNbPcOL+UCgYEA6qqN +nKxRogTWmItKTyfuG0RJvGL+ehjzMuxx5HZcUxwY7t+8Pbls6hvLga/IqpidYGob +8Vzq9xHx6N2AashulvrgPhn249657g7IPLjBjuESGo508aSqDxXA3fHtYcjY8VbM +puls9mCEM1rMqhZuZ4KV65OI4aUiF1Df4xtorXUCgYEArDLTDArw4gFZKD72mqIQ +/FcYWslrOCPs+GnthdfEmlw2qQcrw/29T1DvtvFD2Z6wGFCjUmzyQtlAvD5crU3R +pyqWo+YyEURvlIPOL7/cZKAgQQ4+TD5CvBBkgRKsaTdY6yAvUBZyl4JT24HlUNP6 +fds2ZlueiYQEP8aIxCBHANkCgYB9MrNM9G61Jymqxx3FL4OMjplcZeBpGeiesaFu +B2vod2QiBcIzmDswh9XCghtoUPYd216/HhjJ6owK9iouTnMaIA0FdOrQ1iKSjNZt +9xBlf1UeUAS6a2TDmZKFly5T7kk4MVwMbRtSM1o03+uzjzYmCXic7+CmqmiO0fow ++42UhQKBgH+Jkyv0MJNq6BLfOnVR7nwSiVzWmkxLS8YWak06ZQOdBSm66RXuy2Gw +Pocfh/9iwU4P4yygweC+mJHIDWICUsimfX5Nnu2tpE52skhQIwJnhQFp5fi7okXI +nZEu92304WhSaT08BfCgzfvTv6ALllZJJFDdG1bxoCw01t24dcza +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c4.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c4.crt index 141a3c9215a8..a1004a51976b 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c4.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c4.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUBKLSOiGqfamUa60JiTDMc33jgg4wDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0 -MTMyMjE0WjAQMQ4wDAYDVQQDEwVuNC5jNDCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAkPA4JMal7nY+H0Rcyr5Kmvu4uL6HG0kWw1XLXq/ORJ+NoQqGJ+X3mJ4F -HPHWb9l6yfumutK7HTSUx6voZzgFVG1OcZ6dUakffCkbjeoGVe+4G2ojlwfvjQrr -JhoB6aXJLWRTDY4VrNOgijlBzYwyZHFF4IHLAWD/8u0RCGLdiZsCAwEAAaN/MH0w -HQYDVR0OBBYEFANGShToR+wz3wlqm3RqJGVNkBXvMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU0LmNsdXN0 -ZXI0LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -Tots0b/0b0tceD8D6XlqY++Qg53berDvwBjEu2ntMqlrXh4r7h6fOr9pmJ/Tf66L -VJVnYWG19uh9owI5pO30/C603N33/hAljDvdidQZsaKU1KJ4Vun09qWstRF8PbZO -4OxTzAblcZtHs7DGhKJL+fovS1k2hz6691jfxFXXls3S7asOnFrXoCDkG3aGvvNp -+HfygNewAQSnimKKzzYHbwqqhnXAPQbI2nF5h8rFtoew3sd+Kdh6GTqCPZWntlCX -hEG1+MS17pms3DuiJt5TPsZh0piJwkFksbuzkYEwH8Hg9Xvi/0ztY7J9wrCqicUL -r9j1FaOwi9QlwLVdFAEQAQ== +MIIDUDCCAjigAwIBAgIUAKKEtzuqjiVlpL/KVrKMXSx21H8wDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE3WhcNNDIxMTIx +MjEyMzE3WjAQMQ4wDAYDVQQDEwVuNC5jNDCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAK47THKQz2mSn6XosohLlef99gQe7q67qZpyBOR7ee26BHMD4c5a +peuCqqbs2+CTIrshp0tuiT5L6t2MTRPquQaDXieZOp08YC9BJim/cLqlTwmnIM/S +9Q0wvzDCKs9L3nU1EvQZ4CFtJJZ0G+yivkdKmZVU8qlNLCq5p6+6qZYqXAxcW41+ +UuP29L3JF2Si3glhx76SfD4ThtNjN3DoBSafCbbNet5XhXDq+YJGbcp6QU+Hshcw +lOKxDs4liws/l+AUFTskoCtLoPp4GaH/e83+yGEwBLDGIDp1Ykhdnoq2YGYiQ2ae +7mGqEaOJO87ZpsAHzFFRiqHI6iPFb0PDTJsCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +NJd/ROxy5v0Vv/84Vi6fTCP9fEAwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRYIcbm9kZTQuY2x1c3RlcjQuZWxhc3RpY3NlYXJjaKAl +BgNVBAOgHgwcbm9kZTQuY2x1c3RlcjQuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQATIYFvnBCSu2xoEQHmCWNKCC2n0KcVnZt1jsr5 +i3mQxoeAdP9S0dC1gKk6jRWSVAb4seXKQazapQ0cUtfpnc/J5gUsuUm4RFz/IADy +Xi+3lUBNd1alK7LKbDmRt+wV1+HOmJW07K386HZtUeSD1o+0FnEzpRiXpkrwitGn +NDzSRjRAJz9rnwIVacD+T14LtjzxxMn04ntnmH7K7ytP0Skdtoel5wzsLJMLWhDD +MtBmwYU291lTJ4sA8Tw9cIU98W9y069N+Ymir4a6DxpzxD/tXgoLm8LzxIo1mQK1 +xFxiE5I3kaCcDlzSL74v8HrhRg5S9YDrK0FacLujsuD7zIkT -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c4.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c4.key new file mode 100644 index 000000000000..be8d3c5d10d0 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c4.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEArjtMcpDPaZKfpeiyiEuV5/32BB7urrupmnIE5Ht57boEcwPh +zlql64Kqpuzb4JMiuyGnS26JPkvq3YxNE+q5BoNeJ5k6nTxgL0EmKb9wuqVPCacg +z9L1DTC/MMIqz0vedTUS9BngIW0klnQb7KK+R0qZlVTyqU0sKrmnr7qplipcDFxb +jX5S4/b0vckXZKLeCWHHvpJ8PhOG02M3cOgFJp8Jts163leFcOr5gkZtynpBT4ey +FzCU4rEOziWLCz+X4BQVOySgK0ug+ngZof97zf7IYTAEsMYgOnViSF2eirZgZiJD +Zp7uYaoRo4k7ztmmwAfMUVGKocjqI8VvQ8NMmwIDAQABAoIBAAL6l5GD4U/0Rk9/ +XYIQDMWVJX4nizwokDtIRMYlSE3ktsvCnrs4+IpAxysodI/v5BGf/yy+fJE4mhDk +o7K5+iTe3R6/M23ZyOk163GeSM6gIexP07NJ74+krDgosbOun+SQHj1XLepUY+JA +pPOUSr2MTHjtpWJ4o88tMm1CO0EKfCRgt1T/PrDu8HuhqBBiTANO/3WZneiqk8tm +6jSpbnL62AxsWo8YjAIcsCvpI+iiRpKfxuTPRcwfQj856YxHXopqsQyIg5hfTK61 +SdDFSMZFglnkaUrR4M8YhH5W8/Bei2PWkdk9EdGaXx/QbbmnOOmQkrTz3E8t8s0P +1IZNjwECgYEAz8QgHCFfp8gvsQKpbtvQkP80U51Rf7DAVLF0hZsKsIsebRRV8blE +IR4mtnuHCSgnYZhYSWSq+KLWJYkJidoF2qdHHUVraTabsUOmZ1Vodm1pFbD/d4pd +ymAZJDO+l6VXyBf69/01QM9E2LSh0wvIrgwmb4rsPDVs6UaWPJ0XI5sCgYEA1q4q +Q9QriK7iA75KVmP0mEvcK+lC7S4Ob3s3UsZMHOGTVwEwmZsrlfqllQY4LjIaZQzl +RDyX+knlG6ahI8MCCKppP+BY4q0zSzbiBtHDymtfS1QOd58AH7YwA2WC+cyy10aC +TAbY0Ld6awoxpx2NW623CJQAuE9I7j3MBbn1iwECgYEArKCjoVvlc630WloA15iS +ifdRzeuXzMXaeERdsakpz5g39p//7PFndzDrL5Ihl+0iE4gWSxz2pdH+pn9fSoe5 +d3ynrN2CKpBx+m5gM9mWC1hLCPfgu71ewojoOtk3kh6SejHal/RuK+RLqtci4g0X +d+2mfeVq9Xfaf1Tj/Yb0s+MCgYEAsWfwCWl2KFfM8qb4K3gikxBte+QhqHrc9wdt +6ntwPpWy+r1C+k0esawV1Zb0KkjBQyxq8f7Y4L7cFAOZOkJWle6tV1oAWiIjN069 +bkE+eTJN56Qo6HV+1rXp64ShveiJChl8Z3NpDOHyyi38Zkunm1Fs5ivYhHcQn8B1 +r23DGgECgYBwFa9bJtVx71JAw5ZKExPwjrXBSh6dFAxclANmEinXMkUfrXwLiVup +N2pTFWcJlGBxXuXaGnOBqE9ZW+8Q19Y8cufKbdeqLaRhmDwG200abBjSaDQn6fkO +ttvOKRmgotRwxTeFzl6E6VPC/axP96TX4fKGZEwrexZRcCD0JzLnCg== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c5.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c5.crt index f0e9b704cb1b..ed9852ec3bf1 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c5.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c5.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVANsQ0wa8/+5Cod0nsGdhVVTxFa4WMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjQuYzUwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAKCP9rT1CKEdFPbGE2ywJgZiX1qf5H36DwokH8diz/XVk/t2QOU7Hqui -5HJkQ6Du6ywPXOqAIg/Iv3Rj1Vo+B9jcPs/DY0Sj3mKdZHhySaoTHPWjv7mVa/PL -fMElbmB4sYkgfotHVxUir3Xw5byGkJ+SjG+xYrMOSktBNph4Zh4jAgMBAAGjfzB9 -MB0GA1UdDgQWBBRrw5xcRVozWKOs6ZHd4Q524MCFyzAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNC5jbHVz -dGVyNS5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -ABG0KkEjBMa7QdsiR3Fi51gX5w4r169yXmkh52nYOKw792zI63TDZ00WNIMAZg6y -PG47XTbF2DHGsAuPbWYTDO75SLJT0f8XKAHpw8TIdv+zly3CKzXN/94R1W1HNUuM -lTSy3gbGy3u6AkZXivbtMNaR2loBGsMAQCHDM3gmi8PYsIaL+EDekMUF0NbiFyBY -TVEGYERzvU+ftXryGn2BML6ODKGGDcSlzTQb+tSSC/+bfoaouYUcvU7DTHUpHE3I -f1RzSC/NRv/gx3HKTdlpNNUNqIANkyWgtKMBbzOhn0SkAlIc5Ap9QBYDIZW96Aj+ -/Vn5JlqUvvdbtbBbEz8+zyA= +MIIDUTCCAjmgAwIBAgIVAPlbDG1QA/jcCcRyqUXdE797CG4bMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxN1oXDTQyMTEy +MTIxMjMxN1owEDEOMAwGA1UEAxMFbjQuYzUwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQC72VZVHfUqYAOhdg9pa49bk4NAgF4MM3UZiF7h2Dzn+kiQptK/ +dEuL3pXYMGWfno8d1h3zfkKz8AvECOKFtrPsRn7ewRYmt/lMwwaVWFmtIKxA8qrq +zjv6civxQ3ryDU1C1nsjx2Uk2b5JFM8NqauBmm9LFRllXQmRWBqi8m5sBjGRPty1 +eU801JlVBClziuyfEvXdWNOCjovSICKiqSirsKR1NjylPYCEf6U9B7coIrccTXPv +VKjsbzkHbL8KdpKXgCPzJlSfevuIcuIUJzdJLcSfxjbio63ulVi2I3fa0tsWhSvR +w5GzEbZ/tOZCeveIT4ZT7OL9dyEK43fjC34XAgMBAAGjgZ4wgZswHQYDVR0OBBYE +FG8zhw68o4y6xzfClDg8BBDIAXxBMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGU0LmNsdXN0ZXI1LmVsYXN0 +aWNzZWFyY2iCHG5vZGU0LmNsdXN0ZXI1LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEAQEfkYC+lPm1NlaU5nM7Wh1CSOsO8GKmkg/Cw +ZD/bHOhshxgRnGyAMJGpE3ms2sitXZ0YYAgcII01iIzbg0G1xuWLCvB0mgHI+U73 +ajZwiKMdqe2e55CfY1h97uMYJ5Wv9DeRWNCww2eUZlxpxDSzp1M2gCuPGh1xUAUx +ClfqMRN3I9MVtMVGr075+ah9RyQvj6NqpvRdjhNGvuYFnRvSZV8sROijaBq2ZryT +iW4PyvN51gzm5qL8ngfNRfAvmjeRcBhPhcCx/AYj2uj5BhDezroefH81zL/rNGjq +/JT70GOKCjmCSSs1kKX9/CSxrPJmeDC8M3NG0uMP8Rkay/goqQ== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c5.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c5.key new file mode 100644 index 000000000000..a988edf3a051 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c5.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAu9lWVR31KmADoXYPaWuPW5ODQIBeDDN1GYhe4dg85/pIkKbS +v3RLi96V2DBln56PHdYd835Cs/ALxAjihbaz7EZ+3sEWJrf5TMMGlVhZrSCsQPKq +6s47+nIr8UN68g1NQtZ7I8dlJNm+SRTPDamrgZpvSxUZZV0JkVgaovJubAYxkT7c +tXlPNNSZVQQpc4rsnxL13VjTgo6L0iAioqkoq7CkdTY8pT2AhH+lPQe3KCK3HE1z +71So7G85B2y/CnaSl4Aj8yZUn3r7iHLiFCc3SS3En8Y24qOt7pVYtiN32tLbFoUr +0cORsxG2f7TmQnr3iE+GU+zi/XchCuN34wt+FwIDAQABAoIBAFaOItMOfjh7zEB6 +C0jqeEBcfJZcb7lUfoj4xqtuvUWwk/4e+C2ikurMcaQPTMwbPdukHOHk1CxgF7PI +2kN9d3MP2oaeVqwZikQd+usWrh9xVh2JE0slIwrXz7qX+s8MZQS+mjfWuRqdQCoh +bfRgMTgDJnPksXJ8x5tSitd5OHsKJg6tLIFCnY0VHUTMWADIIkDIRrhNW7sbV6Fh +0Ycedtfk2j7erY22eszqK2vGoDPtNSkmEy45ffVjr6mTCIbr+EB1DMahXzqColui +lL3idBGC/7BGVfTzSfYm2+0GIzg/5z77NB6APAnuSMZuwPL1lNfx28h7w/FhNDYH +4wEKy50CgYEAwueLIspSHkTfVij0VFUDvc15Dv3aaV+a2Pr2hRgoZuN1sLxmuksT +7752ydL88fjUqC7jKzeoWZHkpk2Hsc9Th19zIdSfTgKvOILBUkn7RIGFcOmjsqOp +GWKZ6m1EzwotPfs9GITTHpOshcs/M+iyh/8fb9Hrzl8wNaX4FaWExWsCgYEA9ruc +9D6WfWt8pCbHhnA0HqyMx1PWswFgR+uI08Sdz0vo9M0Iy/R5ZN15puRm6aIp1DEt +ohBUXekcXoCdXFOg5CR2ABvJtyhsAmGG4fRE90cMd05nVcyqc5LwgcEX3Rr35G/7 +HIFh6IWvwtNuoJ9WJuEJ6E+Xm6n6oBHGfCtgqQUCgYEAwQZhZHYs969L5F+xAmgj +yXDaKL+99Efr6mI5jTyMnsE8SGl975f1SaU+rcjYG6hMq7er4ZxAjVyQrybvcWic +F+Cu/F0/3BmKgRP189IzF/iSOG4bYgr1qJ9fQa0vzUAN6GmOa174WzrzTJoqoPUf +AtLmxm6G856P90HEvWNsZ7sCgYBMHNoxOnGAJT1hAhmA7nIVmFMhVkqgE+eYqB8g +fMHV5evqQKnbCQK9y9iwVDsvDXhr3fPez7zG75dxy6k/hwjV+C1yoR/b4ZtYLK/I +WmtAAOnmkAgunqFiX8zTqjZrXdtk2+qSA5OpgDwVFNZGtICGI9vDj3IMciY/ZNxf +B56c5QKBgQCizpxvD87D6xJCBWBP3f7P5ctXyinRp3KwaV2vMjsnoR916C65Ao4H +f+yFsL3GpqfZfejyNHvvQtNr/jyQa1Zull4s6S+NqMLBAhdyaxWPeQrsHLKffRr+ +uir3KIi13uwntmF/44TAYv59aGtPn0h6Cro0bDcKD8PTKLWJSpKT1g== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c6.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c6.crt index 797186f47235..d5c03b9a3079 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c6.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c6.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVANcbAqZT0PhoeOyE4LXgFN/20v+RMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjQuYzYwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAKiyEVPmc9H8quSkeyFE0GpA1yuRJfDd1OJ3I+ogifTkmZcKu3u1wCbx -v675xRdpRzCbB8+hUr6ZC18qWUDxXp1ml4iRr8n8bVAmENgJzzp3RLBZROFuwVe2 -dttun1SMiBlnlcUnmSCX7OCTJWrr3tI0/F8RIdaB+63gf2Eby2eLAgMBAAGjfzB9 -MB0GA1UdDgQWBBSg537AtIe7R0hQVRHdpCNBBw4I2zAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNC5jbHVz -dGVyNi5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AC94yZdlaV+HvgSBn/uc6cDYBXMqe9tW1ENkEpUTYl0Umyy50qxonlsTvZijKB6y -S9vcnvUMs8+AOGPgwvEszM3qY1R5co+Rl+CXI0sx2iZKEeyGDxAFDBldpGaOqV7b -AXKeyR9SB5/Kh8XZVyxamtY0iWcRqdc79xljkZZ1jl3GbhxkXPGtPYYKCwh3u6hB -X4cWgBD0cGvDi4yRiHI/o2ssKa4DGZIMKx3Y6aJ10l/7cJ4cWzkOaEc1RsUTSPtk -MTkO7l3wt0VgCZz3ow5huwjfKwQgKGsLVEZQiLJLCn6P+veo0jm6Bf+pA4vVKWE7 -fzmJuag5fMuoc0oQbt7BxWc= +MIIDUTCCAjmgAwIBAgIVAIE2Pyz8wcBcRBAD6YhRUv1qvIalMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxN1oXDTQyMTEy +MTIxMjMxN1owEDEOMAwGA1UEAxMFbjQuYzYwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDxwut+bfxCJvxtkF2ygK5wIaqpmYUTcmz05eZK8cKnPqNhwg8f +IPuRPOK9xva3VM/9SY6q3lzGnn6tUhu7GEJGACzLK1wPH1hO70lKQOm1O3+Z3V4Z +HyO+2bYCEe/fSq2xWjiJAdGA+d7HJFb+2jnaStH1n2ctAC2ulqJ0P8SnuCxDdX2x +kD+a5/eCpWaNfLPrEpcZuqdhWF4EwKR0tFvgrPmkBpfODWvRNj7NcbN2HcJeFfqu +AsUJKENCH09hd4Bbxn38YWm6T1x2XMSv/8VCYXKDeuvrZr4gcnpgoCZnY5R/CGxY +aZtUEjMKOwERS2TMwArBuOsk3S7D57u3PW1fAgMBAAGjgZ4wgZswHQYDVR0OBBYE +FMw0NbjiJCX5d1BUpW4Bk6h6dEx7MB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWCHG5vZGU0LmNsdXN0ZXI2LmVsYXN0aWNzZWFyY2ig +JQYDVQQDoB4MHG5vZGU0LmNsdXN0ZXI2LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEAJpn/P+RJrCDIkpRUV1TKr6Kimtt6E1qq8kMP +qC6lKiLpo9ODgzmSkHymtYMM7cZXq7CIt1JH0J/jlPMCIXXkSfMjgPCrkqWaJf/W +np0A8qFkGPFFHQQ3jbgcHVNdUbffAljm+T+k7dBfTAE820RcZP9V+22DdLR/BWDN +BF2zI9xtvZ4qGI0l5mF8CUMEKBsSFh3AGa3+BBhDv8xT3MMJCANa5Vrl1kIzFwoe +8sSFhKynKt89H6027hJXUXI/L6QAPuSP5FVhI0lewEhLMMEWxy8FakuMe4rgyJcj +98Q2Kd6TtNs9yOAt2u52sEap+K8BVsKSzEAr1bSCzI58/jto9w== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c6.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c6.key new file mode 100644 index 000000000000..b760e718ac9e --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c6.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA8cLrfm38Qib8bZBdsoCucCGqqZmFE3Js9OXmSvHCpz6jYcIP +HyD7kTzivcb2t1TP/UmOqt5cxp5+rVIbuxhCRgAsyytcDx9YTu9JSkDptTt/md1e +GR8jvtm2AhHv30qtsVo4iQHRgPnexyRW/to52krR9Z9nLQAtrpaidD/Ep7gsQ3V9 +sZA/muf3gqVmjXyz6xKXGbqnYVheBMCkdLRb4Kz5pAaXzg1r0TY+zXGzdh3CXhX6 +rgLFCShDQh9PYXeAW8Z9/GFpuk9cdlzEr//FQmFyg3rr62a+IHJ6YKAmZ2OUfwhs +WGmbVBIzCjsBEUtkzMAKwbjrJN0uw+e7tz1tXwIDAQABAoIBADCCafo+W2VyqSfa +1dIhW98IAlRxh3A1f4NMNf9Hr0UbnnDZHWujlJa53s0xhQOYMvuhrMzAPWFlIAnq +9cF+xp2BH3vMwXYPVgrQnKYXjE1fGOVos57azrNWv9x+eOEW/O8fKUTkZoM4n+jZ +c9NDOmSAqsxcJbmz1Xa4pvQob28vc2Vr1d+n1x2n4f7DrmdKvtig6bJ6vGerltEe +gPzUs0p0W1PmDUexW6yejxH2N4jN3BBIS/iAju3JTKVFnB7UDpz+aNIrFUufZwVD +n/VECHW/GVEzq0UTU4wSQWOF7DqJNGVZqJ22UafQbkPwcD92kmyFzx1g5+2ocabo +K9B6tAECgYEA/ogxqC2t4isCFiTmsUX1me+H7E7lwKrSA2QsZEaUO+cCmXkeSnjd +GL4LeZ0ZPOaY78esZjRLwU5Dbq2ToQ9FYWEGqwvykpdmqk+P96WA8iYcGbhFgSZV +jXm6bZnbNPKRx8L5zBm4rta9ZQAIM8tEHaJTOSt7C5QlfD5zuK0x2l8CgYEA8yfe +42zNF1z+sF9243oLIOzeI2MPe1JEMX9rXdKsIlPsr9MK2o5pTJyupn9l8c9idvhJ +9aR+m7ocWAPRYxbkzmbaV7KRCTuApemypsoExIOTMmXNQXvbaH8MMyHQRzjUx5WW +gtJuvvi3O1z2GRUmCgQRt44Qmt9SIOBy1GvDTQECgYEA/K3vvek7CZ9wftM7QRkW +bC7iXLEmXFI4Eq3Lc/CtD5qRdhJLA9tggkOH2L8gYtj5UnCCsSORYOF1B9bBC87B +LZ7Io91EfkpW3RQG8+4VSE6U7uOD/GY14DDL5ioq9zEnj4v7t6aBNPBxLGcPvXs4 +r8KJo+OKT0BOxjypXNhy+TkCgYBli2wInAJWVG7EZ0m2G9yT5neqXY80mBmEdkr3 +Rqi+OUAp+T+aIEkXRoOlYqTHRC1Bs9TnMcfx7toxY37JG+F31PowD5GEQoYqQ5tp +DgrJqo0JSfxa8YeibxRAMknRxIkPxIezoH/BXwreTqczuGJHYoWnmzuSAdtBgZpS +3aIVAQKBgFeIJHpLZrPNEhrkidaUQkEtAPASHQ6dzDvMBtHGx8N/hb6zyjNFV1t+ +sLRlLXjCNbrft0lrAYpsRyOVpTCJ0xMZ0p2OCorKArGzw2ClwEsdZFYd3FU0I/Vl +o0kunMpKxrJNkIylVhqqOfCpeZTXP2ybBRA/2krmdQzoYKFwUGTb +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c7.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c7.crt index 435d12979dd2..43d8ef599f5d 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c7.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c7.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUU67LewM5UhpCYrMabvxZrdBHhCcwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE1WhcNNDUwOTA0 -MTMyMjE1WjAQMQ4wDAYDVQQDEwVuNC5jNzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAjvRiPU8N+9dppeWCZf3gRL7r2afS6iS7YZn3yqYeikXec14GkDotYxR+ -ZPeG3iTwhE73I+iXIScqZmtqk6sL3Iv5Z700w8CBIhLFZP0L6MPDpT0WwBweRzC1 -BA1uXam3Cwp0hux5xAH49h0EZvFT6bxSVdhrunsWlW1N5vZIOlUCAwEAAaN/MH0w -HQYDVR0OBBYEFEMbNKUNe9yyOtm8R/QonVKFl7C9MB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU0LmNsdXN0 -ZXI3LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -kIqfKAq278AizLJjj0tTC7rQTlHRMpVSdzQUfdYR+IZ6d0AuPe3drBKEMLsXcY+r -OndzUU4YVEsq+OVZcjdPsVW5FH7kHYzQsbxDTPujQgn5ED3ggDGobWG6di18hpVv -S3Nt7iWN7jl2ilXA/S7fskRH6w7mVjiRMsV8QiwwOtTkj8e1vQBa6l9S7vHS2vFr -U+nbTpdVrTKTZ5+n36KtmqUdhzmp2V3krVCihOF016TBzU7tp3+I4FNUaCLj/bsC -A4KWacL0cbZul47Zd4cKpFDDo3PxSxDeBgLh1CDx4SJaKcrko9YRJ4ZgA33GAMAv -n6V8wxMzyGROsR663RIySA== +MIIDUTCCAjmgAwIBAgIVAMtrnCz4z5HLifxa72xqgDKRBGtmMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxN1oXDTQyMTEy +MTIxMjMxN1owEDEOMAwGA1UEAxMFbjQuYzcwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDVl76qwTsREi33MP/QrPXBkqC+Trbf6a0e0qOMhDLCMMnlK5l5 +cioG1JIWpOMtwVjMFKGO33LYNN/dpp0NHApUpph5YsIn+ZhOEMriANXLipfag+l3 +ktL1dMZcjCH1mj2sAZBmxCtNs4WuF/LS2MfB1TsnpXlA3YURKJiBAtAE8Imrnw+b +fE7EqFoC6Vg3mt2SnUn6MC/+JKZ2LX2l32zxJR191EMgy5T5kXyDL/hDFMwGBamp +5Q6OZ/Qf0161GNq0kzlGL0nqe3vgYYo9+0GfyUHOAVZQpM1PqdJHNg6Zs3/oaOf0 +KByBUCkqRu3VEjrSRaiVsl5AWh26S3FhUaLhAgMBAAGjgZ4wgZswHQYDVR0OBBYE +FILhvS2rgpcUzFi/SCbdBlp4oyq4MB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWCHG5vZGU0LmNsdXN0ZXI3LmVsYXN0aWNzZWFyY2ig +JQYDVQQDoB4MHG5vZGU0LmNsdXN0ZXI3LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEAYXJlXcIE5oZIblhsSM+UBTA2gba1rvaN2ii0 +I/dUl25Vi3VkJhD01UPs/ojC80a5jqWu53/Hj1vFsxmMmzvgIHZcXBX0Iu3tAe9S +V+T3JGPHangqadTEKh1T0TOfgVKIekpj9+qBR7mEfRvyNSCghoIH3DJkeEKDU1HH +SddZuzrWa/pMGmkKbqerXHGEUCciXFAjbvrkIY/IPlXaLbJGi8MRs0i1QP/m2P42 +2vInwp/4nriK44jsxEb3/snpPsnhXtggFRTD/k6GpTdwKMWMtJwGiXF1P1Yq1HEo +gHkr9SvUesSbeiNfwbgQnMQH+DVk57OBW3HY0uKgL217AyBYZw== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c7.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c7.key new file mode 100644 index 000000000000..0741214e6007 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c7.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEA1Ze+qsE7ERIt9zD/0Kz1wZKgvk623+mtHtKjjIQywjDJ5SuZ +eXIqBtSSFqTjLcFYzBShjt9y2DTf3aadDRwKVKaYeWLCJ/mYThDK4gDVy4qX2oPp +d5LS9XTGXIwh9Zo9rAGQZsQrTbOFrhfy0tjHwdU7J6V5QN2FESiYgQLQBPCJq58P +m3xOxKhaAulYN5rdkp1J+jAv/iSmdi19pd9s8SUdfdRDIMuU+ZF8gy/4QxTMBgWp +qeUOjmf0H9NetRjatJM5Ri9J6nt74GGKPftBn8lBzgFWUKTNT6nSRzYOmbN/6Gjn +9CgcgVApKkbt1RI60kWolbJeQFoduktxYVGi4QIDAQABAoIBAB0r2F03KXtaDEUS +ashz7CyVJgxXxnTWU/qTB3CNguaFW02dTXIKEYtVROPVfowlN/IcxI6KyK3sABck +RkF1bu5uF6zxu//8VWd0C6wfiRN/c4s4hjj6fdgK1tHqTEkK2RlrgplGeX48jSlg +bp2KqzLh4xkLFIBeFhUTLPY8YsS0WrhxzFZT099HbdBg0mELPBM1/2AZHbiB6hcS +vbkfnogOA4mWxuG+J/6r3cqONq3Blvi1P2uT3605aRyJ7l2scaJapyhD4gm56FLY +unaVNuHR2Q1aW++NdpP5G1YXxIOovmYbzIOpfDUscVdsa3yzL0pQVfzTv//++oqT +gkCXhTECgYEA9mmghMTj69bXPxYG1S/rfJIERDMmkayQo7OTNKROlt8UTL+YnrPj +OizhjZHfJVqQ716NGTCNNtda16xyNqSQybr+duXLiK0hgjBaDc8NJZ7/s3omh6DH +zzABqsQ2o8FAXVIGiNvgHOWv2HCw5thFqVYmGIT9uN7/cSl0+QFm8pcCgYEA3ec3 +3/1zN9ytExM2GeBvqVnGNvzogN7DpLDsI17rfi3GMmsIVlkphAm1B7zD/okX/tVq +1QRdgghS6Fc5pFHQgSBHpTIykjYbfWJbOyp8sTb0y46ZcgChl/7LJNO3iN/GGJc8 +lLpfuN4nM/maNxTwOYp5BM0ecmavJY/Johdo3UcCgYB2fVBcx7i5vXN/uH3MoeBg +5G38xYP0ZMCgl0df9up7jgXa6f7fwaywWsiTJOPiuGwdTqAWzsv45RGfyCKbuAKP +nsWiMtnufSIx0kJbhYbS4mQez8wbR+sM7A7BKN0FXXYv6bOOt/xKqQAn2fiW6MSG +qxBilgDTlxfKOJ8w5cW1PQKBgHyrbwT1DEqSPNq+rGKVaaanvWkfZjEg9qIMlF66 +8WuOpA+neqOorF52ZbLXJvZWZ5jQppjPPwnTS2ElwEjFfnNNg3MYZLkolQE4xTDU +igG2con6r9MQ77jJebBTEoZf0pf2QB7/JtBiX3wf+7h+lwh898juxPBZKIXd5tae +eFf5AoGAX110qVZXUMjtpGcqotOs81nG1V8IQWFLIrqH7+aq8bmy0Uali4Nk6j+M +9JaeeGJQPu/Vhu4KUC1u5IxKuUkQlHOMd/meGrNy8A4vhhtV71x6D3An4F5J96di +ONWacNfep9+s0QWud8UMAYz/xSQaVBFtjpqAISlslyNqc44GBJk= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c8.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c8.crt index bab6fccac648..233e8bda4b03 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c8.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c8.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUZPtamUFGkwEsTdz8nVPsnuVKmagwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE1WhcNNDUwOTA0 -MTMyMjE1WjAQMQ4wDAYDVQQDEwVuNC5jODCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAnUV+seRzu1C0Cunk2tXNWXVKIc3Ia1PjZ1ihx5diN8KsqgXzJPtVl2BJ -4+DIIW8yUVqera1P9AcbeQo1btzPvAr5jUFIFAxZI/N73x0KBFK2rHp67RPUgZBn -f8fkNrh6OiW76Dyj8bd1R7PyGrG6hzjW/gkEA5UMY2Uh1hKUak0CAwEAAaN/MH0w -HQYDVR0OBBYEFJ9cx8fxd/vOjAM6gIQw/vqIC1gGMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU0LmNsdXN0 -ZXI4LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -KDBvftp+20KL77/gc1sPeViQP7SzYbrL+lNggjuBIti3s0oD5Cv30QmXe0EqpWnH -uK1BtlNu6JZ5izuDneeVVfscCPUqgzjrDPCdFFHcPBPlqcNlUBdZtsAvq/pXCSzJ -oyy1qkqu82oOhnaYKZVEUdtm7nC0lu2A9TMMQHgGq20GOvLMTxZ07FsYZRrghNRV -xUI/YLZFaTyEGIdd0hfafFXtmnLjBJvevgcZ36S7BARfw6ZZ+Yum1tlqKBftVTD6 -P28A0nZJbPdVDOBfh2zY7/PokAaIziq3cVuoyOx8ti0kPhZJiFSxjWSo2Guz9jLp -fUXqHq3QqRdgaYLt1YCn/A== +MIIDUDCCAjigAwIBAgIUMsIBuaaDLkTz7XiLK/ndWoawaBQwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE3WhcNNDIxMTIx +MjEyMzE3WjAQMQ4wDAYDVQQDEwVuNC5jODCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBANdG+0IQOjtBwn+7RL0iTnmKrHMouVZbJAeaiB5OhRf00T2Gn0nx +p0immg20rw09yWOhtoEemsUZ5q/iu+lhnIsdfHMrlWiEDTMoK9jlUH3jQBxkCWia +KDfzoRHDROjw7BgF73LRX04o3y8wIQiqz1ATvd1XnyB1DPMl6ScgAa4gGlJlxqNk +5aIgEiNObMV9LcggzojXXeIbaahmL3Y3tOc/hAMU2UJYq9zh9vEL+hjV+jum1ysx +Is2qw64ttWGjGNc1bKsmgBzfQQrUodUcWyp2iFXyq5lnpVTdyNtdGi7ehklBN7to +kJoz8PKXCnLybOa4qRm0Rqo2WqPJZT9xxJ0CAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +qPpqydscQtoCgHccHnepKHEo4ecwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRYIcbm9kZTQuY2x1c3RlcjguZWxhc3RpY3NlYXJjaKAl +BgNVBAOgHgwcbm9kZTQuY2x1c3RlcjguZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQAhemLND4r2/e2DeTSY5M8Icwl2h+vMj/lUKC8R +YYIveIfIM/inPMiGCeOA3iD3CiLDp+4Ed3m2zliNNbu5+0vE2f45ht80MF3fccKb +l5BVjG/2g2xbHIRUkshoTR3aw8GTXPp6qPIjaLIxJcr2Hb234UU9BzG7/deK3drT +eBJbk0O1UXlxcfVFhlc81CmZFzbXfB9KbgN53WYJ9BpB+wv9cVq2nmKcMeZxfx3a +W0GIQluO1O2hE5EFkLCoMLqzyXpBxSQZcejwijeW+LD1dC8oc+I9eo4q6qnKYZ/A +qmlE1T6tZ26P038xXyJRIfZ2qVvtraVhVXOLK1ZNwi3eCg5U -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c8.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c8.key new file mode 100644 index 000000000000..45eac0c441d1 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n4.c8.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA10b7QhA6O0HCf7tEvSJOeYqscyi5VlskB5qIHk6FF/TRPYaf +SfGnSKaaDbSvDT3JY6G2gR6axRnmr+K76WGcix18cyuVaIQNMygr2OVQfeNAHGQJ +aJooN/OhEcNE6PDsGAXvctFfTijfLzAhCKrPUBO93VefIHUM8yXpJyABriAaUmXG +o2TloiASI05sxX0tyCDOiNdd4htpqGYvdje05z+EAxTZQlir3OH28Qv6GNX6O6bX +KzEizarDri21YaMY1zVsqyaAHN9BCtSh1RxbKnaIVfKrmWelVN3I210aLt6GSUE3 +u2iQmjPw8pcKcvJs5ripGbRGqjZao8llP3HEnQIDAQABAoIBAAMztgG7njNBlBHP +31UVITD4CdouEvVcFBxT/CmbPPMfDkxAekkONCxqP62ipyZ5Dickk9W2P+TvDDD+ +0v7XKelqYFxIwFCEKAStRltmLA9kbrUvrt9JEf43Dq4KmWy4s4JJZOKU9My6mVRg +IvWl+MD/Fr8VbaEhdbZZkKXd+ThskKq97mUy5ockL83P688MaYDe+B9S9CfBE0fR +FFDKngokaJRjhHPnEalAigiagc8BV8+IhyKlaL7+F3zh9f3ExjOFRdl9xLndCLeb +dncY60IdgpHmIKBBuw1uuIGNIwZYKRTzr7fVBxmWbYE0WP2GuC4G5UgyomuW/CNR +A1dZoC8CgYEA2MDK26tuKYNsl/YjWaZuA/O4C/OLL6I5g3n0AYH1voD/U0OIOgBV +/5UY+s/RqaD0+ZJZNSdhfKCzE1alCwhquSDs0/XFkMeGsP2+XuykeVCkiq3O3Ngo +tXtxhV2JdT5n+i7AyVNwpN41wAoMSEDf+Eg02X9fdbET+iGDjtRr/yMCgYEA/kHH +o9uUSTFw4SSq8JNmIkVJmogIsTl2cK+/v9/dpdZw/6KKqBOiGu2/OviyxKzZ52Np +yGyt6Wn1B77VLYFef9ywf18FJdSOMQBNcv1W8T4leYaQ9T2Y71sy6mj3kOQlb23x +6g8Bow01+VRBeTN67MdtM1sZUfliYM4OSga/ST8CgYEAqaxRoB/UPn74tQQiq/0S +Rdm1CvX80K7m4rrGfZ+kJRqJxD4rF2xH5ahrQ3sHHwg2fF9JochQEeUiCVNcIwTV +tlHF0i6kFy95inc3XMtV/PlZtI1WT56OT3JwbPvJrJPLhRawJ2k/1m90F/5FmxHd +wVeUt2Vz7odNohI2D8rNN40CgYEAyZpk7zND0AmI5KyLcjieClyAMSzeH5Bdrppc +Y4rhmAaCEODUKtNufQW41MywCyy3lzaHgSlTeLnQVXTmWa+zu5TEyioQzd9NY+NG +hgAE+UJ6unKN7DcMVfYtWBsk+dk9Ka623A1MEfp+qpU5WR4NwUOb/dWlLQ+eBv47 +7EyPTkcCgYEAg3DpbUeTeLduJt6Z380yL+M5SjBC7bW40KaKg/hTNEAvc9eHTZv1 +wgPV+gei9VDN7Hubzj9zbV5wS7OTrdjDK4B7PpN9vD2yElNGmYzsAxzapB4KQkYE +Gn0cFUQ7jhphm5uyUtH+K+FWRZTN/o5+PRGDoB7+5Mq//HovdKQTZ7s= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c1.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c1.crt index ede8be0a2738..a44fbdb545ef 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c1.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c1.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUKdCyNvk9xgVTDO9On1JBBAJgLEUwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjEzWhcNNDUwOTA0 -MTMyMjEzWjAQMQ4wDAYDVQQDEwVuNS5jMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAk2afL9SKZO/18apqEJtcnVn6j5KFVgiwLRnqDe0O0waKAuAatUkRu16E -TUQiz9gUJ3LlFA/7+A+6pBwgVj5rh3iheQ5/nn34OSP4aVjwMMcjFJgUxPVvRxR1 -pjspxKBsMdwTt6EXBg7yJTWI4Lk92I0Wy6vb0JAf58syQYd564ECAwEAAaN/MH0w -HQYDVR0OBBYEFHs9eLWpbASF22YffoTchou3BPfeMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU1LmNsdXN0 -ZXIxLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -Oz+MEYQX8+5uRpJQWkdqw7fsmRUuTmfXSayz09SPQ+jF69iXTa64PvEiKkeaLSYX -mQKNNKpeskYKdGQ/r/fBFo6FRLLvtFfLfGC1OoOIZxFy2y9+0MYroEijQB9dASeh -L02IksNsvPZnOMvMitETDoQPRK3mG4OcFbdvWuZa8oBJ1cbHqJPngLaUn6PRc75A -j2BvbeRLT2OZf9whBfLpE5gI8oWhTUXnGps2xKa/368aWr72Bi4/sMVMLCQkwxxP -cwbG3GKF015K1Q2BgfTDSPnbiCXViuSGw2opCoflVlH8qTcukxCyrTiOmaXfdds3 -qB2TSCnpAV5UehB2XEoNyg== +MIIDUDCCAjigAwIBAgIUKYcI+BWm+vfX221XUUgOr/3reeEwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE3WhcNNDIxMTIx +MjEyMzE3WjAQMQ4wDAYDVQQDEwVuNS5jMTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAK4ABXJPmbySrBkCM04UlFehN5IYtxkomJ9jM/hirkGbOba7uyhs +3ZdKiR1PGtFTOwE9N2mKKQep88NzOPL2ji6uvvCSpYO6kEeDHiWWr4vbjn4y1T/+ +4Q1lebO2Jd/eskoKKn5h9V7xNPEzKAi0Yq8SBPhUq+zNL9fhmeUIrsH79aj6+4V/ +NmNg6Ozg4DD91Zefp0+FTFrke1hR76pKZz5/OqF+K0rXUjPILuen7DmArKP6V2D2 +KVEwny+zaKy6zAR+lmxRdYPwhyCtNLHPMPB36XnUNjKhtcGKxaoCMApsilHroPLj +dlXAz1Jq5dAT2+SXhhk6VDAZHpoyx6I+uNUCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +HtB/V6LPv04eTSww/tLDQJeZoNUwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTUuY2x1c3RlcjEuZWxhc3Rp +Y3NlYXJjaIIcbm9kZTUuY2x1c3RlcjEuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQAB6LAOJk+yOJYclLfAXhqkUl68SQBi0mheKA3I +nSxPPHd23GNS5VFUpXG7ePpva9kREUw6zgCIKbMNoq3oh38zXCqzVfZzVnlUrjV8 +JF3xSAGxgfYqob7Pm0NWpLq0tFA8cymza67GTeGsEYNnCw6v89ZNJzwJkgiuySJl +NZv17o+Ldh10J/p6ne8iSmwRVHmiUM+DuxPggJj7swSeT7pQ6wwJMFND39ekG5ST +UaJs8pv1SVOrAgm9ExAd6pTej1lBIdwlDV8hxmVDD1BxyCf+63kp0blZ52SqErPf +F5Qzfow0kvKZynrC+nETIpNBAwIf9vy4xi4XrMoncPTb1n6C -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c1.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c1.key new file mode 100644 index 000000000000..e26205f66a50 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEArgAFck+ZvJKsGQIzThSUV6E3khi3GSiYn2Mz+GKuQZs5tru7 +KGzdl0qJHU8a0VM7AT03aYopB6nzw3M48vaOLq6+8JKlg7qQR4MeJZavi9uOfjLV +P/7hDWV5s7Yl396ySgoqfmH1XvE08TMoCLRirxIE+FSr7M0v1+GZ5Qiuwfv1qPr7 +hX82Y2Do7ODgMP3Vl5+nT4VMWuR7WFHvqkpnPn86oX4rStdSM8gu56fsOYCso/pX +YPYpUTCfL7NorLrMBH6WbFF1g/CHIK00sc8w8HfpedQ2MqG1wYrFqgIwCmyKUeug +8uN2VcDPUmrl0BPb5JeGGTpUMBkemjLHoj641QIDAQABAoIBABVFAmVuo+PVvWd8 +id9QYQZ0j3GdR3sx/QogGyEQla2GYd/bre1V9XZs0ZBSyaaZ49DAdhlsHpzbDRBb +cYmIUxQTdLAStQctEz2dTQjvlRDlwKgzjYwk8Mdtm/ANuC6Cr0z2q4AceF09OKld +WqufD/XHoEeSOMaZxHCWxB79AzkfYIGyl6/Zbm6O0DIPbUbXh4Gh8rBuuj+t+gIx +2KOONpPGijms8/G7SDi9OWEyY2hdorMVsThTUp1PYyGmueVxNg6h6U6VL+SA5EJE +yKRLogHgHjbHeZmCUR09xOGvsyzApPaMUD9Tt7bJtdk4WUDxCOkwD94UzyFj1TKr +n6TOttcCgYEA04IUx6K932erz0/DOEPtaECBOClKgbRaC/x/84gJG4hsDksl5P1Q +8cJkQ1h8SID6p59av0Zx3K4HV+daHWKO2zoUsgDzpH8T6ecMulinuhYiJPlpj7QT +FWwUuJ4qvlXJFPn0FHoQheHeMbLm5SbwNQeeAwTEn8z2PK/8DCuMMesCgYEA0poY +YxHb6ZAfKvru11g2I6rT1wpG0+IgAx7KQyhminS9BB6u+JpLvBh8n4o20hG92VKa +df2Z4xxukfzekAPca2tPYGVX01yTWRit8zIZ6SxL7uC1STupQzQLeyp9UYMXlSz6 +Z5SOm06uT59JICd0fjbRcC3WXkbudPPE0pMvUD8CgYEAgf1SdBdrBmCa0UC+RFhL +EmWChVerOOf056T5plyW7Fr9vhMlaQTeuJa2NEWvrIfIhTiE90V+ORij3ryLP8AQ ++F9L1NKM6y5GuNyNkAFopA9yULMQ04biq6nPWl+q+WuTt9ae0e7rNL1KZ9UB3xf9 +TTovPL+zcFrxgv7Ns1UYRp0CgYEAs6V+6ANz8+hcre0fj9JPXF18FxNNF7DsXb0G +DAOKdNDf64PA/Y+gyvKYW/8qhEdeCuPLnpqVioXWS/HIaFDi1T7UlwXbxRBTIM1M +lQdH0v9WRo6oFbAu0oSyxn2YKNAKbdXQREsU2qZjSsg1OJhvZrqcLXTsR7956YmH +e5jR40ECgYA/42ug3MZ5n5C+u9NNfEm+3x3wo75vRmVDtd3b5cJHq5EHuOKkC/DG +rW9yTphoe4vL63875oq9zIZm0z69l8Qm3kw5LEJt0yUHaD+vQwtcxVuJzSKaOmT2 +7ttwZlWmGwQCTCAYVMf6p4hGP4qO4bwoxagUnwftBPVQUNLt3ogizg== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c2.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c2.crt index 27b65a7a5923..7bf8c7226c28 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c2.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c2.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUDQRkdVBMn9YP54iLThtM+V9kTEAwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0 -MTMyMjE0WjAQMQ4wDAYDVQQDEwVuNS5jMjCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAwIe2m8DEVRgwTgfukT218TRvgaJ88GO5v12NUIg/uzgq2ADAIS4lynhz -5mzsdhcArzWFCz7MxP/fMxfwzRP7dNm2pjz57+DdUUSpPrBCDL/3+JwHWevd6LgM -mgLOSohsJOZmWe5PUVMiP8/sluBb1+63v0e3iDNUXreVal0xxF8CAwEAAaN/MH0w -HQYDVR0OBBYEFOOs6r9usaTHZ735J+HCRsZb8mDmMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU1LmNsdXN0 -ZXIyLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -PsRSE3bUNMgAx8goznM29Uu8WzrsA+8yDd2Ts/Cjj928vkT4xLiOOAGxnNpdhB+P -9ii99A1bbfacXdyK6Chm95DoiLXfKZusHpu9C5EvV9BDQJDEqHQbf3LUGo7Pl55l -wdnGBvAmNcAHZ0mZhvm7F0bXBPJ3beBbnjrcJGcL+99x0SwsXYeeLGFNdUft3nA/ -wYk386vX4h1Rq0c+aJkNey9eaf+qUMfClEtAX5okjIvxT4sETd/w2gB9tKldMX88 -0KkwjIk26HqQb/TYZVX5rxrP11Pf3mzbWI28Fnscw6ewNRQr/BWAY4p650MJ1dar -RLQGA5ZR/1IBQggTer5Rcw== +MIIDUTCCAjmgAwIBAgIVAN00xIIASDUxm27YBQBd5uNc+b3YMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxN1oXDTQyMTEy +MTIxMjMxN1owEDEOMAwGA1UEAxMFbjUuYzIwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDNSvOBvU5gTGzSQ7hGHdD27WoQuquJy5DyIYbX00MgwqerCdnw +DLHFKxb5WkZODGXHrE+5CcrxEzPr5h0fNjgbAGbtoBs5QKbEAvXiGmFl18wsYtkb +FzMt+0o48opv1rlwW5d5k+nXEt+xwBXvmdekl74ewSchLyeKKN5bzK1D5f47Dwwc +rOqd43iFJUfBKGQLRtGHYRqLxCf/LXv3b7NVFnygvNCgu+zldK8s1udw8RWSyyNK +oJXfcqXvSR4wEq0nsyhnC03u5BzFQI6BtL+0REt1dHG8a9TQKW32oEN3GgQ8bxAD +ZdCtjqA2qmA2MoAoccJ+GBWN544VhP/tpTkpAgMBAAGjgZ4wgZswHQYDVR0OBBYE +FFgMdBSaU8g5EQJB4ZGlPKcFaP/rMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWCHG5vZGU1LmNsdXN0ZXIyLmVsYXN0aWNzZWFyY2ig +JQYDVQQDoB4MHG5vZGU1LmNsdXN0ZXIyLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEABpPG43dhFMhrzIgL2MYSqQjLk4WNJgdhC/+z +Kz8qVGFi1iGTq2yuOf9y1dzab0+wMCmqygTJzZ0nxt3B2s48zj4d8JAMxicFMH84 +fvE2oML0TmIVDm3SNWSVl8eQsYUOEIG0WpZPg8LZVyciOI50acLl3qv/zDqLxmUF +QAM6ZFxZBcLnPLWInX4p3zSemEDAc2BlIwMLAjSb54p4aAXhwQErGZ795HJiU1YZ +iBGLGV2wHPqVGFooa2GdeX0OSlMPzZ8UnmdN0424Wr3onm1rVrw91n/kPVa5MXdD +7P8eLr59C6Sz2PsA5SSwISt8LD27kGCsb7gI01clq+GgMXZP2A== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c2.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c2.key new file mode 100644 index 000000000000..12c34e88055c --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAzUrzgb1OYExs0kO4Rh3Q9u1qELqricuQ8iGG19NDIMKnqwnZ +8AyxxSsW+VpGTgxlx6xPuQnK8RMz6+YdHzY4GwBm7aAbOUCmxAL14hphZdfMLGLZ +GxczLftKOPKKb9a5cFuXeZPp1xLfscAV75nXpJe+HsEnIS8niijeW8ytQ+X+Ow8M +HKzqneN4hSVHwShkC0bRh2Eai8Qn/y1792+zVRZ8oLzQoLvs5XSvLNbncPEVkssj +SqCV33Kl70keMBKtJ7MoZwtN7uQcxUCOgbS/tERLdXRxvGvU0Clt9qBDdxoEPG8Q +A2XQrY6gNqpgNjKAKHHCfhgVjeeOFYT/7aU5KQIDAQABAoIBAFLuPLlD6oXvSaSV +E7tLby9ZEBhK+GHVWKvfjFPidYMl70qiU6Zr0mJ2ejTn2W/NBU6olpoG0TMa0UYx +8sC5i+eofWvv2h+CNPwIwCCDDKZyCvpBQjf14shn839Wx0LPqKCxd1r3PCcUakYp +dRMSADFu1TAiUfbPUDc5ND48TCAZbqn+wxVrofyLG5tXdOiXf9eOwhECY5kMC8MN +t1X9X2gdIJXNiFbIwE3p91vVrNmosqvX12KR3QG54DhHVyM7mWrmRgZEBjIcdshe +1olIk+varEdRe3/uLTnplUuH3RusnB7bAoUaWqBc45IaGMdahdW3rP4rU5ymquVL +hJfFGysCgYEA9nvW5Ez7KyRXGhtqgCRryujFDxLUaKWbWryId1pqqKCK6gpn6l4i +zKNqbJqpQvaryBX19f8wwwMRm3P2g5uYhodhjr7Hyv8ljunz8u9WnHx2HvIHT0u8 +PFCNHAjG8ZVC9AiwjKoOwV17vlaIaIVUPUCTxUjNH33vHe/sLqUKxusCgYEA1Tf+ +kSQyEgN010yM/I2kqpaITMhGY/ULYjYODRnx9HIednukZbpIpPaNFBrzZnP66R7q +crHhPlQux0kd+jZiTw9eTVWHib4eNSiCr7F0Y+7YwY1iMLGyxUrcCVF/j0eLL/F1 +DvzewJNHJIxN+bUetTzb1KopjzOMH2fG7u2V4zsCgYEAz3S8EbuZS15y0U5x/Riq +gUYZVe6LSOjaMSytz4cSbotxYDsR63j/4S1Jzc7+15W2T9nsAviemgRjNWwM+Ahm +ABTCT9rTXlAR1I+k+7vmMh+va6xZ6qvb6wgBPK9Ggt8WUz9dGYeok6eTF0jN1ush +U8yek1YLD453S8M8DqZqw7ECgYEA0jJBZBSqx9dgeF+fHdsFWqbL9+X4ftI0Rr3D +XGcXgESAg42gpCjhNL9alVd8XKQ511gDCM8Tnr6e3xfV1bWHdSlRbivDUqd6YC1v +qITomRv909Ht2tNTyeviv8z4U+fT74+6NuxTLMYEfKNUu7cybrlwflbCYcoPeWZ1 +V1YhTcECgYEAlPOplx1qLzfKGCShDfl31YYjLX1+1Mfsvh7St229o2KAJcnX1AF+ +/E+EdjoI6KJKl9XUZo5kuEwsveOjpbjN+BHrZyUlOBQSJnC+qlQSN6ArtQn/65J3 +f9+I0w0VDZEmYghqIHPy/6mJEMLS5P/uwVhxUSz4thXqrsWZmgstKVc= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c3.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c3.crt index 89c96600569b..96316e57eff2 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c3.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c3.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUJr1hlTc7CXDlsSooLb5/05DMugYwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0 -MTMyMjE0WjAQMQ4wDAYDVQQDEwVuNS5jMzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAk4EUO9srNajozMBBRVv23wKGKGQQaiQ3w/8x1SSDTsIQciJNIsyPQW98 -f8lXsThi2IQtRjSCtNxZGWftDRu3t+4a0L3tPEvrbt3H+OK5McIbS2SjhsX8rIqJ -EUY7UJtdDm1DO8Oibn0U1DOTTsdPloW3zk7GGrDo6bxHpitotyUCAwEAAaN/MH0w -HQYDVR0OBBYEFOUchU2n+E9rdI9UjdT1y6xrSUopMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU1LmNsdXN0 -ZXIzLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -DLA+tUvoBH8QkdHqpcFWMxAU1pwBN4dFh48iIKeU6tlxBZJ3vuivUwvUMZ+DdgFA -QRAkmTuBm3t0WnIukYO4huGT67C7nya90wSyghff+9ef9DnqVvd8rYg0S/axjaeG -tYrDI+qK0GQWF5wddskzpFfxIaZzgeppWB+CBjgFpntDOFql6L4jbaU8rv8GtpW8 -zHCFAF+38sqI2iXD6njZPjDSN7aYcYmDutqyeAB8ulRJ/q3lAK95kawp2wMXX7Yx -Lz9CrwenfQrVVVpQt6l19zhZQs/Hu1WeDvGGsicxwjq+s7fhBr5rjLYqpUSf+SiK -YCNk5fKpE2CNh8b4yalySg== +MIIDUDCCAjigAwIBAgIUJW7IQ8FRtq/JCv6zcHnGMm2CfYgwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE3WhcNNDIxMTIx +MjEyMzE3WjAQMQ4wDAYDVQQDEwVuNS5jMzCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBALnoNNARck+Diqji7giUyUGlBwxxHwvxRk/vGn1WF3+hKb7chpf8 +IDXdZVDRn5WQKqV8N6yC9QnHTzzHiKxZYrws81Sr1CAtpgiD1WwFKP5NwpPnpPAv +hDU0DHas0QkconuW7iVKZ4t3iB6lvcdjqHFuLprGV56HwyhiYp4kEBHvIvr54hJf +MDjs+JJMTNSjIU5llip4J9lNq02iHFMBaXOiXW8YLG5VkBSLzruX5wT1FhzhcOz2 +TDNq467T6+eXYWUuJbxqEaKo2rFUaR1KB/X4VODA+vvGmQBNZnFiNJThjVu+jvGy +anjSoHLS0LbbwLPOeLXeNmtmQx+Ni67WSykCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +Zj64nueP9XeqNYcpIPNZfK7IF1gwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTUuY2x1c3RlcjMuZWxhc3Rp +Y3NlYXJjaIIcbm9kZTUuY2x1c3RlcjMuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQBz78FyYAjOGlMJVIKdMKqIA7+QGzdx8D3a1nwS +DgqC3JZaADGmqSmIcxLZ6CNMMilSR5LQ4KXDB4HchGi0nHhp/XxuT0gkPxDK2XQZ +SVCq6GPReH5x4zmKsmFdg8Y2JYy1/BewZB16XUZue5zDajR6PPjQFqvoMOsleWfU +BiDU8VFQl4S9V26PszTEhqieaTdhdbwj8GAxIdswvDEVgebsnZqN0DoWZKW8ZvLc +BmvVhM5HpCD42ustpolJN+ClpvKYEElEStyj0HQGwRhRDWmJZXP/FhLeoS3Nj1oI +ItJ3V7PgqqW1VOyvfYeR+XOki5yzzzMckRNIu4gIzjTnZUkG -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c3.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c3.key new file mode 100644 index 000000000000..1a61dbc3b722 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c3.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAueg00BFyT4OKqOLuCJTJQaUHDHEfC/FGT+8afVYXf6EpvtyG +l/wgNd1lUNGflZAqpXw3rIL1CcdPPMeIrFlivCzzVKvUIC2mCIPVbAUo/k3Ck+ek +8C+ENTQMdqzRCRyie5buJUpni3eIHqW9x2OocW4umsZXnofDKGJiniQQEe8i+vni +El8wOOz4kkxM1KMhTmWWKngn2U2rTaIcUwFpc6JdbxgsblWQFIvOu5fnBPUWHOFw +7PZMM2rjrtPr55dhZS4lvGoRoqjasVRpHUoH9fhU4MD6+8aZAE1mcWI0lOGNW76O +8bJqeNKgctLQttvAs854td42a2ZDH42LrtZLKQIDAQABAoIBAApGIhkM8HIi1Q26 +piiqPv8WOtqoq8G8s+6PSmRP1tzyT/zLcigXF8PyTXrsbXMrXvf7R+u25wotzhwP +85EsIBqXKgbCt5E5r0R8M1YUvkG46WkZcvzlG1uCBLL69P3Ea9mvAHqQxJfUDh08 +z/f4yCJ7Ui8H3r1qR6eahv8HWNisyWvNck04JTBEt4pZAW1GYXmNgd5w39AOY2t5 +cSfozD33A4j5x/N1NwZMxYtvOqpjDhqz76+ykE4AYpVm53lz5Gw7PfPYADcZ/DSg +2VXwoFPdZZrQhv/N8KxCHl4NaEeuG21uxSNKD4AQmcEJAZA0OPGtznE+YDUNFY0Y +bK9F+EkCgYEA+RGW/HlBkdzn6f2djryJvGeZbdfat81D1Qe8ImgwlR5Qc+jUvinj +9K/nJrokZ6zSO2bH6riRgl/P2mE3eabF2MnGw80rbSLc0PrjnYQKalx2xCZyCPHQ +dnCkOGOegOQzPte5F4VZXwvnIxvXprDZFa+0+ObUzqbwRIS13m334U0CgYEAvxSk +PERA30CE6VOauFNSNIYtBYPpG96BNaUnFtA1AtXllGBd9yOHhRF24Hdjljm0G3w+ +RnCM44PZws//XQLvBFn5wLy4yhhTzBTSRNiADBy8Wgo6pjJfXvgGxC7WEVCMiVUc +DmuXddc+IVNAfCE9AYPFDHmbNwN0JkrI2VhiI00CgYB6efbYMjxXINz+CFPJE9db +SUp+ZE3OYma/w+z0sm1pdp2zUcQDuw+4jCNLvO8X+DjtMu6N8rLC/KB2T6X68VKK +9zYBpMt9AE67iRQSqhKBGPTiIdGjql2LUHrAq+QOl6jHd0cyVMWM4oqHjjJuarpl +WqDQYJo3AIpX9zaDqWniZQKBgQCsVsyuQo0UDpgR7cLdPuEk2GOg1tGXvj5hiPxR +eiZl0U/IXIZDkbr4DMFgQK4EXzUPmSZ916NqzI/KQlk6pZu4HALDkDNGq1xg8+KW +D4Kl6gEDYLh3D3WvLRrFNM0EPtcSQj4Uz3uC4Eh8PB8sFbp1NEi0MLYKj0XiB49R +YILa/QKBgBRl8LBvTaBWXJLk7YXh17/VAvClzjORH+T86pTW/e9Hrp4cZAItWE3q +CG4OfqUMpZkTp4VIVHwRaxZ5xfD8G1ZvnjnMbVSn+450JwoK8ZrE0SSp4dx61OZP +EJtCUGutPN43xSp+ZHjXGl0fKxBMkDineci5B1oB1nBLUvqkCPNz +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c4.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c4.crt index 4f6ac740cf26..3552e6422529 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c4.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c4.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUaagjAMKpb7/8mdQov867WsLAx3YwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0 -MTMyMjE0WjAQMQ4wDAYDVQQDEwVuNS5jNDCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAiGKK1GCMeyJzrzzi4pNaCWqd2qiFPISAANTh+laKkKQDLbrLo6BNfv9E -f2ZnpFO2JXWbDzNd2UDA5wKjlNPacFvoWbQM+pydH5KOmQ0tG5AkrWgnMSVyZ4UY -7tUGAnkMe7ZwtWWETVTOzW478mqV3TAM9FjIPmUFuIyR40hF7EkCAwEAAaN/MH0w -HQYDVR0OBBYEFI+J3qFQpd1bJy1q/oww35Dt/lB+MB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU1LmNsdXN0 -ZXI0LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -huoeSFpQjEl5VhTgPQe2Kh+O3zjNTIGUXV4XjSf4m1hR1X5InTjEzSVrSwrioAve -wRRJZfQ33tN9RlII+80LNWozQHMs7MTTPMpOOAL3XcOTUpIaPPEAye5GLfs8hsM1 -gYluFjoKbswkvkgHR78XqHsifsNdABSaSn87JUlP8TV67GkYTzitXwmtA8ZhpqFi -J3GdoM3t3OwhkJYdYEXjtfWvlzQovlj79zP9NCc32/K37JRLkynzUUpeic5dIHGN -+Cs9gV23e/kzpHOVgMq8it5h6Yap6HI2XeiRUZfltB8Gm4nQxHFPkuA7M2X5nfXC -jkQ5EpEs7EsqXazTAWauCA== +MIIDUDCCAjigAwIBAgIUfpXPSMAftNz+iMtcsu6U9U5hdH4wDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE3WhcNNDIxMTIx +MjEyMzE3WjAQMQ4wDAYDVQQDEwVuNS5jNDCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAKVK2WJI7BTYWhcQq0JZPP8+iimUCbmEYfAriKmXgYIySi+0YAyo +S5clXq8eXTaN5fOB1P+emO5NOmyp71qpHjsnXy+qmB84UiGtWIcNzorABfz4NPaD +cdz4Gpesv3gcKXyj/UUMQEFSfGXyUfA6HsGpJUHUVBrkFZKP6BtqImOlaRbf44LA +puhk1p8u6tHBdE2Dq2WvoxBRlcx71IYCyxY7uZWAa1hw2p675pe+hhR+B1+Rbc49 +AjUqPFd5Qr/yDP+GXAeGEQ1w5/VlKWuZtAT+Bbr6nMBqQE2/gFVhCTuxSknJcbzA +jZhS1m1eazP4OKpxGDpglCq7oyeLphl6JycCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +nd8Na7PynFgVH02pQoq5ZEzl2o8wHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRYIcbm9kZTUuY2x1c3RlcjQuZWxhc3RpY3NlYXJjaKAl +BgNVBAOgHgwcbm9kZTUuY2x1c3RlcjQuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQAgTJNGl6gmpbZuYNtfUdmmUB0FdAGIquIgX5BF +GaAUBIgaKitLocrixI/eyyAG89HEP1J0OWmoQoHRWUbzLoPw7/Em/D9JL9dDUQsb +BmE+56NbBsD9Ax4DBwW34rg4k3XmzD9iG3boovG9/8XPEImfQK+cKdDq8EVxxQW6 +IXQFMKkn7b/zX4xEdSF4jgtm7HuXGlvM80EelSjGHpU8+IOEzIzpjCw3Iff3tqEB +GdyDGo7f0rs9346htO6405F7HMTTiBs8hJQnnkr9lGSTaSsK8/f+OwcM3KMGtIUj +kXQLuAJp2+JzZUpNCOsWtzLHs5E99viitXxN0EPXazuDIP5J -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c4.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c4.key new file mode 100644 index 000000000000..d6ffd242f465 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c4.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEApUrZYkjsFNhaFxCrQlk8/z6KKZQJuYRh8CuIqZeBgjJKL7Rg +DKhLlyVerx5dNo3l84HU/56Y7k06bKnvWqkeOydfL6qYHzhSIa1Yhw3OisAF/Pg0 +9oNx3Pgal6y/eBwpfKP9RQxAQVJ8ZfJR8DoewaklQdRUGuQVko/oG2oiY6VpFt/j +gsCm6GTWny7q0cF0TYOrZa+jEFGVzHvUhgLLFju5lYBrWHDanrvml76GFH4HX5Ft +zj0CNSo8V3lCv/IM/4ZcB4YRDXDn9WUpa5m0BP4FuvqcwGpATb+AVWEJO7FKSclx +vMCNmFLWbV5rM/g4qnEYOmCUKrujJ4umGXonJwIDAQABAoIBAAznUnVvREApAaNy +UYRM9iDmRMqT0XcyQ/kH/6kJovpt1Z0dbjAexUPn5Bk0psnhpaAZtG3ozNSG9+F6 +xKzlre6n7LDfwZ9npKsXXrqwU72WLwF9bkOzpY8UA/Nvm2+Tdz5RM647vhpmB5W9 +JzNjPiUX1h4/pqSGG2mvzUqyluZYvQuEcek/nG5fssdRh90QtwXjGgnC+eG8ljze +G3xJbQ2uuRIGieP4qNooqPVHb8OpaDCWaAOWaOT29MCB+7yCemEF6SyOOeQUWSQm +R27hMVUMpzlN1m7pcNw75NTbltOdVQqYxWQqJ+/bhYL1RzG2gpAln6MWyoJ9LyRy +2PTx6sUCgYEA2mO1uH0vYVlMDr1ulrv6mFgibbd2NIfSa7zj1tfb56i1BLKlSv8U +d7tkzT22Sa2R6+nfL3ptmSsqpRJjTpJki9+27qC5p6D4QCuTZarwYMYMDMxXBUBt +eYPYohAcUjunauLyd5Nv/GNgMoxVQOkQDahZc1q3v2Kv8OGDp4llB6MCgYEAwcI1 +tq7tLnnMpCM32br2lQq/eTtjPT51pEvfIGUX4pjHMTVe4ROBjmea9e2feNWDYMDq +GMyyOQJmcuC1CSTcS0Ai+uR7HKjHja+DE5zeuKJ6kb+tAO7eTogU3bdGP5m+5oCT +pOAw4UyoyHc9+v/S5YgZmzbR7HXMpqQ8UDNz6q0CgYAZPSjFN4KAR6eolG/xb6wY +aycfcvJwQ7onxV63d57seOzirBq6PhyJcdjGd3lXrn2MvCyU+hssdnapOSTM8dI4 +8BPeJCkQnGPUVrTkLQs2w+yklgEAlF+SiZBBuobrYgWJLs1C0IFCs+FsNhesNsMq +MOiVkkhmD+uxdP6YICLS7QKBgQCUWBzHwSXWb2o+QLiB+qLPeWsfZliUiz2GjJBI +PR4MXuwiwLnjmQuX+7qoj0WJPe9YJFsWoaHSBARr756rYY1ID9K4RDX9uzsNP1Mb +sP7UXZF6BUZvcHLDrWxMVAV0rEF8OPTRRtQlqouO77yEb1kIYys3qsEOSSJmc1e6 +D6qwAQKBgDUWZzZrb7NjMPf60otdqL8O3f9Q/5+BIpKyRMQIMDiepHuIWDui75/h +XlZaqrc5Z4hUfDingSgYIoom3/6h/bEIGzDScNRq78Dd/lEZS7WCH9hsjeAkCMIe +UICm+R9PtXUOb9ywSkIykf+q1pqJ4jJ4rYTMwDALF2nem9jXJWAE +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c5.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c5.crt index 8253f01573a2..98b3af5b0081 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c5.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c5.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVALC2GLx1iArQPLmEOTSnU9hhJiiFMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjUuYzUwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBALttwAoG7pVbxG29TtVi8YYvfyrw18qj3pIwvTH3uD0NsqlGqEdl8Sop -mwobhEc279RoLf5rA54KEcUaoEjPJo0eCPfGIQxWOtSCHi/Dpp28o32wFzj0uY8v -qgOlhY5akgRQyJXdgHBSVnaxYbHatPgO0P9seGLkpOPEhVK/mhGnAgMBAAGjfzB9 -MB0GA1UdDgQWBBQd5etOJglTebjjwgVm7DnZovWhBzAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNS5jbHVz -dGVyNS5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AK3qV6LeIWj+CMsy4qjHmC+EbXAUUS0Pra6aFrXBxOZ5t7h9aFEN3zJjqTTTl7cw -AbS1it93Tmr1wyuIiZ+dCtCr6M9wJE5vrKpe5rEp0tdeSs5pm0OkXPJVU/2xihW+ -NYRw+oV7qG86ldnLO067Tgy0DLPYP041gJYr4HbCCOjPLSi3B6Kn0PMrYcvPdqmi -lHGqvzqaMXq9dWYD80PKkYVOUwwxYzZ8sjOWAE1JXCVXI1ImGh4yFZAArKsY9uEl -7scQIEt0emutzHaMLwiQx2yTUjttHaogaa8S3PAHNZq4yHHCrD6a15Zpouuz4EdA -s5Dd5xbtE6zBBjGsauInD+s= +MIIDUDCCAjigAwIBAgIUNnRV9Ti0oaTYgZ7ndGhGbRB9jzgwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE4WhcNNDIxMTIx +MjEyMzE4WjAQMQ4wDAYDVQQDEwVuNS5jNTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBANwrg7auNvNIWi2EhJfgklukXf+S7lswqeZF2kCnXPMDuR6GHUnt +Sh/TTJfmI0jCNKxU2mDFLHJNaZsa8odAehj0Z63f8A59Q3IzbcoDp7YJ5gnGFC5b +g7cnd4ziw8wFghe/FezJ87TgcdYuK0R22nywcd8GgPyGL5+sgir4z+LabYTdo0fZ +W0B3wH74Drnfu6eEkVNni97XEkRwlUE0i7+YjaO1pSqD+tmm7brfmduMIp7nCvPE +k9nuG7yKQx3HUXT8gT1P7ogRNE4E8DeuU1E9pkL5SB/CnfWLbUTMyHDF69NG478D +ERkvoP60ub5ocj8P3bguim6oOZIXwOyY8FcCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +kwWKwYb9wFOIcKGCrVxLo9nwsCkwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTUuY2x1c3RlcjUuZWxhc3Rp +Y3NlYXJjaIIcbm9kZTUuY2x1c3RlcjUuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQB1xlhjMOhhrCniGx1uO09pPG3PBKZqNY9Ke55R +On4SAt+ugpym2Yu1MRbs7lt9zSDAPaATvzZ4I4vDQQV+RoFb4wZfXs7tdkcsqc/H +puxW10hOY6YU5+13K2W8i5UJG6Swl3S43ULD9aHAY0vtETqjEd+7wGB4Lf6JMo0T +mAZkeohI4FK+ciBdygwVu4NGYM6UKyNpVqWRu1ZOFYIf8hYDCJCXg8xSrpj6a0WZ +P6xpz/S2meQ5hmYWDnHjR8ITRpdOwYUyTQ9l+WWfOoufjNm1BJCaWpsQIkNkxTY8 +K543KU1Z5ZavsAGjWJsw81lsoCWJ/eDj2d+NjfIgI9h6mszA -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c5.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c5.key new file mode 100644 index 000000000000..1f7b3192ba5e --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c5.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA3CuDtq4280haLYSEl+CSW6Rd/5LuWzCp5kXaQKdc8wO5HoYd +Se1KH9NMl+YjSMI0rFTaYMUsck1pmxryh0B6GPRnrd/wDn1DcjNtygOntgnmCcYU +LluDtyd3jOLDzAWCF78V7MnztOBx1i4rRHbafLBx3waA/IYvn6yCKvjP4tpthN2j +R9lbQHfAfvgOud+7p4SRU2eL3tcSRHCVQTSLv5iNo7WlKoP62abtut+Z24winucK +88ST2e4bvIpDHcdRdPyBPU/uiBE0TgTwN65TUT2mQvlIH8Kd9YttRMzIcMXr00bj +vwMRGS+g/rS5vmhyPw/duC6Kbqg5khfA7JjwVwIDAQABAoIBABwzDpm1UqByYep2 +kK0LPHO0Z722SgUwBHLT+5JQXV1a2rkIIHQ5ikbtP2AtP9Ov3CM+cwH1/y1csOwG +1Y1xOrI7i5P/nLWeYacc4zD+qtpsBiS2VBtj6UjqKyvGV6GoCvOTq/E6JJbEMJBo +NW59cpqJ1uvj3114qlzfMYTjfWzmpFMCl5wTFbBtwrN5epfQgmomFxF07SRvRWbc +zxGwYvf9Z77gmbhVhwETpuwT8PQ/+nUKXiMou13Zns0C286o7cT3nhf9vbECYH1x +dlkNDWl7llLArsXCSh4UYhKFplL+7QOme02lnZCK2SGbrLRiyWbm2ZJSoT3D2Jf1 +mKZqQlECgYEA65KKUgsc0wnhMw5BBwz8ofPiMCYPMLIOhpd7IxLH9cF5b701Ibl1 +bcIPojBYezDFOmCDsaR2uWkUeUq8LnhHWKtDPDikQY+3Rs1Y1gUGteh4VZlIsUru +H8ZJDh65BjFR0MxVEFrADpXYiyrP2Rd4TUu7BqnVdkucSKDQKLOiKfsCgYEA70MO +X31uliKZJBAH6JbF4FhuEab4op7k6ekIijLVHOd7C7cJXsEq35nrWPftn1zbUbxR +zp6ShMjl75XNkVY+Y1ngNrI1iUO9fQgVsVSGmhliclJ+8/VuNNsxC1ANBf6jQVCk +owSXkwH0hphl/xZVCcF9cu0JvHlDAnRLDYRzAFUCgYEA0GFMIc7arIOsxXDybL48 +KzaE+npwLW8UElKlBh1y7B08wm7oNPDMw05Kbfm+CxrVHBKL6/PDYbdl+PT3CVQ0 +KGc2jmq8QZJTWToEPy0rBnahG3cAKnRRt6G6tdbPbH7sFHrrNBBSm5RJfLFeMnbk +X9uBkR9iR8YSdNxrxdVAuUkCgYAHDpZE8MErQnf73VEuZgwOEu2kLaWVvlQIJixv +TxKEGLvDW7G2DsKB99s+M4nZsnZxkblj+79clURa7bMzfzS133fChT0ZAFt9bZie +M10I/Iiv0IkYDvwiKPjoWYMY0yrjiUph2IfT+THKKoKPpIv7jq5xjlZELdtxHhb8 +nhR6RQKBgFQervbCSk2aEQWG4QKzCXeObXY2MKdwahnUhvc2ps0px3fuFh2/txQF +G1DbTpc/+vggdjRby4tLggvwAQIwr0HVEdVYuYi6AUOcbmulApmLSHlhwOpzAMKk +/T0j3nRnOWnlTeVb6clv20peetQ4Whsft5bc7SIa6IAfjwm2Ezvf +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c6.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c6.crt index 8e4cd6f50fd8..0da8153b780b 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c6.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c6.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUOLkd6HCk8li8bYemSJ2y1s/7htMwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0 -MTMyMjE0WjAQMQ4wDAYDVQQDEwVuNS5jNjCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAxUpbKRNQ/WxzJj7oYFBzeP3/JwtVTaTqlJexfKpEXAGlTXrUYRVyZP80 -4p+2Dc5sAFEFXwUcmYgVlLCj+mc2OGg9Uv6EyaT1Bz0r4oEbIB7ePBiefx41i2Rh -d6zy3Jg164hGnrHzmOdqhz0hjKxUAgBFzEtfx0URtIpscMefimkCAwEAAaN/MH0w -HQYDVR0OBBYEFEyrP19EhCIvYsJFVyZPZ/2Uis/gMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU1LmNsdXN0 -ZXI2LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -pZRghboAl/dWnrUXB5Wp7Bigbpom1KPFZQ2AYyEs0RF8xH6CoAMEMZsXDm4+sjFi -uDfFoXVNORrwdy6nsu9/Lm6dC4Fl2got2B1ZRT+jjpGXbJmPeH6gjvylqo6xopJ8 -kqozuLyLaJPenuPe/jU7VZY7WTJ36LIQMq/hCf9RVfD6513fGM1DifQwhKK1o6Ms -u1GSveA6wXUZCqfkbr3nBikhgLQP372dF611mrLQBL34xDqDtIfGeZ01wc/omKCq -UfDYh0uwJXG90TlM77bjL6LrRP6rvTff9pnFEENqytn39slXXahGoDos4WnU2y8F -hUcosMm84zADrczmErWI/A== +MIIDUTCCAjmgAwIBAgIVANgEngU9m51QUEzETj3aIZGGuo1OMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxOFoXDTQyMTEy +MTIxMjMxOFowEDEOMAwGA1UEAxMFbjUuYzYwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCaWNR2ssNRqK1/+UFbIe4zSAKRPSNrOt4eQUvoVEXazXiztPPs +7fY79OrhJW8Yz5cMMQXb2vr3VpF1+6Z7Os0ot1R8dGdyol5BfEHMIfb91YGjo0mO +LxPXgYQJQ2J8kYeJego2pCjZvQA8RANHZRK5UwA3ilkGzM1+Y05ftxM2bnbIERR7 +poTWzcDpJG+S66osWBRLj86WYtRbakRZp39C2HVS1ChVaOl8JYLGZPvvfRX9k6GJ +lgYgYdDE4B3ASoJcS5sPjM5d53E6T2w1spdep0Q0rPI25nKyOO1jQV9plvJ+0hov +pfI422jh9GTdsdqYqPusvV9ABaLdau8M6laHAgMBAAGjgZ4wgZswHQYDVR0OBBYE +FCQNnF91ymRMvM8ZwKm8nRCFeIRAMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWCHG5vZGU1LmNsdXN0ZXI2LmVsYXN0aWNzZWFyY2ig +JQYDVQQDoB4MHG5vZGU1LmNsdXN0ZXI2LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEAhNkKQpi40qq74fFZEFGUbSPUPiTdOfYVKdQw +ZplXSp5Baim7EUT7P71UQg1g1U3eikeXVK6Wk7JLJ0CBS81kXv3q4KtI18YTs3Kt +I+mfs/adAHWgKzgralAFtqgIR5NnFtr/A82d5PUfKfheiJEaiWHSzFD9w0myRY3P +eMKcgpRWHTrYReZxIyTCasVopYjiNdSbQdJDUo+5/W0hrDexKWl6tDl3YTBgVfuP +Ju+0EM6ftRWyKPDvBFGg42AfL9lXYJnt6RqAS7I0NTljIit+BTTUkJRnsqGpFc17 +PlYL/jPnMfpRyqnPZN0yk88unE/vvbJlc35dhUCoLVTQwkdjpw== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c6.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c6.key new file mode 100644 index 000000000000..531a4fcba5a8 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c6.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAmljUdrLDUaitf/lBWyHuM0gCkT0jazreHkFL6FRF2s14s7Tz +7O32O/Tq4SVvGM+XDDEF29r691aRdfumezrNKLdUfHRncqJeQXxBzCH2/dWBo6NJ +ji8T14GECUNifJGHiXoKNqQo2b0APEQDR2USuVMAN4pZBszNfmNOX7cTNm52yBEU +e6aE1s3A6SRvkuuqLFgUS4/OlmLUW2pEWad/Qth1UtQoVWjpfCWCxmT7730V/ZOh +iZYGIGHQxOAdwEqCXEubD4zOXedxOk9sNbKXXqdENKzyNuZysjjtY0FfaZbyftIa +L6XyONto4fRk3bHamKj7rL1fQAWi3WrvDOpWhwIDAQABAoIBAAgywkFoAtWUDZSC +iKpV0jayUzV5ZZmAjiOrJ3KG5vXcrceqvWgg1MIKowmrP/buk9nZUPr2TsKhvB9y +I721XXoHO1CLcvhU+oezJ25wgWjjMWauHUqVKa6zBH7IWo/Mht6nsBsVApVb+NFl +VW8HqWfG8XgP/B4IEhJt0AjZmU21C1rLZgpM4+tfR9bEC6KF4HMrnyIn6x5v+bSw +hSYSNK/54KgpBE1Ju2ZB7a5eeKjsEvlOeYLjBfzt7sbHpA+4I3YuisRlBSb+JMrx +GqqkYXA3OJB6Sh4lc9lXbzoM62nYAQjixS7xkJ3F2NjcxpH0J+xUKNhL3dO6dSBc +olPao5ECgYEAwXzYo4gb2lp5IG2fM9vZVUvVmGkXgxzwG1pDQIwPQ4xK7TByxpiz +Gnua7l5CgUw5GV2qhXY70TeS5nUKXI38NiUludBcmhphGjnVgZW6RUR9wMvVvOmO +Y66C+zhS8Ya1KpfQzrER6ziAjMHRxWmh3kgL4UkXgABCrfIIX4QUEhECgYEAzDaz +RVGbCHfOK59hUNplGH4KlOvXsaZRR5WIfjN/DQ3qKLZR1r4qj7qmNgpCwNL0u9PZ +wMXBiF6bMEln/okr653GJ15giqYc+b/xiPswoG/Fwk0OmRG1mmCd80SQ4j7ReM4R +kBJsEd5n/pOIRxfrYzuBZNxf0s0p9Am65eJ6RxcCgYAnv1TlqzVFkuZlIf/dcGFi +I1T6nUQmPBCbHdzI6rzL/Ir71MMPRxkr7I50P0rcMuNb8aDxD8vT+92imBKL1uMj +4FeypzoOsjKWZ6EToMWGKz7GF1mOGFCW66eeogBv+2NGH6/WdgufTZki0UIdksic +2CIQoqpAMk2AXEMaFBDncQKBgB5CcLfxyqYrkno2SKZBZj+F1a2RcMmbGmogBzSk +DsxxZmQX9/J5BTd88coKJTALUkZOsvJ0myPI2PgDuIwn6UElhMoqmuWQjbSYM4MX +rXHofgY3gtU9oYo5jM4SN+G+ay2aY1HU2Pyg1woK9GOMcM2ocJjwaqaCzheiJXaQ +44uLAoGAJ6neGpLb+Gk/X+lR2/EB/1bcFKNHB9VoBt0NBEs9iLEVoyhipWsXy+VJ +XqHOBTLANo2/PGmdscrt26PRDbfZwpSPsccgBBUa1LPbh657fyYhJCInahM106DI +H90IoVoqOPQlGgyBSgBINtxvRPQDMFb4OjLE0dG9StaGE54wt5I= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c7.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c7.crt index a344076342fb..ad15521455db 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c7.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c7.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUXDFTEo12oo6+EKR1kSq5mxjGqq0wDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE1WhcNNDUwOTA0 -MTMyMjE1WjAQMQ4wDAYDVQQDEwVuNS5jNzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAg6EDuwYFA0XwT9/7mMHsHDejJrfDq8dZsYm094CQDcddBa3DRxHuHvlP -HI+2yNhrc+LsIXa2xXutpmFfu1Um3BWrnvndOkK0FU5VdyNeFdH8eWjT4QVVr6Xa -I586qIafTcLAgt2k01pvocYhAYd7fGyTYYsbW4wjxyXP26jspG8CAwEAAaN/MH0w -HQYDVR0OBBYEFCRFQmSe0o8k8hohFXJ1FtB4A9QIMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU1LmNsdXN0 -ZXI3LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -CQJWjpCXPAEQcDcv2pYKzSbgwO80qZetTcWXSzx+dosP3LZZqkqv1EfPm8As5wm7 -2qeaEmOzrjsAajInZUzLwcfTBo7MqlodlDN72S6/kOkUe/Q/4M9WRl8elXf6D3Ex -tj3X9dUf5bUj1cqeNInAluOsj3ZoYlFceo3ng3berAjgMxMjqnJi9O7KxLvsTYdR -fX5a3A7KbSPjlnP0/n6FHXK+cxqysmv4m2TkL5Dsn3jK1gGJJz2qdUjiKu3jR0hR -hDsyiSlVVgqGVcTWyTa92lAe0KsosXR9eTV6qKwXFKA0FKDlPvVtp4pelDVWdaeh -QQsTs7Luvaoo7xg7W4GhvQ== +MIIDUDCCAjigAwIBAgIUG8p1q2vEJG9geHFk5Kfxpb0U3UowDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE4WhcNNDIxMTIx +MjEyMzE4WjAQMQ4wDAYDVQQDEwVuNS5jNzCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBALXBWOmOzymaMiMUfuArblb7MQVSpva0cXis/OsJPSJhoHkXJbYD +XT5TKz+T0Aqfm8LtchU/E9SNiJ/MWJd0QfpVEHIRp5Vsm/9pmKMjOSYPnJrfvtbM +Xl5aeXyS01gQqFTv5PErtNFOeriIW+ZbqO/RMdy0oqjBVTZkW1d2er8FCzLv7eZD +KMrPQE9G+R0xZAMhg9Z5ObPprSi/q9ze5Qfsu5MVxZdcdzvZMVQ9fJZG/cdmRToc +2ZOslG/0F8WslqEt/PJPPOANI/PSG5tp5nNwrfmwW3Cq6Ny7SwipVXHOuEzmoodZ +kIGOTNJgRBNqkjKeR+Qrlpa9p6NdHFDFpnECAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +wAE2HlPlKFrjXRp7KzhXQxXrNPwwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTUuY2x1c3RlcjcuZWxhc3Rp +Y3NlYXJjaIIcbm9kZTUuY2x1c3RlcjcuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQCMvM87RVOyuk+2eVsgqQModpJwzKc+E90FfY2p +OGdj1o1iL17/uix7OnZhLcDrGwM+XjhEMWXinxpC1uyqN/ND6CyaJdaupTV+LT3H +rX+ZOxMOR9YceGPy1IayrWaA9olG48y3pv/VvNWrB2zHdy2de1zeF/UjC1UnSgVs +n/ttmzmaHOg1qwauZJkHwIiv/2LqHKDJHWSwvGZ8J3jeUuWVCNiYH3/Oda8AmUiv +5MA+jWv2KBLGt5iEl3UlBFKLWg9V5RV8oM09sFy4FDO0El5OmFeyg1cMlh805Jad +xGlgUbUyv7JIx8it+00QWh0aguNcLa59InX1GoIF4g6Tdy9j -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c7.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c7.key new file mode 100644 index 000000000000..70700fd5d4cd --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c7.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAtcFY6Y7PKZoyIxR+4CtuVvsxBVKm9rRxeKz86wk9ImGgeRcl +tgNdPlMrP5PQCp+bwu1yFT8T1I2In8xYl3RB+lUQchGnlWyb/2mYoyM5Jg+cmt++ +1sxeXlp5fJLTWBCoVO/k8Su00U56uIhb5luo79Ex3LSiqMFVNmRbV3Z6vwULMu/t +5kMoys9AT0b5HTFkAyGD1nk5s+mtKL+r3N7lB+y7kxXFl1x3O9kxVD18lkb9x2ZF +OhzZk6yUb/QXxayWoS388k884A0j89Ibm2nmc3Ct+bBbcKro3LtLCKlVcc64TOai +h1mQgY5M0mBEE2qSMp5H5CuWlr2no10cUMWmcQIDAQABAoIBACtphpimpSGdNGt/ +mjxl80FwVwmb78hOWioGO5TSDJmZMvCpUDcUv6FC0Toqzo6zB8u71jtv6QckVSVB +9EkcmiO2TKrKP7pnii0r5irVH3HRWcj+N0Fr6ABmYM0Z7sQCmopLEGZwV4yEz7gi +zbyMWSIK1/uM46smHnnS56RQ5smX/jkd+Vd1pExpe5z5t4z8JGk/LgYt42nXvkos +0TlmDQ0wl+bkh1MqImm6kU5NPwR3l+36wXqV3geL7WoJ8aVsJdG040udGZXVrzO0 +iXOXzg+r9hQHnLzFWf1VocA4NGkUQZml3J7rh5tbOjtWp+lAbVgAH1l5WL2J6g11 +EVfm7IcCgYEA3j7lMkgnnJgm5j8OjIRYoLdCxL3glpo0YVDKlHWg4kBg9hIr/x7I +MpV+4sYE2gndPttoal4ISxe8g4FGiCD6ZDeZPaA1ZfoqxWgFoxcnldYj7dnQGbjy +3dnGtEJm0mzNSTtTWXw+mHf2wJfDduS0lMxe4dOxITLao5KZD9kdU/cCgYEA0Vwm +O6v+GQXThKqp7g2pUjWHYMiswmgdngp9cipXzVzhuCaOumHSsNW7KfLpuqJ7MG9Y +ZGzmJD2c21UEOhz7/9TUC88jcbtuvqc2ay+UxSV/OTovzqS420Ebgq+Pmm0IIMIe +hTSs2P/GJcTdQ/sM+/pPiNcF39VK0HZGMLVwbtcCgYBqv/0pHL1iGzc8Cve7COrl +L4FzrNyOmj0krUDQ9Hs1kcTBDVNytF9m5KXQ2VqFPI233n2V8dq77wfOhq7RPj3n +G0mQ7z4W4IXg0ik7ACk0cGpLmf66DDsj0rH97ZMj6bzlOwi7B9GtPJoaQgtSNWKD +wnKkuCDxmFKqlQTONvQ+WwKBgQCEr1Ev2lZFB5NBbmQaEiBwkKpG5YfQAepbYiyU +8U8KQUZwzkcAysSb/CkNcf09ooJOdRLhOr800rOZsizmo732Kq1k+pdtGiwauqDb +l6FCvPv/iegIoA7kLkRANAHgDh/rmvt60qKFOL0c7MKKtg5JmB9WC2iWrngm2/6q +GiOyqwKBgAmeqd5nc6uEn/0zlZjR19jlXN6WWvdBfeYFgh2rz9imL0/bdmS8ZWkQ +yfn+Fj6uKpTLYT8kzVattqddktJCYfW5Lppq2QoNOPvVQWk+HYRsVCS7Y85eP4af +vTra8g3qkrXUFEit/8gM6SHAL4XE8pqgsJ0F9cYc4KiWQKDfIS5Z +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c8.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c8.crt index d07bc610f1e2..bfb74f744658 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c8.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c8.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAKEAVAjsgY4Ft4ATYFfKD8C11yUNMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNVoXDTQ1MDkw -NDEzMjIxNVowEDEOMAwGA1UEAxMFbjUuYzgwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAIJQgqB0qJj7Db0GiPnvl4AsKtFa5DV52/DLfqri1FwhOcIVhtnLXFA2 -wsUT/g7aJ/b3tZszfTwG0mKSmNS9voTlpaLAb6Gn+vq6bBpgvgW3kvcP+r5lxMIG -U0zs8yC3hqXwUOX/FbWZzd87eEiM6fDfhwg4JbP7nSk/JX8MUEqLAgMBAAGjfzB9 -MB0GA1UdDgQWBBR9W6ltaRBtK+Q3dWeXPFp5cwdSFjAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNS5jbHVz -dGVyOC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AKopACutziODzeAqmRV1qNb816EqKoKdV7KtsiopOH4K68+VALvTJHZBHHk5xsM1 -LvWFvjDtd5zH2KcLz+Hqq9fGBn8OWgUu7CK+S/rbMLpM6eM4O2UpsCRnvXvWJrnE -Xm2UdW+dVIM1do1/tebMRJA2KQzZExRlWIz+idn+lUFxSW7BZM3tLOQ7RgAuCTcA -F7zFjCQROi9PnIyCNHr2TGGJ0GiJuJ4Lovj4WB9jA7e/bYPJzbY2zp4cAuIFBZZU -rbUGeIJCvY4jEyV51KfFXXJVMmCGtrtPA3GNXeqhS9ph/FDftakMPTedx4Ah+hL6 -fhSR5Ce/PvqTW9Hx7LMz7qw= +MIIDUDCCAjigAwIBAgIUeppoImu0Az/EBkGkblU6YMOzZlYwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE4WhcNNDIxMTIx +MjEyMzE4WjAQMQ4wDAYDVQQDEwVuNS5jODCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAN33AnMUUC9RLgDwI40dc1G9cKBeVwBIes/rmLQRMvMoeH0+aPLt +6KUapUSO7w2lBiNWKxMiqFaiVcvg7LF7wgePaUrSBeDN8TS8/gqYA3TENJCNVVx2 +n9/vxsXdzwQkTfmk/wJe729EOKxg5npkID8a9gSQgdEs8oyEQRCXaVmmReV1QeOy +j6nKTz4k6Mf3oHbnH07cSLzSq1NF5vmyP/qNSuJxtQhof7F/ab8tZc76JIHuZ3Co +J2FPPra9xh2LyFxJiO4FXi49kAypjVgQ9km9kCIc0RecZJWiXh9cO+Txuf5aHfpO +v5MxFmr3Ox+Ltfy6iinQvbc0nnFetV4ZEJMCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +B35kkSeyeJdqmCS3e57y9o3ydewwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRYIcbm9kZTUuY2x1c3RlcjguZWxhc3RpY3NlYXJjaKAl +BgNVBAOgHgwcbm9kZTUuY2x1c3RlcjguZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQBvfDxlJJ8kdfTpkBgEd5QcFJGlABmpfYSilyUQ +vFKrrhVxUCVWARoR1aPUvVAm/JqZcognc051rS/nQh2+e312LiyByMrRFEnvyFR2 +FNrNAQhs6H2Rhff01UMn3CRLF9ON4JDuYbS3u6skRNWh7y+XdTyXhqfa3zcviJ9G +wO9PXrC265CVMr9P2Irucl8m1KBbWBVM+8XtwTl4VR9p5l5zY+JuejFYIzjfkaKR +4Im1EALvqt8B+vMTjKUuytB0C65vdKGEL68HnbI3hRaxlMVQlFf4Or6rFTl3VGN0 +ynellyamTboyHDzUGY+875PpxQ+NEaJiDgDq+oIrth9Dry9w -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c8.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c8.key new file mode 100644 index 000000000000..57d3dfe919cf --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n5.c8.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA3fcCcxRQL1EuAPAjjR1zUb1woF5XAEh6z+uYtBEy8yh4fT5o +8u3opRqlRI7vDaUGI1YrEyKoVqJVy+DssXvCB49pStIF4M3xNLz+CpgDdMQ0kI1V +XHaf3+/Gxd3PBCRN+aT/Al7vb0Q4rGDmemQgPxr2BJCB0SzyjIRBEJdpWaZF5XVB +47KPqcpPPiTox/egducfTtxIvNKrU0Xm+bI/+o1K4nG1CGh/sX9pvy1lzvokge5n +cKgnYU8+tr3GHYvIXEmI7gVeLj2QDKmNWBD2Sb2QIhzRF5xklaJeH1w75PG5/lod ++k6/kzEWavc7H4u1/LqKKdC9tzSecV61XhkQkwIDAQABAoIBADPW3qGV2uxSH1Zk +v0jT2fz7K3So09TjGJu6wF09rxKPHyTREJ275pu4z2RlpXsgUYCrPUAo1fv1yefl +ziHdb4FogwTQbEMGlhDlGsfKnPQGZuhjuc3wfoJEc73+gbzL5dIqMMyS84SfAEVy +FCH9hHJ4j4CdZ1Q9FSCwOqnvUtF2zl6U/2lv40a1i55lSgGHoxMDs44UGq/F8U0K +muuwbiKkiRL4ICO7ZERCCxrWR6pRfdrW8BWJyzjsF0daG6RRJa2RMMTlyOS2iEG3 +7N5iyQ+PWKgjCAuy1L5w7TWHy+3tv3E/el0xsGcUavVMPPKQC4LojFb3cAVFvlWZ +AZkshSECgYEA+iWrST6iWnxRdLWUmDLLrh28dxAGrZ7oNMwlWQd1fvJ+rrSlnhle +wuWLYLO/vy7dHQtA1l9eBnbueT2O8i6GVnArC1Vhj57OEOe3mfJo0ZBpbNka4tE+ +P7XzWTAUyRnR1lZg8AnFNDQO6GwJrIQ5lz3u2r1IG6UlyhOiafJP6HsCgYEA4yiI +zxdpCp44sHQCeeGDPlaC0whqWXRo0poP3PPSA9k27FXrFH6EVc6fyHg4ltwsAyST +fjF4rfdVwz3RWW3jMPts07seV1U1Ahm8a2FhgRwRqs22bc+ivpfNmWptua46dbg/ +ojQsl0z9SFwivWy0//RZfw/g3DJGbnxSGfGwGMkCgYEAyilJXd+NzH0uQ5lAZySe +MbB8kOjVzNlhJtJZ2LLLhXYrZbw/IR+10uM6mkOK0tLoBkIC+32FesoBfuB+N0Hc +dd0fHIC7YRvhxA3Q90zO45bHbR2kueyLV2gQLGNMWR5R1NxoaXYr9z2fUr2Fr/QI +qy91aziQHy9/8+mcV+1qISMCgYEA1ONsNAmQnC7kmhpfJ7K9uXKCcRz17pg2lQll +zIGFVa4A10U+Z5qak4BuJwbkoh398YQZ5q6elwuyCKSqytE9lu5Lc7NuIBLu9uT/ +vPMq2kFgHV9llUx/DXhfa2RfKSGsEB95I58aG0JALcyLklOK3kY2ieprpnJHp8xD +daUYkGECgYAPuE7wJck5U7DyK/IZ96OMUjja+kW0inv4LnrxawSV/+lvXrj1mRWA +8+WniM04zGml0YXIM0tg9LFH4+qK/DXFOX9a8zJC5fKNuh2UevKvwnfdvR+qNByl +ll97AoIYNLFuFnXJYqdbfJDr1QTmyEQKvjapxh55LWo/j12hQb6Bkg== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c1.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c1.crt index 1c8ba3354b14..6b8811f62055 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c1.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c1.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUFpfeijJ00JLPz16Tv/3R4i0TQFgwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjEzWhcNNDUwOTA0 -MTMyMjEzWjAQMQ4wDAYDVQQDEwVuNi5jMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAkUdwEbxEusj+J+9qllbp/jYccER/dTYUWoDClkxfwgNUN10Xs5A08ERS -s1DZ0IVfymRwQlyeciUY/Q0M3gkRXY3vU2M39ZPAEWgcMBezp7VopotG/4fPnGoh -21vlB+qNu5CwcWgTcdWtsNmfrSkQ03okWfd123WcZhqYxHJxL/sCAwEAAaN/MH0w -HQYDVR0OBBYEFMsR/lNScpkcl9QxX8JAMVP7mT0WMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU2LmNsdXN0 -ZXIxLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -hFQp+02dHbsLFZs0PeqWDb1Q4iB8+0esIl/GaH9YuEADxjDaOGT4eEvWIC3M8kh6 -6v5OEJO8z43y91EWJdqzu58mF51c/18u6paC1EdkO0V5xR9TT/AXgNFzoswmlpgJ -G4CI/pKuTJlPlu2zdf74/Npufl3blx6UNdE705xmPmfS61Ws+wm5Q2qGKF0IWlUM -98OVPftL8MLv29ODtD4RlVe9d9WLlNZKq+0WoIG0UOOHaBaAyC4ZeHbc1LKW8m+S -hlyZjc1E3p9epCeZQi9rT1xPs3kRxsiNnhM4HVVtmiXmhMDoA7sO9A7OfYrROpLm -0V84QIeyRw2ArSh3si31cQ== +MIIDUTCCAjmgAwIBAgIVAMiSK7pGFp+kv+pz3h+rgyPZ4/qCMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxOFoXDTQyMTEy +MTIxMjMxOFowEDEOMAwGA1UEAxMFbjYuYzEwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQC1vrEJFi3Nq75o9gsikD2jy2VYs+bGi2ozRa6RbuBHKzVV8g3Z +Swhp8GZ4l+o1sOu9xwVxqdps1LtNaE71yTuqYKl1iI+bSfqdozRQY8UmIXjzEBwi +8/3Omu8ei5TtGypfXoM0dQf0D+A21LkY/3OUdMPxJWBq+s9XiwjiMBqr4ZJC7SvL +0RnYMRYzf/3J9FHQTv0EV58ycuKDazi9KmHWurV4RDHDZTzib16BaJxwoiXX2bP5 +tRl/tDNO5CBBjc1FUi0hBKQD5WC4QxBfARztm0loRbBZLa9ws1JF2BNhb7pzJNq3 +amhrK7bViD2NkQgFEW7mNtOaMOk4+qbzZqaJAgMBAAGjgZ4wgZswHQYDVR0OBBYE +FDZ3xKPK9V5104JEDAHiVwsT4izeMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWCHG5vZGU2LmNsdXN0ZXIxLmVsYXN0aWNzZWFyY2ig +JQYDVQQDoB4MHG5vZGU2LmNsdXN0ZXIxLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEApr+mTLOkDbHrhxeb1ZsHWlFLsyb83edr7U4K +G7ZXUioXEYhPqsk5iLjZmLH2rTyQ/7pN08zQminUbqjOOu38E8HzeAIypje289Cz +5wNG5qJIktnmBjnVS5hG01aFh5EzzoAg4xUFG8utMY2lr/S2TisxubX/d1N+wtMP +O3SaECk7ar/F1mr1l+HGYj9tQEJ+qfb3Din2Kie0MPKD8BL+TpMSS8Cti1vkxdmP +ByYroCQmhohv0IAoCNlsnHm6SsdE5yseI5HmjRxgauFOHfzeebLYVx7yUkJARdzf +XHJOeP9ljd6edIhkHuswgl3JrU2WhMhGuDMRC89j2tMIOZ0h2g== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c1.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c1.key new file mode 100644 index 000000000000..45c1d482100c --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAtb6xCRYtzau+aPYLIpA9o8tlWLPmxotqM0WukW7gRys1VfIN +2UsIafBmeJfqNbDrvccFcanabNS7TWhO9ck7qmCpdYiPm0n6naM0UGPFJiF48xAc +IvP9zprvHouU7RsqX16DNHUH9A/gNtS5GP9zlHTD8SVgavrPV4sI4jAaq+GSQu0r +y9EZ2DEWM3/9yfRR0E79BFefMnLig2s4vSph1rq1eEQxw2U84m9egWiccKIl19mz ++bUZf7QzTuQgQY3NRVItIQSkA+VguEMQXwEc7ZtJaEWwWS2vcLNSRdgTYW+6cyTa +t2poayu21Yg9jZEIBRFu5jbTmjDpOPqm82amiQIDAQABAoIBAAMeOkqT/jVt/cJu +fTaqt5oCrHKTBJGxtODcEFCHsNKbwcqfkGAC3uo1YQau4YpqBoNfIZ9nePs+w5di +g59JJGxKBAJDlWYmROXVNdmuuoRK1PjDlR3+5mL36sQRBD00pvuToyfgXaJw7sl2 +CRZuFIkunyl5GRBAzROdC6oYxe5etPtC++C8xAiR/xA+STxmqKoW0p8olKDczgPx +eHkGn7L8BeTLCL6pkWF+P8kRftrqvjunqXsuNOpkS0fvF0uJZtp53aoHRsOuGE6Z +ts1EEMvgWTS1FyUSz608csvuLGaXCUNSGcujHAtw64ia5sAxym3cnXJalBiAvnru +h0H11HECgYEA/o4cstxP9B4FbYE0NNVHw+RhVEZZEO93yIeGg5Klrc6vcZzRcJR5 +q5HyhAx3FJUAMe8wWLtu/EZgYPNSEAIx11ibrOc1aXBKwV6d20peBvOKw1Dfg8aQ +aUpclZI2s+Jzx6ab/1FnwRm0/73DqlG3WFl/S0kfW8F5xR2LyKcXexkCgYEAtsbH +2HuyCY95LsZsWSZVKp/M0y5Aocxfreu7LMHDPHH6uptX/ky0LJRkirIgQFt0J48X +sb3KO7X+FqrVQ034sBUiKZ3q8HnZ3TTtQzOTOCRv9fS5VxVe6uU4wtHcqm3Hs07p +4Urhg03xU+czde0l9cL0g03aQvoxUyPw1OimZPECgYAqZjlZgWC/5JA64R50/mFp +u4yKoP0M2RY2jHxXRgHapWaLofcwrUpmzXR7kifzga36o0CGRxIBayd6fozYpcTv +fY7QieJXoaCR/NSGRijLN52RUtMn+9bp8rzhHO2HoNxwEMxDBqglfcasIkuI/vtu +99lADPnwKpXBw14ZGdvLYQKBgQCtNHiDnMydpLmo6t4YKWxBxUdbxJP4/+yXqzEn ++8JhEH4Swk1Drjp7mHAiwyQu+Lt9FWvVws3XgeJ0ZpVMf6IwcwZ5u629Dyu3JYXF +ziUv6c+zm6LbdGQLYwdED432b6v0Ls8R8Vz9iiosYYdxtDbpy0Lc/zTbja6aZA/A +I2wzcQKBgEkXDsmRmUswCZg0MSATbjdv/eJkcNOMkEmtQdZHFpE9eS+1fEgB3m9I +AYQbqom6tS5sOfbURES2EoxLOrVExP0v5C1YeZQJJxAbysnVnRoR3fgOcpNaZI9n +ksLkojWPmhi4dwVkUFwAs0Uy0615i7K29Nc5KPK0536sLSmVxRQq +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c2.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c2.crt index 0380402e43f0..c186b2b30e51 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c2.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c2.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAPzUcy0n1Ww7M2AliNfADZ8oKBhpMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjYuYzIwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAMJ7+brk5AZGhtD9dMKVhkHCmsAhULpd94aF8nK50Vw/Jgf/SdE6yYeP -rBxBC+Or+o5+N8Dwe70escEOH6o793gg8yBWyL1AagoJZDV/+A41cxropz0jVW5X -4SJu+y9tEgVA7m6cdefow1SG1EiMLMtEHyw5wKNmUKrl544O+MQvAgMBAAGjfzB9 -MB0GA1UdDgQWBBQGIpG1JOyVo9JCyp1/h9yMhHE3lTAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNi5jbHVz -dGVyMi5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -ADM6lIy99jbMevqPYHqxVhbCnfXP/3+amzXOn6OUGme6pXkfZod52CGjXeLSnEsY -OTwMAlzpp+ghdNHoig+ZewyBsSimdiHU25TZNAuOIFHcAvrX0DPm3zVqnmwAa38a -Yft8UdcGJgN4oFoY9vCgxiSmOgF/hBHABmj7/WO4tHiM2EZzBaOJcOeGaYe1e8t2 -z9LY3mbtdUJXZktB0e0MVFsHT1Q09+U+qnJzXWoZ3zVeJkcGt7E9RUyzHa54qFfa -II1g9+BjWg47iuWm15W0B0IJSZqeTcsvTtxSPbX5Y3ym5pcWZ00IMTFvRlLaLPQB -e2O1dfbp2RJd8ozmBcI97FE= +MIIDUDCCAjigAwIBAgIUMo7Is0KW3xIjT15ACPBe2oDcco8wDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE4WhcNNDIxMTIx +MjEyMzE4WjAQMQ4wDAYDVQQDEwVuNi5jMjCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBANHVNgKap/Tr0PvyGN10vaveCsjbdSsbsfFT74HFEGg7Qkx0QrVG +VKHDFbzbFHtPDQyVFXK9M1xJrHmC2esDBbwdz4hI+sBBLDK3NyJzSnenMI6nh7+o +UGv9OTBx9t8eZnmlnRDdJNnHhUubjHj4Rbb4bSQNJsR75eWJLfiuvt7SwLuu12aG +AVxGYQ/1/CycQnKargRqxAmSGMfWdKHNnuOnbfv+icGF87/dETXumHE2as6n5scE +sbUe2dzcmV1TAheKp5Xh3tXfxy2tj+q7Ea6axU2LLMoyqveHZtsBEm1TOpR/hDbN +geOmlYB1VVx93WAtwZBX7ZGICytj9vtINNMCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +q+V4VDqbFBgI8marci1dbccWz0YwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRYIcbm9kZTYuY2x1c3RlcjIuZWxhc3RpY3NlYXJjaKAl +BgNVBAOgHgwcbm9kZTYuY2x1c3RlcjIuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQCF42NFkQxSPLUJG/SHDlOiv0ZlmGfSU5N2t5k3 +ogqMaWy3A6GwOxHVnyt+bV3YU4W7MS6SzzMoAXwFlw84VzO8NXriAdVOvRNTddYd +7XxH5uZq69/VOhjs+DjlF/I1+cEChXlA4qDbBQ87sQVBU4jtnnfJn+prbpaBtCgc +UzwiwpN+lt8W/QMETzMxCpXE1/fGc+qE0et3+ENGRblbg/mbftoJ6SBmUwz00N5b +IdxTEC6rjPep1WNlMPBkzJrxXAwzz06Vku6uWXuvjMgka2XFBBCVA0226Uc9ErEt +C69e3OuXFZ+ZdsimRzBaWfNiQtVXWOgfVAjCOqYQYg9Pou57 -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c2.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c2.key new file mode 100644 index 000000000000..38ca2accfe88 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA0dU2Apqn9OvQ+/IY3XS9q94KyNt1Kxux8VPvgcUQaDtCTHRC +tUZUocMVvNsUe08NDJUVcr0zXEmseYLZ6wMFvB3PiEj6wEEsMrc3InNKd6cwjqeH +v6hQa/05MHH23x5meaWdEN0k2ceFS5uMePhFtvhtJA0mxHvl5Ykt+K6+3tLAu67X +ZoYBXEZhD/X8LJxCcpquBGrECZIYx9Z0oc2e46dt+/6JwYXzv90RNe6YcTZqzqfm +xwSxtR7Z3NyZXVMCF4qnleHe1d/HLa2P6rsRrprFTYssyjKq94dm2wESbVM6lH+E +Ns2B46aVgHVVXH3dYC3BkFftkYgLK2P2+0g00wIDAQABAoIBAAy8FnWhbc5fNOUs +Zj+jnm4hRbNt12QDOyNvMF9q+zDKhZ6xF6EN8s2IwJwgLopTAHLiLx7Eaxi0MRhf +8hE8/Eqv3sMYrQ8zHDvfuiQIQF2XzpN50kVIVhJOmESEIY6dAEhIAtQCU0ulctgc +JOjs3QLaoFUTgYqIVwWGYYZwyR+Xl0cbJ51TEtB4ysGfZgXODzxukckeE3Y2qhsJ +NPcUkeZqcWAYD/YTfOJ5AYFGVJjSGbBbt6MhKJhg4nPc9k53JiYWvzY+3SQLVEm/ +S4SiGXVfSeNh8jk37inmvqI6HoqB4rDW7ietVSmNSaC/NgqbbkyJBenIBDMP9k2L +2gApXIECgYEA5nRmiHlvp+PyzHMMTjE3CrrGafcF22hLzNLr7GNirKzEJliryaS7 ++xjm9tv83xvtE9/bw3/As1lp1n03mwVjVE9C1aTEThMfjzYQdIKiO9FZxshSpITm +xG+6Fwvx8wi8PEQKdzqhSpn1/XCr8qTr8BwhOvSnMreE3YCaGtstgDMCgYEA6Reg +OATNXv9sLh67i3q8vWDEeUE+N3vcvSnLGsCzTQNuK7AtSZoJ1NBKIDx+sAcMrTk4 +mWm/9NgI+zKEgbyGZXNM3fW5882gJPTv8ZypDufacmeNBUM6GjvxLHA/5bOZsW+0 ++2u4MqblQlPiU4CVKQPzNh7sF6YpfN2SAN3jWOECgYAzWLgHrFbuZ2GPoAqb2WdU +Iq1Wqs1JR4GsD0q1jqdLperGSHduJCAmIDRFyFodM9Gs3fHPK0W6MC/etGORTLAG +9uoDRAThCG29sOQVsY2IGmsQWU3gjiQEnIgv3l94MPgPzXEvg4awYoFmiJYnKAvR +odLdmeku6ajW2XJ8jV0j9QKBgEu2yOxpYuyAu3sfsHvWGojJbgiVEMilC0bG1JqU +Danue1Lln1BRIPBn2CwwelZjc9Kwy5nJp/i4U/KErlYWDvHVShqvPv/awzI9/dLV +uHSHC4Yt7LjQ5YawAd8Vdyv98uaYConAq8yIOeAZr2LAWbNA/TePQ9FKwB83Ob3e +VYMBAoGBAOZX/KESQzOzBmerbTWfuSAjPCzPNQQ0b1H18vqj2q3HZ1jIlAav9SvJ +GOTk2b5rM3kgxzLEaOI4s4Vlel5micMcVF+oN13Xly4jCaM1Mu8jZ8nQznWdlIxn +9a1u0mD6JWX1QOML7Pr8MX0v308cqLhXWULlZcJFb/dR4hNpDl1c +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c3.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c3.crt index 8ea16b43baa9..c042ee500770 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c3.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c3.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAMdfglCcL1l4qotlEg6YabyONoRUMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjYuYzMwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBANBW+tmJaaum3CPc+7wj+qrDkbvuDue24I2Yodp3HSsChx94KmFDYhGY -uRWwsDF3Cu8iPyUi1xk4fCQallLCzkwny9j6dljAHtAYkVfNP07WBdPJ79IWUn4q -bldqLTam9ZiaQZ0jPtHa6PUmj8doc0QWM1ySfJcHqCP53GkqpH+vAgMBAAGjfzB9 -MB0GA1UdDgQWBBRiy14zafEFR9kFLpd7PeVFrLdKUjAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNi5jbHVz -dGVyMy5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AJOOREHVw/nFyC65uY+iZIzMdpcmKKWdLwaOw0MAn0ru9B9m2qGT4kwCX3m3D3HJ -53btTyvHa/URXjtoIPL48VOt9qB30MPAyjQjR6C1po5DwBZHVWMZ2QJXq5RBol3M -wamYq6dgzyUxWYI03/51az1IuU9N4IS2E7/HN4YVWqE+CPNroh5kvrg85QQuiENv -D8NfxYYshGJa+kTTuLnkyz2EEfVwWeI8+7x93/tXryZE1b1EvNMxkl91ZJbtg0bC -Hb/v98RCddP50shEgUxdMQZD2ukkzCabnRGmjuLsvH9OtsrSqCoi/0umapW9wea4 -fhJ7Rt3VHnAkpbsvV3jOx0M= +MIIDUDCCAjigAwIBAgIUddNCAkMn5XshcN6RxKcVQhoT1U8wDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE4WhcNNDIxMTIx +MjEyMzE4WjAQMQ4wDAYDVQQDEwVuNi5jMzCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAOMYtGJ3IhRYSxbHAsVqc72B29mI4pLEdK4SptLpTfb2hVFcjozE +5CXsznkXhRNH32hLliW+lo4XzGrhAlYilKvlt8J7h2ZYtSwVlgOr0b/FEUAMxwBV +MG8NfQ9cQDGtFailzq5y0akTSRjJ9z8odW6XNHFLlzqz/MYx5ThWk4gg8EnimLzz +89NeA0sf2pUEE/JVjYYuYNQbhhAYCVYHsuc7Ls9HWTnzUBmuAoXAfDtLlJDbMWWZ +y0sC5MdMsO+1IknD2k0FS1S0aWlMniLfM8PpwWyruvrOKsbjmL6bGIO6kr1NMt9L +YRNw1Bg538XR9kdVIs6iIVpIPdQ3hqKEZlsCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +rQkS5bztbA8O7x98RE9lGj105ogwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTYuY2x1c3RlcjMuZWxhc3Rp +Y3NlYXJjaIIcbm9kZTYuY2x1c3RlcjMuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQBI1Wc2Pa7Dhh4Bp6JqyjULaPnbJmEsyre53o45 +hzQcePWr/l0a19GawnDIKyj0W80OH2IHEAuRTTirUKy0vK6NPkuCjNFkqYBIdpRQ +AQmG4pYGVZJVSvI3l8KBPJ0+n5cskcYovDpP4hCEDUf8JOpPMm4fuiqApf33scZ6 +TOVl38WUk8PMj8QvjMkoGZepN8UHPrcSDj8ANDbW9Itg42Ngl18bBdSwh2Co67gA +exMMCDqJCHkai22Iw89zrNLCy0zJDwpe79xmDC5wwjB3F9uv6tjG3kN/go/xfUls +CUtzq5TUvrQFC1JwW5Kr4vTaceVsDC7zU3qjjnlb1XQObn4u -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c3.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c3.key new file mode 100644 index 000000000000..202f4ccec11e --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c3.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA4xi0YnciFFhLFscCxWpzvYHb2YjiksR0rhKm0ulN9vaFUVyO +jMTkJezOeReFE0ffaEuWJb6WjhfMauECViKUq+W3wnuHZli1LBWWA6vRv8URQAzH +AFUwbw19D1xAMa0VqKXOrnLRqRNJGMn3Pyh1bpc0cUuXOrP8xjHlOFaTiCDwSeKY +vPPz014DSx/alQQT8lWNhi5g1BuGEBgJVgey5zsuz0dZOfNQGa4ChcB8O0uUkNsx +ZZnLSwLkx0yw77UiScPaTQVLVLRpaUyeIt8zw+nBbKu6+s4qxuOYvpsYg7qSvU0y +30thE3DUGDnfxdH2R1UizqIhWkg91DeGooRmWwIDAQABAoIBAFvvLV9LQUWhNMzE +xg9rd+14+6qF6stlsqKhtF1fy4s142S7po3AS2g7BJ8q8VmL2BpdtywSitp9Mej2 +rD54g11PguO9pGO/d6CgIHnd/ey3d7NJOOqekhvzBvTH8MjrfAApF5Gxs8TRpN7f +LDOqSlf56GYRJujB4ovNuTDkg5cwkzZgGIVutbNW6tuQ4PXACI3Nj9mLy21L01zG +e94fiziFdZUpPy9nF69m0pPTxIPjuW8TbC2Hvx5Z2e09cPJWcpaCwnTPgleYC+yQ +5hJ+qIa818cqbs8atyaiHSVngptVBTBPVkFosvozj0V0Vym4wjV4RDukN+EUNGMd +F11FXXkCgYEA7SJ6s/MIW0+4mOtxSmT4p2tMAQd+U3eyZody04BCRWXfCu7qgZdz +h8dg6KxN/ktjMj+L1hMVuex4T9j3P+omSmbei11TvNZtrHkbr8nCOHPI30K7+G+v +9omk/0lzw4+LR9yoo3TnM5rQZzD/UaithV0OcWZIe0fcAAQrmG0GKH0CgYEA9SnJ +Q3/GQxwTlkDOrhMRh2Ri5KDtFTvXr4zfhe+ZDFuO0O++ta7Y6QlqrPZ6Xze+L3p+ +YkoXzH4/n/zrbNYNPMmI2eCwiEc78gMRA+3nnzzRKtc2f2Zeg0WFWURSqnJA8f8e +hO3yi9SinHalmxddATSWvqWlgT0Ps8KU3aNSWbcCgYEAs9eNZYIw2Ud5wYf2MKyp +JBes2a78MIM76qRZAmxoVkdvQ9R0R3o8JUKhjXR35h74iThGbkGlHY9oo23x/igv +iAfCScGq6zFYlhB3XfdO9A9jzWbINh9GPVygpPfo8uyffg2sW+MiITINUeWIDT82 +8kjj1j7zQ4P5Jo4DHiO5IuUCgYBUQEn0MmL2CR4KErd0RwVNHAiIPW18UJZss+LU +6mQURRqtXtaRJUdRvnS0f/fJ5SQsuNHaAVBwbCp9VrOpMV5XN4MaLQc9rg1BvCAK +06U3qfTP3inSWmG8hxWuebZW8PC0uVHJqOQjfa1sS7HZvIuvsFzlo5Ne+Jb+1a3H +BSt1vQKBgFJxydI5NAP+d65nMQI5lCm+l5kCy3fmElmES4yz27wEqHexluqgpjM5 +0uIIokkdznHwbXP6kONwP9wA2ikbHOe8JCEGhtKqcKHkMM1msnBS/SDsBUjWR6uv +/bbgJOrd4sJeAhGCXTf6X1I4MW2+pqJf59Eva8PDBsEwJsZbWA03 +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c4.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c4.crt index 45168d7a670d..21d7df35e672 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c4.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c4.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAIwepFjviZuQeudI1+ErlWyWbb21MA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjYuYzQwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAJDY20sl1QeuOSxilJdb2r8PygM/o6EPlIJk/NHc6Lh1VG2bxqsS091j -Cp1UY5uHdynMKXxVhgbIG+rKHgP67rh+3dTWzvThWDMZ1ljMMpdf4NNH3caM7Wdg -Pzj6FpJbuEevBnNp9ENRjKFv8DxUDiJPFsXcCZ/sGuK0HUZSzla7AgMBAAGjfzB9 -MB0GA1UdDgQWBBTPFm83nd3zU/X8KaQpSIF8w8njkjAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNi5jbHVz -dGVyNC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AIsgxyXgff4XN3ZuhybqDrxKHvMOOOa7npzqODO97/oZ78+Xs2XMbDSp+ruYZylt -AMsos5FzLydhYESKE2nrrMEUAY+V+mryNGX5lY9Z0TvdjyA24uVVBMSE+R96Cm8G -UUP+ekbou3e2aVYKOSyaHL3mwqbmo/CB7VkzmOxzcCqVMB4Oi+fD5EQ/qqi3Bi5C -FNsYdelnWQ2WUX7pfC8P+F011WiM+HYo5I9hhbuqf5/ALG3CRERKZiXcMLTgFsys -ckjB1QHLVK7yFpiSdUqqAZb4ZPMtP/zr9YjRwyMRH7vDyyQSF6pQm65vAmT7+h00 -FZUilhJHYxVDYGCJ/AQXVyY= +MIIDUDCCAjigAwIBAgIUXVOLQNWQeU65Ho4uzZHeY8+jGXswDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE4WhcNNDIxMTIx +MjEyMzE4WjAQMQ4wDAYDVQQDEwVuNi5jNDCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBANMHcSUqk5S1+RwGryfgSkX6yHvKfLuImonuwSlCN5vgHHKbo/M3 +ErSOmmyQIiCTnF54i293DaYJ45UdjfnSQtXkoCOIi4nBBLg/hhUqChJ7dT9sBczM +aB7/zI+XdVridPSzOKoruaN6S8cVBOpXsr7MGRWvDQsrxfOG7kYDveTQ3dXcU4Xd +C37/+elin3jNvoEfOuTepxyEF9InpWmvdrh0+xwI/yxCkK11daEGPzcBuZkGL6sF +/uGDkiHKYxaTm4utGLHCdbgHCnUx504ty/ds0DXJQNcg5vyFlruwP/hLrPqPbq9P +08+LUygUuRWcRtrOZEx5gWX2cRx2wHl5sCcCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +T+7pAVYShdWkgXnJEPzcM9Ec+REwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTYuY2x1c3RlcjQuZWxhc3Rp +Y3NlYXJjaIIcbm9kZTYuY2x1c3RlcjQuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQAkwdv5IzAS9C0730Q0GQm9bMv4rmH9eQMyB/uR ++e7my1xkCND+IAriB8BYmNATFdykP1ciNZTi73xGJYbcugNiNGDJ2ipOPVj6nXpf +3UfDtsS5LwSjowB9LyHqvH/kJX50zGKzIdH55MW6SBGs1yvYgJ0LhCsohzHc828A +no7K4QWLIO81XnVsTRngq6Z7yMvS8BKQHYox+HHTOORIRZL6J9bAhnXG6UGJEWOs +f8Vr/bSWf7RFk2JI8tkfPJz8ofTYqdEh6oOZKqjzfJ9OQkruGJ2DTn1m+7kETc0z +7JeG5sTTLTYzMEfIg03nGuh4HNDQQ4VwwnOFoujCoqOhqkyO -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c4.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c4.key new file mode 100644 index 000000000000..95c40f19276f --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c4.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA0wdxJSqTlLX5HAavJ+BKRfrIe8p8u4iaie7BKUI3m+Accpuj +8zcStI6abJAiIJOcXniLb3cNpgnjlR2N+dJC1eSgI4iLicEEuD+GFSoKEnt1P2wF +zMxoHv/Mj5d1WuJ09LM4qiu5o3pLxxUE6leyvswZFa8NCyvF84buRgO95NDd1dxT +hd0Lfv/56WKfeM2+gR865N6nHIQX0ielaa92uHT7HAj/LEKQrXV1oQY/NwG5mQYv +qwX+4YOSIcpjFpObi60YscJ1uAcKdTHnTi3L92zQNclA1yDm/IWWu7A/+Eus+o9u +r0/Tz4tTKBS5FZxG2s5kTHmBZfZxHHbAeXmwJwIDAQABAoIBACe9V4fFuEwBZrtp +4NM0EBiUjOuO6qVQKLX/4Z047Irsjb7hY2L6RIn+7sKx89NJqOE7dxqTA114sERU +7/cTKuXnq2Z1oL1cvVxLDh9vLr1jRWWfnIoAFj18M1P5FC78xUSNeYbGPCu2bI29 +fhL5V95JRhIIyH6JjDn7d2R08O4lNYkjtFkXvwWN2rd0QChGi3veWwOPzvQKkzdk +qSgAE2HYYRB5fGmMJgVeWumGswJ20DuOLNJ6RzGW/YAStU2JkdyP6qFszT6ThQbc +DkaMCUG8woKhSDzLdfN4yXV0Ij3ehoKeoGwyHKfkp4HV0vkPkLWkg1oSC4CEIqJp +hOu8NAUCgYEA4zrGKQOHCxBTRK+eNGKqgUD4ON7pN+megrfgO8aQEGt2zT8SQtTw +qI3USpCp3Q6e/nToBCqzbp+ZHwDF4jGgrG1+Xe3rSKTkIhcsWYICSWsVAJLG0z6E +HCZju/QcKGIKP5lv11mz2pWSCXIyQoyi/+MuG7GIb+Rpqn+R2bG/Cw0CgYEA7b+O +5qxb/nnrgHWZ5zZQ9j0YmrQ5e0Y9NLsyC9rKx+ifUrylJ99KCIi/VUdGlO7ruWbB +uIenMQTFnl3SSYavWFLigVyqiJhyP2MCOMpehWezOxaAJNlI/ES6pi5BKFDtRnuz +5yLxl+u8K7IPn+N+2KbOgnIHOG3XWAfk0m0PCwMCgYAR5AbGI+j2DgsAVXN3XG1g +T+U7VRG3vUUltaSKEFA3er6X2mdtq6ZnHwOf+1Pke8o47qt16IbIJvi/YCg0bwPf +j59rddgYmCL+LCTpxzq23R+CNAxDhaCRNtSTb9znsRBtgX80zXNDupMM+1jJtJkA +3Lo8o/XCGZHYQsMRCNWp4QKBgQCCXHnfs/yRUYq3GkS0Sc0k56U5Fr85/+6Jnmc3 +ptVvvfa2aCfq7/1pJoo7GWTzPvpT46svInqjXyl4x5sJo+c7d98o7YW4dlaWuRQ7 +zYL8hTr6mEDVc6sYYBs0CcLCd3VTI3smrrtoAONh1jXJBvXDl9s+HYYmA9yDMXjf +hNstvwKBgQCNva7FlgRpnCQlo6QWuoyDuZP+ANmLPIpLNM7KX5eEAwMHlXM8rd1K +UPSoMPeYJQkQDfGpBz1t/J2EYmxXWiaT+j/9jPEeiA3zkdV+58dTI7ciDm+QavNi +DnCLak+O3+0q746CUIXsB5F5b5leliHRa12KkJgyluE/6yCrssbgoQ== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c5.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c5.crt index b4a07a31d651..ad8d8d1ffa9c 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c5.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c5.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUAOAVgCzC25AbALSA7HijoxKx2BgwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0 -MTMyMjE0WjAQMQ4wDAYDVQQDEwVuNi5jNTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAgsoBkvyRXFYOH/3/8AB4blT80XSJAFKu7L/zH4IDbLNXqah7xXxaii7s -AfeSyNOmDw3JZR6O8LZZA/52q+vf63VeEC5R2lh9xFWDfNLiYVyDYoau+dVy1el3 -tev8vTcUgSlpCFSzD7Eqt3353dXFAzEXI1YAeEHj+tEipY2N+HkCAwEAAaN/MH0w -HQYDVR0OBBYEFH7CQtBFpWcATKjoiCf5b/jy0mlZMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU2LmNsdXN0 -ZXI1LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -DMqOMi8l1GzyZK67fA+Q0H05+dBGByALXCagAbso4BayA1C2QH8/cTShEHYSyEUC -WSm7icucMdlyuxDZ7J+Hz84p+el4IejcsCUKNPB/pkWsSt6xNhOxeeuaoX9DYkt1 -KQL2nhHRP/FgrtH/nippMOaz80GXnaIXowcW3qbL3RCa28pY58N8NkrX6AtQfi7h -UofAwoC/S/N5qVuaImZiqXT62HpL/reKbVaZ/l0uiHdHrlnIAcgtgGM69c9hEOaz -/W37Ql43n4F3NqtYErCFZKtjuDIdptt8YUGhwhG++ZGMhQZBGKsc8g1OEG4u+oYR -OVGs/kC5nfp0Nhfiuz2q1w== +MIIDUTCCAjmgAwIBAgIVANJiQf0TsVbGEGFepXJB+JECC6PIMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxOFoXDTQyMTEy +MTIxMjMxOFowEDEOMAwGA1UEAxMFbjYuYzUwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCgaL8ljNM/40ZyW6VppL87aeZkDimhzBNO3xT6ZCJ1ogawyKw1 +j/UPoLtF5U0JImFn+bmookEOnmXPDmUGsUiViEvObNgX+Lw02HryfbJejhAycH+d +XYVm8QzS5AAPgUFn3Em2xgkvn2LHxcWCE1W0KP/q1+4C8BaIFjw8d+D+1RYmrSxs +ptXYe+eo6ZJ4oS8sEvGw7uBq2Z8qRQcaRtLmx1GiMkN1AU8TFXfmXO9DZmFXrl+H +VtbSYWYYQ40KLF8bjXfvpO59kjTcSgYf67Y1ZBgIv4yIflJ1a/qSbdRdNXEMqEEN +4uLT8CPuc8ZNmQgOo9SmfpCRlg3S+EpxyjPzAgMBAAGjgZ4wgZswHQYDVR0OBBYE +FJW5RGrISKuTHImvLdge7aUjxAOPMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWCHG5vZGU2LmNsdXN0ZXI1LmVsYXN0aWNzZWFyY2ig +JQYDVQQDoB4MHG5vZGU2LmNsdXN0ZXI1LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEAdlCxTJpm6SJNerOZMcsezkKeSTWXJzvmROlq +LQoxXEd/FSdlVb1+JOaZxrKHRhEC6N6pJ5yXXumCZ71qlUXuN/FTDIlrsArVswfw +up0aWvpya8JVP04ZVTi+czhDWqbqcdxwUpJA6a7ZDa27/k26k0dqUa8EX0Q8iCi9 +NT9igjqpYIJ8O/y+2RwCGOFVqg59YhkkN72n5sJMLYc9YqqBz9gynrur9ftMyW7H +ocFKUGPikSkA/HHOVXPij+EZx1NQWmhPQqWQs0qOCls/5j53Mla+ofkntL/wPFzh +Zd/rsOF/tqAIXD4ig6ciQtz3R1xqFCFmcZ+vkEgtOHsmu1na4g== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c5.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c5.key new file mode 100644 index 000000000000..3976640a233e --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c5.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAoGi/JYzTP+NGclulaaS/O2nmZA4pocwTTt8U+mQidaIGsMis +NY/1D6C7ReVNCSJhZ/m5qKJBDp5lzw5lBrFIlYhLzmzYF/i8NNh68n2yXo4QMnB/ +nV2FZvEM0uQAD4FBZ9xJtsYJL59ix8XFghNVtCj/6tfuAvAWiBY8PHfg/tUWJq0s +bKbV2HvnqOmSeKEvLBLxsO7gatmfKkUHGkbS5sdRojJDdQFPExV35lzvQ2ZhV65f +h1bW0mFmGEONCixfG41376TufZI03EoGH+u2NWQYCL+MiH5SdWv6km3UXTVxDKhB +DeLi0/Aj7nPGTZkIDqPUpn6QkZYN0vhKccoz8wIDAQABAoIBAEr8zKbUL4lFSRB6 +0VgSoSc4OXD6wfGWQkxzsw3es9FD/la0aPTdKFW28PUa3alwfhKqRmWT/E/GMpfa +BdZcGC93OOD2Nwr1x534uXkU08aqKN47gxzgtFVMSI9p8LFO3nGcJIWiDHdLzlTP +mqZVPZJsPD7f1K9g51eU9XHbJL5bxvpa1I6dunBaGeWKAAPI0NBbiyvHPCeHavd8 +hrKyToG/Kj2qnTqtOWAc6lBAISVAIY8aS3D1fZ05hQM8WrrqgoJ4U0sqwSDIMdZE +WR7ck7aNh2Bgby46S/wLQ14mQvfI11GL3YmdXqREf2YwaZFHD/uz0oY9SH56Ytwf +lZJr0nkCgYEA2dJzeSKWvBLB2QPqbfyXgVBPTMQ0LK+sHjINT3Pw0uSmnL0PTgw0 +5TxpkWGHWZvM4wHuatBVOzcfNgK8Oth7dXTDFXEGZsBiUFgZYDrfOY0+4WSq4LWH +Ff+X8SPPlK9aqGmhHbVOFOibCCilXMZu/0N3j1swPJr4SvHuihGQGBcCgYEAvIY2 +SJSjEoKqhYCB25S9QmRDbT8hw7FjYOilfutsLMdhTmcjlhDmHaNhZ2pvLfrQU5nT +nwXcj7F+FLy8UqjnH04TxtQIcFzw5o9I/nDHYPbY//Gmdd9v6RLkaTZiz+sfKupx +vffWWrYFRUMr7NxDjRYcQCEzwAP+vLMkyzJd0IUCgYBDHifasZ/Ev/rfReeckHKW +tApbWzHAezkL7NxRGSahtTbpoQ/wOq6p3B2jYGv2Xp2ZbWTroNpCbs+9MsaRU1dn +b90U5EjWrH0eyKa5Eirqbu1rQjutFgJT8vf8o844wHes4S92GgG/S5VJxEx/890L +5twWXkDUy+OqJDYecR73bQKBgHpzkn7CTMmRJcFhHvfJPMUK6uDiyMObTLl9Y0F1 +GOZc+s86HtsyTsWhgXrynxR2fSpQGCyKWiDbYNGHZasrGjQzOpVC09hoBfE70GW0 +S0yoCXr1MvINF1uEcegE81mplq0Baq/BcvGomPPMFZPb3eSeH+tnpX1rGuLmWVfC +dz0hAoGAZKpoJ2OC7KvHJRerXhPylPlFH/Gou9bMPkdX1zqhose9VApFP6kfMD8p +1Ieu+OPz9L0bVxS2TeXYj4VADIwAETrJARw1Hu2a8pjFwXueqS4QS3yg4/DeN0sn +05zFmmewXv6VwrCNRJUjxahbH61r0QzQqyY9BHKymlSbPfRP/c4= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c6.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c6.crt index 0b4eb6773362..7212bb7bf638 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c6.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c6.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAKtpr++icq5uRPMlFd/HaLlFjtAWMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjYuYzYwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBALAFA8o7mX4GYsexgC1JaQzogoJzVK0XGeeWUMKIU3kjhfjezxwY8Pqk -1bCohm+WTdhRUxtO4Ku2AMz7FaOYnhf1gAALT87M8iEQD703Fd83jcmrAuWG0zib -kzos0xxzOrg+yMU7Tj3YGBqDKuOiXR9vNR5vpscU81bg4ZTT561DAgMBAAGjfzB9 -MB0GA1UdDgQWBBSQZ4R1AxlxN/+tAONyY27r6AKU7zAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNi5jbHVz -dGVyNi5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AIcqrPNc1Yr5f9aeW24lw5pmyH7sUlfCW8O5xyL4Kg1Nr8bWKFQBoWjeCL/5kMD3 -/yrQrYZRCMMNd2Befr+otqe1ykuq1l2fpn5BNLTDlOui07MLuvHDtpV3qUOjPL9p -ocemCoHz8e3LITaP2/s1eYf37p9zJpyI5Lw2UGHtvbWZ8uOS2yy50Vlf28WHAETK -/BoE50TjQ0e6/eMxammm9TkC3C7z7m7NFJ/421uyy9Aagc2LibEihD7+H31mDt4r -X3dDcaCDM8K5n2WiHlJ3ioB8cNdnZjrAQUDJg//iYYLUzN9tH0i0HmPj4Jah5JYS -bLzwggw9Hfc7gftcBi5UAFs= +MIIDUTCCAjmgAwIBAgIVAKf9iYYay+CmjihS6ubgY0qjssKyMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxOFoXDTQyMTEy +MTIxMjMxOFowEDEOMAwGA1UEAxMFbjYuYzYwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCljTBXLxWO1EyJXNuUXJb4Fv5JUaq6XqlOsIaIj/A0Dx3Z8jtJ ++yMQ41cAZ1bzOBrkpV389WYKLpq+PJnePNyU0liDM3QxaEUWtxKEnVtTZrVqfc2t +5Nsrb/v8YKIs1cAkivWC30i+37eP4P1O3G4i5Umm4bgjnK3gpo8NIgnSuuGNmxZw +wnmaHdiWDuxokpakQypE4Dj5X9U5P7Tks0bpxyBZ8lC2XuHiyXxhHBKEZMppCtv1 +S3YWQaSWi2hV2LTeg+E2LmIGuZEXuNUPZ9Kpx86BBGjy/ToDTNJOA/utqVPpL9P0 +ZXYMfb6rqkxcQrvYAjzIvA2BjwKiekC1Fv8XAgMBAAGjgZ4wgZswHQYDVR0OBBYE +FAWrmjKn6jqSFXt+uYidVlAbkQIWMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWCHG5vZGU2LmNsdXN0ZXI2LmVsYXN0aWNzZWFyY2ig +JQYDVQQDoB4MHG5vZGU2LmNsdXN0ZXI2LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEAC6nrmnVC7VYAbdPOfdi0bBTPtVWt5AaVy+7X +AZDiKbOQX8txOHcP7iiLHdV9rLD47mYS5022nstvIX9Nyzoonm9wY0fCP6PA3ZtX +koIKoFSJUSrM9c9I94i48vo0tZBn5MiJvdZAiviJn1vf23gzsI2MAmcTENWE0jp0 +Ii6Shmy23V/yd8MxNozdysQiBSSW/LGIybQl1JNxmhZhODVJ7JxSGMueI2+xJ9ct +A84FcdGLMYkWyVNrOMtd+EsAsdKmUjY4EQ63Vu8u9fecVDfN4/cuCfCJKOMi0qiM +cATs6V51nSdBvpXQWjKoxhQfcxq6I77/r67h9EKbv/J2xrxmPQ== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c6.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c6.key new file mode 100644 index 000000000000..3441aac51709 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c6.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEApY0wVy8VjtRMiVzblFyW+Bb+SVGqul6pTrCGiI/wNA8d2fI7 +SfsjEONXAGdW8zga5KVd/PVmCi6avjyZ3jzclNJYgzN0MWhFFrcShJ1bU2a1an3N +reTbK2/7/GCiLNXAJIr1gt9Ivt+3j+D9TtxuIuVJpuG4I5yt4KaPDSIJ0rrhjZsW +cMJ5mh3Ylg7saJKWpEMqROA4+V/VOT+05LNG6ccgWfJQtl7h4sl8YRwShGTKaQrb +9Ut2FkGklotoVdi03oPhNi5iBrmRF7jVD2fSqcfOgQRo8v06A0zSTgP7ralT6S/T +9GV2DH2+q6pMXEK72AI8yLwNgY8ConpAtRb/FwIDAQABAoIBAASY0g6c5eO/oDLX +dQvYMlaU1N5jIFmCejMPX2Ne+jpUo4b2XSnlvLu7hFWKvEH1FquvFKTXvGVsYkTS +cl29O00xAt78+8bJCjoVchpkM2TMElEkFlgz/z39vKjtlszZHQSNJnOwdyWnTEKR +6pa/ZwBP25nnSwsTtUq3dUg8RlRanpxbEWpKUBpYeNgOd2ZPSDoOBVhP+02TdOU0 +LLITFtlHL1/Ve1P6qj061mqg5P6zf1yct53EwwGpDq1ivw0nsc/n+ptVpAZAELgl +ZZyzZ32COWT15tfVcqj9ngp94dPRxya8BzGGODQSpxkKxMwp6QTBpE4gQaEv8RJK +GoZQ0VkCgYEA5M0idLTHWJhKNoRIyVUpicgy8+DRI92E1Uf12G351yzwnj9mlk8m +6Q7NmbvXgHLI/d/x04rk648kqy3Lxlqy72rKb9SvafxA5Av9Db+7yuBUPcE9M5HQ +9Fr2qH0rEi3MWdzsllq1YoWQ2sC6aB4/PosuxW0cuHwy3r+8Hr8Km0UCgYEAuTs9 +uKRbk5e0hGzcBKMcuqFIXrCDDXZJcpWAoD9/YG/T6ZTXMQL5ibWzLZZ7O8eLW/ry +5++RABKRtwhiDPbLUm+smPNlrm1TNFhiilQ6QP9euyaR3Ta0WQJNIKgXL6ddKer0 +KdQCfmYVfkC2EZPA+xw6fOB7KCO4uVzEfTpsqKsCgYAkOlrvUcSSwaJWmYGQ53wp +RdrThNB4tlWXE8rYlylNKd97nxxWRngpxN9HLVgFDafHkxMOZNq0dbjfpeOzJ59X +i3g8Vtfjlz6LHV7XM3m/uN9928AeD0OfvVt6+D6+pSPLQtDvSbOYwv9Zs9SQMeHf +vFgOiKVYjMndPUzDLYq3ZQKBgDpplyYyFnrtvUyBc8S2l2m3ULYjncBSDAlpYf2m +6IlF2qLvgAkBsiWeOhnnOYd5ftYLxiwkHSrTsEXzC9Cgxy+A7gHxx6AA89svACw1 +MYWrwnjsSdVj2e2zzxS8TUuG0NdWM5c5N2wazy5YQ9I5MQM/PKaCt/PJiAhUW9ZN +5VQvAoGABE1JVEb8WGJ0+6SDKCiTLYfokIkCdC7Zco+qHmmf2eQW/JLXuGHWGm2d +LBFSpOTEaih4rFjuoaFUVNexgX1TWVtAyuEpDuIqPjN6ZYGIM7k0D6JUpW3mjVpS +l53w0ayMJ8NWcVDkhWK/tTVT8YMx7l61KgBkOG2vtkzPIzZzuIk= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c7.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c7.crt index 1cd5275e2681..fd18aebd605e 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c7.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c7.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAJkbTDOQxMn77czLVIYYso4B2oguMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNVoXDTQ1MDkw -NDEzMjIxNVowEDEOMAwGA1UEAxMFbjYuYzcwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAJy8A2khQkFtzq1bi9E8sSKSrJ/v/sWAVASrv8qwuiyRh0F0sxp6KgtJ -4ogpiI5WbhbIoqWqjoDwGgGvcvf24m+/sr4JDdKb/ZyzkKGOLhLXugQwZABOCPf5 -4DJIAs/JqoxQ/XhbMPhqLTz6x7qPLj3Tdwybsbj85nZyedkx3gyBAgMBAAGjfzB9 -MB0GA1UdDgQWBBRq79jvLNS4zDUaRG5JNpPgOqXlWTAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNi5jbHVz -dGVyNy5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AAU1INEj5h7Z4j7GFgwGkmaIOJhLrpFcurqAnrxovGRm0MuSvFSsdORSvRW31Ck1 -f4U/ee5emLhkv5rieUIPoWmJH0B3f5YCNb/TTXJXhD3XeLhz0vsd+UtduT+EGa4M -V9t2l10rTAvYa1RY2FOsof1UtNblkf6iKynBYO9haz2EoYCE/RGsJMFIComKoXhu -s6VVjehIlT5kDiTOBu/Dnz8Ol5k/Cx+KAvZJXaPEpLsXrzgVtvH2OT5j6IG7B+nF -LHzvHuKm6EkgisXqP2taQx8SuWL1twjQbxl2Vjl6UEI2QtY08GE56a5JCqqFxN2k -hTzjW7Iyyv82mMfiuLb297k= +MIIDUTCCAjmgAwIBAgIVAO9ZEHuT1ILucRh9xuUHD1io6S5+MA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxOFoXDTQyMTEy +MTIxMjMxOFowEDEOMAwGA1UEAxMFbjYuYzcwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCwY2yEute8sYbBquatA01i6B9hwLCZBtvsC8qXVhC2KUCHa1e3 +uOpyCsFacsSjlpUfkoPVITYM/rZY7RFFD5lmWvLErby8t9NFGyKAtWlTU+l0gXEN +l8nuOgN0XVycfJGhR4uiSo1Ar5EjZydNEsToDRY6Dhy5XB1ScR2cJ0oPPNIq5fOO +Q50QQpnSCbM26rwg4s5IfmH11gqmR0Ph+6fvPuAGHIxuZSv4wwkcmDAVOjIjeVGr +ddIJipER5vgnSvEEs0vCIhOoJFrIQChQxYo/KQYn6c0CuzyOKZGBJ8q414N1COso +wUjWrCg8SmEcg+7O8UJw5nnJL0mHit2C6ENHAgMBAAGjgZ4wgZswHQYDVR0OBBYE +FAR5b2xJjYmwIX8k+UJXQLLrHv7GMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWCHG5vZGU2LmNsdXN0ZXI3LmVsYXN0aWNzZWFyY2ig +JQYDVQQDoB4MHG5vZGU2LmNsdXN0ZXI3LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEAftJgZ5C1imUUCITbF8k02/T1AOi0juJ9IkEM +CiWHSf/Lgzqm879lxfbjFUMMmSTrFdEGPVmSB3ODUCJ6rq9+RieJbfifkJagOQmu +n/1NrqP+7awLznJ4sNMxjYbnN7Fl8xuIfIAXg0jhUyiEeOZR08wTCmnYFyhEfaid +f3S8HjHquHMoyTmnQnEO4fzksHcZrt/4lyPc8ZJyWj000V+zz3nxhR2lSSUGvKf3 +tFp6c75kQ3aNZAvNlUjxxDhdOQ6v+mSniucDLBUAX6hyLzqgrsU5/th4SnBsNQen +98NfLWQ57Ngy5Z3KnAc7NmyU/VJIZGtOcCSZJLqurDwRH+xOrg== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c7.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c7.key new file mode 100644 index 000000000000..e64cfc4d74e8 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c7.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAsGNshLrXvLGGwarmrQNNYugfYcCwmQbb7AvKl1YQtilAh2tX +t7jqcgrBWnLEo5aVH5KD1SE2DP62WO0RRQ+ZZlryxK28vLfTRRsigLVpU1PpdIFx +DZfJ7joDdF1cnHyRoUeLokqNQK+RI2cnTRLE6A0WOg4cuVwdUnEdnCdKDzzSKuXz +jkOdEEKZ0gmzNuq8IOLOSH5h9dYKpkdD4fun7z7gBhyMbmUr+MMJHJgwFToyI3lR +q3XSCYqREeb4J0rxBLNLwiITqCRayEAoUMWKPykGJ+nNArs8jimRgSfKuNeDdQjr +KMFI1qwoPEphHIPuzvFCcOZ5yS9Jh4rdguhDRwIDAQABAoIBAArFD0+tXtXJGxrB +FhOTQ4rZ9GySCNZXGl93utdxrFSLAUMjdTglBL+Q04xzvHjmjVVfq0f8nqG9faHB +W9AVKz8aJ3+FwiPdr3slNH6aVQN++J6h9qanUW+HwEU/HBP78SJhTW/L0IMWFS0N +s9MbZcVrYyOJUAyrSKkLegzaQ0XZzVD1dCdW8sgKX7pKKNxI8SGVDJHKR+uvYR+X +5wVH9W+1BvueJ/tqwVRvadysO6MwffSJljudKdbDbscczRn4uuWoYuEghJmDdZvr +oZbqmDWFVDkFTjFDbc3iC/u7NTpSc9/CoMlZHbFDxg2lvMYx3U3AcFNxJdqMJ+h7 +0BMFJlUCgYEAxbZELCjQDYsGM8IhNCTrhxJl4A0hMS9HNJWUgk4e8+ljOdHLowW1 +tZBwXXJAgaOiw0wLWa1LeJkylnWsuusJFY5WjHxne7mOaY7+D8JuoenihvB7uWNy +4WxqR9DrE0A87kezudk1QXcXli564HMPwry94btJi/Z+lYDwvhYRD1MCgYEA5GPS +K2dtmmWnYaMn+6KYZzZVYjsNwDZF2hDCtfx21ulDV5m9uoKEOx8eakbB/VzzdAWN +tywbWZp8ieGzTNP8tSXcwsIa1LUJF1xt+5qUQKUE37vj+IYh3r4VBjU7JuInF+4w +NiR3I1aoaiuCQa6QIThAO2MV67MFgZqQfZvI4b0CgYAjno2zovqwpe5NHtlaiphA +MmlWLydetBTQ/W3O+AINkWuF/C0CxsGQ31/PBglnEbL+GlIlKtpg9MCvaK3g0v1E +++rNzo/aM3jbvZ45+p5x3zAnfWXc5XTJ9f1DyPjA9B7GEvbnMaGtlKYUMXWDwFcD +j3tqEpLSe2scxikZJeKW7wKBgQCb6v2/PfPYVQ+pG16fbohG5hPyZMhlcWfMkGat +IPbsjwC3XDDnlFS7m+oCud2Tn5Jgoa323/in+kyNQPVMPOinrnqAx0zT6+eR+Drp +asVNQJsMNtRShRe4XeIyg8KnL/tqXeSyAcEH8hA1sxBQ0OB6nVFUebesWmIm8aM0 +7VZ9rQKBgBxwhvO+TzSeJs/9T2HZ6L+/Rn85WrTIjbcqZecfdDuvzd5NKAVX+l5S +qA+DVcqIz/64LI5c+pIY3CNHo+XlYwF5l6YLdpDAC0A5qXWer9W9kV17lh3O6yjK +9Ki13Rk0lhzF1u3B8MpvE6j8qiLuu6IONb7LIEfj+hS3R5DE7PXu +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c8.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c8.crt index 5bd70c6b909f..ac26166a7082 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c8.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c8.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAMmpIJnFnhv3d1sLi5WuoNu42eN9MA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNVoXDTQ1MDkw -NDEzMjIxNVowEDEOMAwGA1UEAxMFbjYuYzgwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAIMcNI3+aYOuGJx1YVq3VntERfFqmnAsssk9FiJMCzbBG2yD+hPlrlGc -CUpGfTBzGEpvdd7ftSoy+2SJv2tercjCkg1enCS/hhnUp96NmUPm6yq6XOV6ICte -f0hUVcuNnXwJitsPFpVJbNmECNE7EI3au1JOKXBBlfRet7IlXax5AgMBAAGjfzB9 -MB0GA1UdDgQWBBRzmA0meyNb9xdcsfCBrjVMIEyM7TAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNi5jbHVz -dGVyOC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AGgndV+7vrrWgSHSMdpGymWF78PexgQBDcyUv+qM5OawYbreSUfOT9lcLef0TFef -oX4wfin9jP6GmazZpeVIjAnkHN1OjVE/m+4VD+eXnS0pb6u66S7KoEJflliXh0Eq -pY1axZraGEzSaL22ac13jimgrzwHuDP0MDl71S0e7x93/k2X82MrK5jt6mNw5Fz9 -PQHAeSBJwyFTRHDMW0L0RviIoUg/rskOvAMu1EzydVsktms0B2BJPLW/KGWp6u3S -NvRctRml1QO9ZxfgxPJZoWM8NsQplu82xqvA1wOhZ+4fLD1lk3ylEwe5xIYVd5WZ -oN3FzFHLoKVW5so8AyZQG9s= +MIIDUTCCAjmgAwIBAgIVAKXQ5jJ0cJrnz6O+vO0F5rBp4sCbMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxOFoXDTQyMTEy +MTIxMjMxOFowEDEOMAwGA1UEAxMFbjYuYzgwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDAEH3VKJtGbw5xSAkYHr+e4o0/4k7nDDwJIWZ6/j0LI7icgaSu +5kkhJmxQNks4nNpXSSUQ/kw5Zyzg4NMgkOHTnwx/U2erW5k9x0yPavMvMMPgc7/G +NKmbhnIkeD4EJUsdBykeq6GGP25PbbjbCl/Hjl7TOABu0ramDaZftJoOtn3mFWIz +hOXRMhCE+1vory4ctsEbYs/MvXAy9DBt398dlqkG7oEsjD/4c/MrU+aDiilaqGM5 +nwevWwJwjUKmJ6PJN0k6Eb1n0WDh6JkY7A0p3bIFxhpn0him/+NDHaPKxfKkZd+w +/3Xx8EKoew5rAAV15dQT2qVty0N4mHkQZorRAgMBAAGjgZ4wgZswHQYDVR0OBBYE +FBdgvcuOzAHU9MhbtwIG3Dm73IVWMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWCHG5vZGU2LmNsdXN0ZXI4LmVsYXN0aWNzZWFyY2ig +JQYDVQQDoB4MHG5vZGU2LmNsdXN0ZXI4LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEAcfYTZl+5BKZz7GyUuBKUl8Hix2irM8glCicA +6rk3mJB/XCTLj+fS0VG/qjxut8VpgKDrO/4Y9ErY0R6flAC79ZuSzajilQ9JlKad +CXGM6hrx9uwONsq2ZreMhl9UiwO4JmOJcegbUmBbLS80mjrHevQsELFy1tr6SdOD +Wcca4XcBymfdobugCVROq27j14nnC12I/o5yzlcK4AWaEkPDtBftruVuCTvVxH6o +7F7vKK6BJoW8XoBgvUpoVWlei0LGdLd+xEplPVLdEH6NVlfbFMDD0sYRG++Tj3Gi +qvl8/QtcFQRTF3v+1C0VEdoLhY/cvWOw82+XjY/owQ1UzxXi1A== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c8.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c8.key new file mode 100644 index 000000000000..3909fa3a75d9 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n6.c8.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAwBB91SibRm8OcUgJGB6/nuKNP+JO5ww8CSFmev49CyO4nIGk +ruZJISZsUDZLOJzaV0klEP5MOWcs4ODTIJDh058Mf1Nnq1uZPcdMj2rzLzDD4HO/ +xjSpm4ZyJHg+BCVLHQcpHquhhj9uT2242wpfx45e0zgAbtK2pg2mX7SaDrZ95hVi +M4Tl0TIQhPtb6K8uHLbBG2LPzL1wMvQwbd/fHZapBu6BLIw/+HPzK1Pmg4opWqhj +OZ8Hr1sCcI1CpiejyTdJOhG9Z9Fg4eiZGOwNKd2yBcYaZ9IYpv/jQx2jysXypGXf +sP918fBCqHsOawAFdeXUE9qlbctDeJh5EGaK0QIDAQABAoIBAEQmdefpeTNkW70R +gPyYiIjhvZoR/MpaGoFo4xq4VMO7oJmUHPHcj81d+2f44/cKHSWEtEzT1ws1U4TV +8cRn9GGpWX9ignIY1gbITZ/EJNc1WAOwPrefXyFq8tyC+ApzizQZItuZrf5bSpKP +jdzXUQYXvAj5oZmxVhMU29iw1ktqkgq0NZESBEuoB/UB9R7hKo4u8sAHTM5P2+Yo +HVxlCsFwGgRUZBJRp2NuTy+HMc95Lv99y/YJ1H89YI1/1Nebpkbm6veG8CX+tUsz +iWC5wXRfyxPu1q+Lue1v6Owl6+BECSWOfxljna0AfMyhypeMk/rJnq7KXnEUaG4y +X5oi+MsCgYEAwor8Bc/Y+s1mLXbPgWj2TyQS/fkolXU4yV6NKq0vTTe2pgHHaEY9 +WpuAlQMNmJsbXJhWOQYiq6SbOtXSoclAIvJcukGc3/my3X1EhoNG6joDUb1bJCBS +KdtM8N6b3PV/eTzgmGoxOyEwYJ0wjm2tWSWjTaT/okBR5/Dyt5tJvecCgYEA/L0R +SeF6J6/bi6KttbW7mKkshejVXPwZy0585uPRviROd6E02ocp55o3ivx4/sxShkZB +jEBQKNcSD3hVd2Tjbm1BxAhe2e4eNnzmeGDeRlVur05H5IuxkMIcvys5rOqwIs3V +SvoLrHTFTSmFCpWXouRA0EkNJKOPR/X9MwtYqocCgYAYxk5WByQgvGuYupquZkmH +xpZMlaBfeDAlJM9hcfCmKsQt2+gjFV7W3RrA30Sp/Ia9ggDo/ikIJsxF79kFFXX/ +fVHlvllWBJyMrFk/sBQl3AF0NWMX9K3B+tWPjynWN8VmESShBFUErLNWem4mlT0Q +L0TerHaJmRtVKFAgpbv8XQKBgQC06aG2gPAv+HHvQkOWDsB6Rp7YPBJ+vPa50TUX +EXzR3qtMdtielKIqZvnbQ15ZBWkNNL0259J5aXpEGJ8VF+K06QBqkpNS7GA0ILPY +934kWoMgeah8sIIqXMi3Dvn6DQSH5U8InHQ+vld9JHtXWD1zHHBIqAIAeBPYLv1P +zI9rgQKBgBYKCAjxg6OAx0Sx2K6mIlSWmohsWvSZHSjSVLZIjZhJ1kEhxyx9jz3S +4uMj7IbgQTU6tudeIUnafunTqWunQos8yY9tpoxbcJ9Xo7C9Da04nobpNZqO0STE +byCuxgPN442v5wQxezGYATP+kKTkfGNnPyUaoK3IQhfKTGFczNJC +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c1.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c1.crt index 7d49edd4147e..ea4d18fc84a0 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c1.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c1.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUdTJuIxtwEbOb8dF6m54R36J4KrgwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjEzWhcNNDUwOTA0 -MTMyMjEzWjAQMQ4wDAYDVQQDEwVuNy5jMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAtut2Om5X3xl8drxF/Jf/qq+xPYYGs+soYtJM5qAwkC0NGQwPCIDw0pL8 -hpuJXT4WrtGmADZ0ERvPULGTMcBVzmDgt5KANUT8kt3NqzdDS9PXj3VAYKKuDZ6S -dqqW7dEKhRmZ4NPERtHA12dYvBgHmMoiWOI+jvas2PvmJ0/dId8CAwEAAaN/MH0w -HQYDVR0OBBYEFMcRlGJupnRk/6cjwiEb8f08SMxPMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU3LmNsdXN0 -ZXIxLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -JhvxXe5MYkolRzbhyFiCZkh4STPxEobkgB2wPqvxd53/YOzfOdE7dmhyspdqj34r -7j/JqZvWy3A2kjsoIefWV1WGNEjfNsApJAPa69+iCzIpLb5m3hHckDGtrk+JkQN9 -9UE+2GvFsJ1Y+mb8TGZOt7l9vzvZeH6waTV3cUifKFTO3dbEgERA3oGFjPqF+utJ -HEKtOkId9jHnzkEOs1YD3vDndIfuVv1Esav8vXeXzDtx73AREfoVg1VEgF0TnVFZ -lPC9kt5AIrjLYOx9Vl0EabpbmGgB0fV9cR167zOYPlYLp4s5mL8jamkbo8arx+2m -/Ayu7j2j3mCnDM8hdJUoSg== +MIIDUTCCAjmgAwIBAgIVANCoYPN3J/AJvnD0isLsBrvUshfyMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxOVoXDTQyMTEy +MTIxMjMxOVowEDEOMAwGA1UEAxMFbjcuYzEwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCN4MpSJsAGSUT70inndC2BBNn6dJUWzM2h40/w1Fn+cXZde6P6 +ZQ4fmHIGAZSAe2b/sIjpcYroaUWeLeb4iy8qc0QvuNvshaPSXRRFWKDqxq9kDbD4 +cGcNl7TIF9FAfX8zz8P3ZenlXJ2jW5/dCwYKQMH/jd36KxEbCSDFcyrtZrlxOmdv +QPXAiNnuRzEJLbtKldPcJv/Ix/1JSqJh40P0iocJgLj48DDqmpHaoB2/4QkRSVFk +dVJepdFgDf2wG313QQ5WUcXGz485dY6SA3qQuJ2r+oeKXMDKbBWF583sTv/IaA/i +09ZXrbg7Fi6Qj+JLIOjZjve/b4hyVOnlyLk3AgMBAAGjgZ4wgZswHQYDVR0OBBYE +FCFzm4dxywbXTaTFgrp9HVpaQDJOMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWCHG5vZGU3LmNsdXN0ZXIxLmVsYXN0aWNzZWFyY2ig +JQYDVQQDoB4MHG5vZGU3LmNsdXN0ZXIxLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEATXeD5PicG822oBYRQhxAWuq/xtGCsZsBGIg4 +zPpML5uzgXDIN8mkMaatDLd9aSmCjA53nP8SPXBo771aRETap7VWZ6nLuKwBeuRq +GJLhcgFPZzNT/ddFos4BLiSsOMfo5TK+hlkl9rpC0S04nvRIrKqa1J1PNGk76EOz +tuox9MAMELlhwNug3lSY5yS8933N9PRR0EQcVJYR8UWePJgVFlx+UnW6ftH8duQU +zK0YknF4aF6e/Fqz0aaQfLoppRdnWn8RYMgZPygIi7qw32IbblqTv7cj8d/i8i68 +HcIycfEA+njKDkM/4FGyvBizMvQ3UwcVnIgMVdjipMrTkt3JPA== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c1.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c1.key new file mode 100644 index 000000000000..db540b483322 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAjeDKUibABklE+9Ip53QtgQTZ+nSVFszNoeNP8NRZ/nF2XXuj ++mUOH5hyBgGUgHtm/7CI6XGK6GlFni3m+IsvKnNEL7jb7IWj0l0URVig6savZA2w ++HBnDZe0yBfRQH1/M8/D92Xp5Vydo1uf3QsGCkDB/43d+isRGwkgxXMq7Wa5cTpn +b0D1wIjZ7kcxCS27SpXT3Cb/yMf9SUqiYeND9IqHCYC4+PAw6pqR2qAdv+EJEUlR +ZHVSXqXRYA39sBt9d0EOVlHFxs+POXWOkgN6kLidq/qHilzAymwVhefN7E7/yGgP +4tPWV624OxYukI/iSyDo2Y73v2+IclTp5ci5NwIDAQABAoIBABVkWu0nFZX9Dy8v +AY15+XB2JE28rUwaRjkOFpwX0JfqULAIt/FFsmr2J98a9uKmeKG94u7b5VdkpBAV +GLnJ9qseZR4S7+oKPmi2CxvnoJ+1pHVgERpsjEVC5gmzeaQRKBBOV9xiG/Gu1L/w +TTrdciQynm2jXOBfBP3AExZiZosT1AV8u71kSK/asynhH63sb/ck7+20cqn84bJv +glMONb4T/QSDpHayk2Es39cAxc0ATo8uzHQIZjDFLj/o32W9dmwwE29blrwoYKhq +T3/fFVb6V35MIpE9+gmHfvNxrV5nhtAKg10gnEBeH2PiK7lcdWvWGuub6N+oRHDQ +t/ICxj0CgYEAtrirvehVaBR3P9yYI7EslqvDf9eKKMSQjGoChW3gSXzVAV87Fvfo +qnxWy/qTEKQM1V978ABAkzytc3zxWusMLjyJ1KlQKlCguuo5shXJlxyk3TZhBr3R +eQGc4cYdx17PqVokWDCy+a4NL5T82jHpb2x3pUjd4Bgnbg7Zuc1z20UCgYEAxsbj +4+5dvIjJTtH1RYJIGu3XKHBS/tMBSl29yfVbn9m1kktywByycc1gLfkCa+UzgyAr +dUrYz1H3WxmNYAfJcnIuwcr65RveQ2rCxjAEv22b0OtvLLHzCyzB+iWCea5qOxI4 +slRVLy1Ne3L0+rEbgI3RVdzqIImt45568q0zTEsCgYAHUkr/ZwnVC8zjcDFgbL7+ +oo9I1hghHg80sSK8ufRcvXXDgk9Z0kEuY6EOt1GhPxGMkzDiIwBzZeEFWaJKJFTu +nVpgfRBLQcPO+YeqTRkwxuMMQhuNK5nzIU9p9IhI7tGfVY9TQVBKlODOYUfobkuO +PWhl5Q+O5SjfThUHTuYviQKBgQC0tkqmhKVpar4BT3X5nGjrOoFZ/Uwm6TQKJNEP +U2XaHHYrKzaGm1BLj9g502E1MCm1s0BvIP2c9bq4crGJtWqaBl6lxmB0e/KEPgIX +Zb5bPk3jELeA3cLEzQsI3KMS9tMTIwyya2MKQdhoO+mmI++J3uSKeA3OSPzyq7jA ++V9iowKBgQCQUAMRGQ83Xof0Ku6GW1S6UzkWsEtqZ7aBc3sSZ3E5s9MCiPzNjQ9n +9C4JIRPSXvU5wnd7nNwtiiYF6gMWdN+dNjiHHJ5Cjv+qXn92UCJVA1VaW3C8FbrO +UuEhu0m9r7OCMaf00dSJftmQXQhrsfePPtD3qk7lzdw8G0TsPmMpJA== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c2.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c2.crt index 91d98d23d79c..77d4f9773a33 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c2.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c2.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIURYFhQEC5s1rDqkRC5KMkc6adOgkwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0 -MTMyMjE0WjAQMQ4wDAYDVQQDEwVuNy5jMjCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAimtML3GHPYlipJXpvuNCtUF4NL0qORN1iKA8wSeQEJb4a/zhPF5TqBd/ -Ixd0OYvUD2dsqGmQNiHvoUp13siT9++3sjFrdE0x7y92AwkKzl2iDsjN0i6dxIIa -EYpVf3YVDONrcj0kbb+OPBG268pstENfluTbhuTI8RaJ3Sne2ycCAwEAAaN/MH0w -HQYDVR0OBBYEFPaU9fqpl9MA7iS5rdP2JqcNeQI6MB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU3LmNsdXN0 -ZXIyLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -cCkIr6yLbz7Soe0pKkZjK26vBXg/GSn6Pk9m8YRzAlVmMOOTwEjaEB0mPPDo/0K8 -TwTvxdgghla/wVbm6j8X7J3MpUYyYqjUOW0SqBM7THrenNEWMgFqFVo/9Kj51U6b -kM0yRpxaOhL8QFKklldQpmdzLUlme7ZpodmOOYdNUcGOzqhsKptJ4vYc1YgzQgcQ -fBAGpgS4tb0KFUWT3IbZYjj+NI6gz6xncX2KSRiglkvX2+IT/Vdo+8k+1s0VCs9b -U1PL6ufye0ZPcl8uqEqrCLd6pJu1A+5FajnUqAGfRJIbzvslpSqyuFAkCAnFKpA+ -kGj9+HvreJYiuATpOqEV3A== +MIIDUTCCAjmgAwIBAgIVAN/FuIXocylN1Xj26AzVyyo176BJMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxOVoXDTQyMTEy +MTIxMjMxOVowEDEOMAwGA1UEAxMFbjcuYzIwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDaCHi4FmMM7zsQjPw13Eyl0DK6jlswixFRB8buv6T2F3M/mrYW +m1jENeXqzgpksivxUF0IMqzqlgbaWS+jHixicShzE9R03LVdU54RSNX9GR9jv/Qz +HsWpexNVh4XFcQjfoo6D7YInQLu+Gdtqblt/qTuyRv6wlSku7n8H2WcgjIjV8oaG +HCpULX456JDRot//PCEfkcBxZERHBc/tWk0e8p+/Ttx4vbe+C2avFhDYhVh5+3eX +8gJ3rW8CRfa5TsW3ixqWg1MS75mNWZkBJrz9AFezCCyOzIi3s+LlXsnWGfewWz+1 +byH1cYw62Ni4+W8vDhLU9iuZ5lX+DoVqhVUjAgMBAAGjgZ4wgZswHQYDVR0OBBYE +FB7ZM/wiSNsJdPh4Ad/KoiQuutS5MB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWCHG5vZGU3LmNsdXN0ZXIyLmVsYXN0aWNzZWFyY2ig +JQYDVQQDoB4MHG5vZGU3LmNsdXN0ZXIyLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEAEelZZs2sJ3Kj/NqZcaTLUgDylkmp6RFfteqT +eip6LU5oAxSvmSj/DgMljHPaxiR81NDZIpHSBaBHMew7XXGYylqsdF39XezPT6wA +3ZglgPeeDrnOxmr2oipTB6Twa/aRpcWos3wM70CzXVF21QzsDSOm0JGBeBKwEwuv +RdPOkzYAs9btjNt6mYRhfEQ+qg009nZD2ASjJ6ibgbno3WpHjtsZDzcCEqs0a7uC +5tn8qSf9LtaorivZYqljzxbEFp3i9KX8VgpHMWc4/f2MfCwCyaim9+CyfInjJPXo +5SBt9F+5sNSbzEjAognV8HKIG8ye1lHt1gLGuR5oqohGNSyaDA== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c2.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c2.key new file mode 100644 index 000000000000..ad6bc7d43989 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEA2gh4uBZjDO87EIz8NdxMpdAyuo5bMIsRUQfG7r+k9hdzP5q2 +FptYxDXl6s4KZLIr8VBdCDKs6pYG2lkvox4sYnEocxPUdNy1XVOeEUjV/RkfY7/0 +Mx7FqXsTVYeFxXEI36KOg+2CJ0C7vhnbam5bf6k7skb+sJUpLu5/B9lnIIyI1fKG +hhwqVC1+OeiQ0aLf/zwhH5HAcWRERwXP7VpNHvKfv07ceL23vgtmrxYQ2IVYeft3 +l/ICd61vAkX2uU7Ft4saloNTEu+ZjVmZASa8/QBXswgsjsyIt7Pi5V7J1hn3sFs/ +tW8h9XGMOtjYuPlvLw4S1PYrmeZV/g6FaoVVIwIDAQABAoIBAAQq5eMGHoX4eEV0 +atYjhcPxN/I3I8jiQTlhlizDG59oMSSo/uXT7PCKY5otrqb5466Jp2jJAZaJIDMC +Ds+/e5noHGqF5jY6CWELmgZhJ4hed6o7vyqClV5yqU4MHwNeo6ZIRt2ztDZId2Fs +87PUAMpVM3xqMGN/5MxUwVENMUdYdbTO08vhVTg/NWmBOXNf1Y7Y6vg7X4XYs3ha +ayRMLxzeVc7LuTtnN4nNv09tFb5XVlT0sp6LQLop80w3UDH1c9JFPcUmeblu6PgS +qkVMfV1SqCxOTzv8wfF3cTUrj9slHJSW2GzNmtXPRk+eoReJyPWYVKx+I+zHSJZJ +3hUoDskCgYEA8X7oQzhM9ZwB9Rcfm1k/C40rpj530cNWcvqnVGZUmw1vN8avJ0F3 +6pqbFKwpyIQ3gqwg9mPrUNj+Q6li5V5OhZTp+qhjEu5B0lih8PD93MbMLBsbeIcz +11VilHfmaexfG7fZue76jAnrANDti0fMaatDRiFGf98hQT1iab8Kd3sCgYEA5yDR +H5SdGOPQtOggabrpaZcZXbpDsSn43g9EpxKs9gHElLgpqieWnd227GOq1Sr8lnRR +Z9V0Zj++bB2EwQcfW6MjQIG0VX85G/9vJCL4ADD3w0FsVl9uchwVIMV5JVepN8ny +odLf0z69Bbk49Na/LeZwJwtFe4R9kAcFlQiT1HkCgYBJF9w83EGO5VVCVWlfn7eR +S302qBmkB7eWvX7/8KkS9RIAk8ZX31zoSTs/GozbAAm9J1Qiv0NuCP+MgyxHuC+U +Bup6z5SNGkgZ3nHFd2ue8xzBDMNXy7RJLLLVtSSsUrrGrPLdcBOtZyMV8Ca1zVl1 +cjy+cTvhYCu0j1vGHeiRGQKBgGGh9zTD7mipM2x38dk334l/DnQNTrLz6/DBulpp +BS8WKBg6gz8hYfplwfOxVmSdt/1GPW0DOhxHKQU6ou13OMPY8fc50xeammE9UZGO +Q/ywsriEVmNWBK2nNzWUtUwU76JlaFz7Wxk9E1Prnieu3ytggFe95yf6G6BPlwgR +i4DhAoGAc8RXYihM2L50+IatqZmKyM7xq51CEpzHP5cQA/c8b82tHxAQpOEk3xtj +OJ0nhZ42fENIrsw0VNXzUa+xAenOAeAtAqfGQ9v0MyQ9kMlsOJM1ZA1WOM4tXCDp +SwqUy/xsT64xNfL2XptC8kttpW6HeVEmg/StTEHWUXV7z8jZYLg= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c3.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c3.crt index 3a04cd5cf84e..6983ffbe53a3 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c3.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c3.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAM/neT1zlMH5MrdlYtC52vVP/WAQMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjcuYzMwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAIznxxhfyJLtn7A5vqmMhlRHfI8NXkNTxlaGhuXE1svx+B6PgkQeHZXZ -Ium+ofQO0Vtam72SYLHXiwY+NrW5VNZzmJxd8uUjSHA4c8bO/UozcYzSB4HXI3lE -eqSzLwwT5PtXVy4KBzHdPFJ/IWpyCvzQPGYD/I9o+e8UutoRRohxAgMBAAGjfzB9 -MB0GA1UdDgQWBBTj5a/lgJ5f8S8/AdEvVUD5dfl61zAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNy5jbHVz -dGVyMy5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AKhwKCJguaevAFQ1cMHUrotPajE5Muu2CsIhXUR+j0YQPiax2wUdNmHvzjNTm/9g -uTNsYa/+FFQom9LToLZXU27tGDjfPERPCN2v8rJVGWR6bCWujEMRpmjJOT73dth6 -qTdU5DY9Rn6v/QSakmBaAm2yF3UTNtd3NXM9PWZlDPq/dvF4Cpv77QMIIpwYCTO4 -+GmYJ+r6RHqocGwPJf8ZXFoMuJpSxzbilkYVzRNWHRUR/c2Z5IsuflMXKdj1iKT0 -81FfQAissG0ied7anNO7QePQKcz9DR2SdtItF2ujMXp1dRezFNMbOQUW0v/iD6C6 -G5sL1K02vl70+3CpIk4Pfa0= +MIIDUTCCAjmgAwIBAgIVAJF9AIeo2QpwpRD6hU7pzjm07Hv3MA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxOVoXDTQyMTEy +MTIxMjMxOVowEDEOMAwGA1UEAxMFbjcuYzMwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCZtqPId1uAsdvYvnGW9kJgUGecqhC1nBWl+F4IsiPpdCCDtVYZ +Q5/uWsrrXQ60E/Ec93nOymEx1trYlW1Motz8+UkWs1/SYhoZPojPLPktGr+JdFi0 +h+9RNuHLXzzit93tAIiACb/H2uULCshVLld506pzKyNiLYoWi4T/sghtnl0UOKby +ldUPNXbTbhYelNbOZTQiQtHRZWGCdTsPjX4HlE6v2qmWcwQ3q2/OKdpdw8a8u70z +L3TP8jsTveSOuNFI+iIBH4COKyJlH5mNKStu1edwNJdIDb3vH4000t7j0Mug3ouq +kGVREl2yGDDqD5ik+7FvGS93MV8XJvTTO0DFAgMBAAGjgZ4wgZswHQYDVR0OBBYE +FKqZEBxXoGlmT2YmFgWWibHuC+5kMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGU3LmNsdXN0ZXIzLmVsYXN0 +aWNzZWFyY2iCHG5vZGU3LmNsdXN0ZXIzLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEAV2IYJ3hj3ZLHkQJ9LRN1CjVh78smseyMw3Sh +jcxEz78+tDtlsSsXfvGfuocXtzXqtxy2YQWrArsmle3vQFm/5Wf8ULv0iojwrFbw +yxSiWsv3Uj78tz2caj+CIBWGdPCN0tITWQvsjnx/FNQnjN8GyWDoxnebIpRKb3Mm +YVCdLzfztOrnyhqUCuraHeHtrSRtUdkw1amOTZnO0AN9xJHkQwI1o11x2DQEIlxd +lwlt4ojpqTpsxVpeJVa93V+D2t9wWNdhVHCH31uomD1z838joluAcDf/xuD2iltu +V3D5T7ARmhiDzqKhyqmD+M+pjd+PFv7j//mou/F6xhJJzNW0cg== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c3.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c3.key new file mode 100644 index 000000000000..0cb82d7836fb --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c3.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAmbajyHdbgLHb2L5xlvZCYFBnnKoQtZwVpfheCLIj6XQgg7VW +GUOf7lrK610OtBPxHPd5zsphMdba2JVtTKLc/PlJFrNf0mIaGT6Izyz5LRq/iXRY +tIfvUTbhy1884rfd7QCIgAm/x9rlCwrIVS5XedOqcysjYi2KFouE/7IIbZ5dFDim +8pXVDzV2024WHpTWzmU0IkLR0WVhgnU7D41+B5ROr9qplnMEN6tvzinaXcPGvLu9 +My90z/I7E73kjrjRSPoiAR+AjisiZR+ZjSkrbtXncDSXSA297x+NNNLe49DLoN6L +qpBlURJdshgw6g+YpPuxbxkvdzFfFyb00ztAxQIDAQABAoIBABr4FvtkqarGSe7Y +1yAn+9RfNxafJghfJtM0ZPc8nh1nMQ86wSz0lvpgdqrd7Gb++rZc20PDcVMnwcTv +Trp79q7x4/d2KFFGwhJ8zM8gsmVOBJ4OykN+eCg1K1+dyZZzsR3Ukp54V2wuxTHv +Rd/gHhvzXbwQLQicOdajywV7zIvC0DDyin2ZOSO8Ije/rxg0lzaE7n0YD/1uOWfU ++ayf6TDhW8uziQn7iclkiAhTxpteXPP/X0PJ2qG880d4rbuUYeG7DFwOdhsuw4P5 +G4OXkC5q5L8lfCaArfLmt3ydp1SCHtsuo2B1dpk249uAjJTH2kqRI6CPEAH+pySb +9vigNXECgYEAxmral/XAai7V6sLJGIsLXvDSuJwiCLP2n2e8wd3p6y3XhPQuz91N +2IyxodXVJ7BwK0aGYZ78ZeV0VC4Se0JSWg2aYWxzXNbzoFtf+lMr3L8ijzpHvSVq +gpcmQJHyF6N9RgRWFaJgkXzddULO1FE2umF0QaXu3Ld1MYxC8mRYkakCgYEAxlKR +Ig2uvXFN53XechijB2Lwq1LdVewf793GsyfJDDQxrhdeT8EUnIkd3MdSSKdcTiSM +fgdM7jg2ergea+LsTicau4kh9K4lsUmkj4avu/rIkcXSaxssAqiFPaUzlBi+C8Qm +zLmWj/cJ4bDxy+Ll3mB8ERJAyTDjGmD0VpO6X70CgYBHwA4qE2kwPJ2IlkWIBYfo +HiflMDiycvmE7Ut5w5i6PcqIvpBLBJkI7N+zSRMalhgJ3ifsF0ToSBBH/J0cS4Bi +JjckccZJ1UUmQMS1Tew3mDqUFpwojuY0MRpZES29VJaRgjHrAtvu4Hto0u7eIjwr +K+CReMKmuAS+FR2Eb7sDeQKBgE+T5+qLJPedFE5w7F5J5njbSYjlSizk/3enPMi8 +iR6dfuXFOLG13whkuJfTXwAKL66nWbxkP4lDAxFA+ev+dSVy6XYgs4BTtIor650v +xSofmgVsJF+X2pQCfNxcgpLFTtS3033IZlLEJLHr50oRdH1Dp+TNtzNDq4z1JHXs +9xbdAoGAataIfRZ02MyocBNDyyoVGu1hpev1YbZmeTgMjo/R4fSZl1xCJuDjSf0P +xCI4GyQ2Fo2tEiRrv603EHk70iijHnoZatojxRSeULVKcn7t1iWz9DmemEUfQG2f +zK284fSPh+bZq5r57T9JFZAFmCgx7f35G6NvZ6ENxhDc4WZFveM= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c4.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c4.crt index ec17b1d016d7..4f3816fc622c 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c4.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c4.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAMPIOHRQfF2kDqKTYZFJL47S67T+MA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjcuYzQwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAIjxhvo7UMgEzsm2jc6fAwUSuKyruhbOGFUVWp3wgdSNxRPGEXYdLskE -BMOADYMcGidTKFxzHl4XK40Qt7GlIB3nsSsttrJ9GChA01ngClYYNQvAaaV0WUxq -eFMIUcRnYENaBS40J/jXE1KOEsRBKe2mphRWT6tdWLWLKdrGwIFbAgMBAAGjfzB9 -MB0GA1UdDgQWBBQyBHPb5d1Ym8c1rB/MH85v12IeBTAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNy5jbHVz -dGVyNC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AAyj9Olk0h2NKZ5ayWC/srj9uZb0lcR67shQpgclJIvAsvjzitcQhjmL8intD32j -UkPFrm/KGEuxitT9nX7gEKGQ0Qh/4c96Co8c4UDxd/RDWQnL3u/LLPTW7H+ZJh0S -JH7pzT7/8omnrAh8OqqHl3961uLznvRNNIiu0ejpNRHwXFoPk9oL65U1syx8YYx/ -s48dbT1stzImO5TLXv+Bpc5gDJ7WzN+9G5E/f/DJ1etIsgS6r87roaq7WtEjFrvS -9I3Ci2+nIL5U4q+6hzdilvMiyjTtw2fQKEVQlp5YdPArJYaIjqzggQOUhtBnCSWP -87bX959dMVWMEJ6ji2zi+3I= +MIIDUTCCAjmgAwIBAgIVAKXXkae5wC2nLtWYyJO1qdq9ItiNMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxOVoXDTQyMTEy +MTIxMjMxOVowEDEOMAwGA1UEAxMFbjcuYzQwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQC4WUXxCl/3HDBtMxme0gPH5P5Ep4uAn2Tq7ZWQsUU3mHAFp6iO +DgZmk/RqJ0H0ATzV+dxQJ/Ce2ljfiqv4cVtaCl5Sg8bSxkgTHAx/G2KIKSqi1uhu +TsLXiu+jNOt4Oti7/UYPaWc7+9whJd8uJfXWETER5jNk51EFF0fyRE5QGyD5Aqp2 +NMNKoDsVMShu3krhvJaG/LSbNdNAODwRYwEeqfEIjeBXc+O81nE5UnXsyAaXNadK +SSnywtyj9TMGJs3/rL2s1Y4feSb+78KXJ5mmsOmtCf/rkrmhrruIwEtZaOn7GvRO +IU+kY4r2NPM4csAgMy22ubQTufOo+d/BpnYdAgMBAAGjgZ4wgZswHQYDVR0OBBYE +FC9G996atrciQ2PRBrlzXuUXbKYcMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGU3LmNsdXN0ZXI0LmVsYXN0 +aWNzZWFyY2iCHG5vZGU3LmNsdXN0ZXI0LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEAkCunGbfmtPWrCNTB02KCyMAdt8/bCePEQBA+ +ddpvjAXrZWOr0GfKIYpkhaQuUGAWsTXYbz6dxd42wJFwevPHeuMI01+EXza7C13J +8yFAcD2iw5nC0d64pmGreo8Y43HQPUIAEoRDKMoFoP5B4egXHv9wm1+pJKFZzneM +wmYRzPZVDFvDz9WbUpP91PspYOZ1D0awefaM8lCEmtLz2pwTMqSwWmhraBn567Vu +AI662JQ8/4apDft0++eBKhX1BtFaRT+XuK7lxDSqGEKIPmgccWt8cXPmAlxt9fV3 +WmPRvM04mN38btI/TAmmWioesV+5AkW1n/k2/fIoGwYxWKAmwQ== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c4.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c4.key new file mode 100644 index 000000000000..2c108b158274 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c4.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAuFlF8Qpf9xwwbTMZntIDx+T+RKeLgJ9k6u2VkLFFN5hwBaeo +jg4GZpP0aidB9AE81fncUCfwntpY34qr+HFbWgpeUoPG0sZIExwMfxtiiCkqotbo +bk7C14rvozTreDrYu/1GD2lnO/vcISXfLiX11hExEeYzZOdRBRdH8kROUBsg+QKq +djTDSqA7FTEobt5K4byWhvy0mzXTQDg8EWMBHqnxCI3gV3PjvNZxOVJ17MgGlzWn +Skkp8sLco/UzBibN/6y9rNWOH3km/u/ClyeZprDprQn/65K5oa67iMBLWWjp+xr0 +TiFPpGOK9jTzOHLAIDMttrm0E7nzqPnfwaZ2HQIDAQABAoIBABhphLWS0M/XJSKQ +2HJhEAHHSqn/dfMmfdAOfkvsQw+bBC/7hSVVU3ZUtCff4v/+Wfa2tnzMN9+8EnYn +1aDPghq+XMHt6W+9X4tdn4SwNQV1o3ZvodVZthD3uRw9Gcw7dBAQ+Nd5oc4eYDGT +jlEnfb1HXDQpQ+JiL3x5Ghye/FqRwuimZnMWINBvi3EaiLLq4efy6bzOmM6LKfx1 +GnqS/Urye72UtWTYz89kI8WWaVYKqomlzUoCQAK8/pn2r8jTd0BZUqqaM9mj4Wus +ixRPDexbnEHRDvaOsUzRa/U1Ct89EBjgy6eNULGtIXiIPGaWhgf+KzhnEZvVKWfh +yaKrIRECgYEAuk+e7CXETRhQJBNCAA6mXmN7HogisrIQFjEoeD6bnPp/EJ8weO1q +td8+kV5tk2DpJt8YthxNUmRdzhs1D+2azOOicwF6rm1vEWjnK9Yh/H3xPj6ghKuX +tuE2VH4bfmODLxfPPZhETfAqFEZc13ydBmWmYmdsGuj81/e94DOhF68CgYEA/U3A +RkYoNxMdCv4junHPtsNpz6GhFqS2u7GFoSTHPN86c2Lw1/h2xd7kg6dOotuDnGBv +pT0ZolA5NBX8iTYbH98FrgUAievItLqhnrZ/Rl/2XBl/rhNo8VfPpzrBHbJytXtR +33I55bD8Ijw96BauD/vzO63JCSILLmwJfi82dfMCgYBXwt+yj0u7oSNHCwdA7aWy +upupUyk9EaGAQqG0mpFEzb6C/umfXP6yMznO0jM9hB3voJGMXyKRGBQT3T8OhLcx +olzIXb8DYSs7gTW7sX+K52x3DJsiQ44QqF+mR0z+KTQHP+jwM8ShDetUZ5yeKOXy +uHk6y0riFjPcK6f2b3uHowKBgQDEAhtMBdPusjqiHABXsOQTv+ksAx1dIT5JxVfr +V0NZRj+7tA88gAagnRblDV0bRpP0OOeLzNj+Mefh9zWAk76vjeueOj6teyfHOjgA +UaXxNia0JAMaF+j4AIQuGpf/O+vppTC4oHJO3AJvF4CsDIjK06LnRlYvXAmc9Xrv +CuewowKBgQCqCguVTvfQGhShqH7NDpCeBCWc1fLrlsj97yejaPvElEyWvvq2wBNS +ZeqqyCD3ethOVXDX/H5DwnFo8440tJ5MFzAdnJvTD7u/94TjYi8VWCrS1hrk7La7 +hKpTUXXzw3P+oXOsc5/tJdxx1ImorGUH8s90jx1HG44XXIMU4MwQKQ== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c5.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c5.crt index f2e2cd8620c8..d5fa9b7b9c48 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c5.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c5.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUIXgmPdXIhG5qFawuVil1bGWGKJgwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0 -MTMyMjE0WjAQMQ4wDAYDVQQDEwVuNy5jNTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAjEj++bXevuo7QkfAhTmoUwf0WcA24+pQWcNrEzq6KtxprsjgvDEoynRJ -JgkuHj5OGKEYE6Twah9oWPzRTBAwjSzfKrGfUlor4M++9K1Xk/HkJjJYc6TMt/pC -GHVwY9GuxmutianrjY0BBab/y/hJXgGYDNa49SGN8z6TcWddsXUCAwEAAaN/MH0w -HQYDVR0OBBYEFJfy5R84iMmkiqjDjTzeqVWDwWtdMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU3LmNsdXN0 -ZXI1LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -kHXlE5bnKp87EkelJJZFeGsoRWXj60OrQ+AutCsY5k0iP6lbRSfwfaR5A82kxrSE -e5RZnFKkX5BgIpzNy9zuQzI5O6Iyt7ipghF4EEwptjZOYO8fNHVRJZmX7fTzDQIf -cGaXaiHTsJi3HP3H5xbnRI/3c+f+BlJsWY0qaH0OqO5HzJG++IakShu6q9cWCDA7 -tixV2vaOEkJ71IzQ0UGSTXp/3TvZK6gKtOWD2oNk2IUeF+UEo9yzEuFox8y3BToc -EzsUbctLmubnVThHfvKdJI40lt8jRAKKmFxe+xxatEqPrGBVt3E7bvhKhvhwxxai -mlPaR93bxokwBI9cbGUdzw== +MIIDUDCCAjigAwIBAgIUeS2chjgBZxYHcSNG0J0torah8NcwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE5WhcNNDIxMTIx +MjEyMzE5WjAQMQ4wDAYDVQQDEwVuNy5jNTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAKmS3aN9YPDs6zwaHjUcKgd8ls/NYg2EnWpJioYmbY5fcEmtBZbS +h/luVMjw9wu1eLCBwvW9HV9ySv0tAUJu2ImxDuXSmQeEidjPxH3SRuZuAG1IIo3o +NxrFTa4ovuAEQJ6/s57/zTsaOqyt+MMua1jLiTQJI3bO7N+lol+/cBDKcL52Co/2 +34s7WrgNwCQq2Fv7IUkhZn9GNSPFLBY60MghgoUjJKp5VjhU3JCqj+5eTG777xOy +VzjwKAJf71PReiXvDDaH3BduGM4T9ToNuZEhxoADZRKfXcRiuZXxv7tEelfhJ4ap +bJOOd8+71AUUYNKH76jzcZckcRqrfVoSNakCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +eulqyzbun/5ZacQTBzDO/Wln644wHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRYIcbm9kZTcuY2x1c3RlcjUuZWxhc3RpY3NlYXJjaKAl +BgNVBAOgHgwcbm9kZTcuY2x1c3RlcjUuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQBNKHByRqQr1WwmVrnXmR6J5PBbVRzywtEO392/ +ucc7aclt4T5ZxKNAqlxi6Igj4LK1oU3V/pcW6nGyjTn27RTVitberG5OhlsqfWW7 +cGCI6OTwkSC1huMV+19Wxx8XBKblOymjTN+976tp6lzGsCGYrAWZFFKCG1AWFSE/ +UltdCqVZcPIHBnNbtZRq5L/1Rm6h/bcMIVVScs3AvwEfVpKVRraVrkCoby6kwkyu +KzrMpJ5h/b2bMJdU98iGJ3lBvnb7zWv8JhhlMBDrWoQhGmAafRMKsEkaXJJqu/Q8 +02a+LkfJh5uunObInUWBcSq3SOlzS4npek4FZUSSQSLEhgS1 -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c5.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c5.key new file mode 100644 index 000000000000..9892779aa4af --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c5.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEoQIBAAKCAQEAqZLdo31g8OzrPBoeNRwqB3yWz81iDYSdakmKhiZtjl9wSa0F +ltKH+W5UyPD3C7V4sIHC9b0dX3JK/S0BQm7YibEO5dKZB4SJ2M/EfdJG5m4AbUgi +jeg3GsVNrii+4ARAnr+znv/NOxo6rK34wy5rWMuJNAkjds7s36WiX79wEMpwvnYK +j/bfiztauA3AJCrYW/shSSFmf0Y1I8UsFjrQyCGChSMkqnlWOFTckKqP7l5Mbvvv +E7JXOPAoAl/vU9F6Je8MNofcF24YzhP1Og25kSHGgANlEp9dxGK5lfG/u0R6V+En +hqlsk453z7vUBRRg0ofvqPNxlyRxGqt9WhI1qQIDAQABAoH+KmSNBFJT6edQ57Wf +16cy2qxLB6hQ2xBMSkYYW3E/8lfpuoGG5C290J3ElHepS0QS6A2Ir+aXcUUheinG +JtWPTNz23H3JxBdeXtWSSeJH11Oox6lA+jkTjZ3FzcRh858ciJNkVFue8yebg4/6 +oND6BTjdFCU0uH8CuOavsUAqxAdvrlht3GJWoLGQPhsVyM5nFQ1tEC/1YFCK1VNm +d1C4R0dmsl79LnbAV/N+B5nHnc3hohDM+115i6LOC9IClcGo7vsmVddN90hsUsjj +HjLkW15KX/T3zcCuDfuySmzGL9gRZj5sJNMlIM5NaS3z+bubl2D5kUCcSCne7DRx +YOECgYEAwSP08wXzoAspGjKrZwEUcrgCHkOmZUgvuI9rpfa6kw17cuLJqTbXhNFq +FfHwMy2yiZjIIB219pcUNmfeCelMmFCiXv3t9LNLqjUumcuE1ekBTAsQkrHwd5bc +ko89aXwTgyxEO/71UoPawjjkdOvFTwHifk5LXEvRCakG9FtPk9kCgYEA4MNgprbC +c3PxfMa1CZOFphYm+o8G6vX+z/zglvSjf1qnV7tCvT3SFiwbbrJbH+JVxRZqEsVS +jP1qiuABq8tz5xK8V/SjWCnI4CwQg9Iw2W8V43v1c1XM9vLfJe44E8y38jJdofq2 +8j/l/Ljp6nqQvISzLZG/Xyg6yxlXEql5HlECgYAz1SaDofFD3jrloPuG5S9Lqj5H +SPh67eiAq1zEaXagyFl894gPNXyZVGoYAZno4jar3qi2lcKjrt/pye440rePmPQ6 +cgNOTF/eCe+7rnazwZhrg/yqob5bGc2QN4jPqlDCPzsArue/gUtQHFyTVELbdTIH +m5N17ZLn+Mi5A1hT4QKBgQCuuPLvZm8u1NjLCKU/NoDUpa/TY1AbYvOct5XXg4F9 +xPkFiDrPZbllJxCEMv4HgSjvtwbqZJ5Rn8YtAwoJhHUrEjN7pF8+b+0J5vdHoiP7 +QxGe74PVDsPwzMiPceoB6kisbF7BRmblVqrwlMayUmklQ3sOV5iO3nUl4SMBDOFs +oQKBgQCcTHiWP7Uia5WpdAc2LViTBeG54iYtKNkB25cQzicABlSdFinc5m1xtgEV +xGQZgQoUcaVE8ZTv1nsO4HV5geneYXb2cilRAPXPaBriHIkWii7MzuGf5mvVPlpd +VS9FqqlZq9YsRaIknV8vlyXINNlKQj50q0JcK3sCZwEqM5pq4A== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c6.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c6.crt index 4df4def31075..54f7ac2af6f4 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c6.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c6.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUS2riIiKq6tZ9TLvmAmtxF0HCSIQwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0 -MTMyMjE0WjAQMQ4wDAYDVQQDEwVuNy5jNjCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAi7y7aEYewkqr2jpU4pwCkxpIN/6uOvfwl/AI2APaimOrbSo5E8hyuWuC -AnZt/ETmjeFvdt1MISlj2BbxfwhHDAB6qAH1o0t8h/n5GjGlrA1EskNAPxGao43K -CsFiki3ulWQMsdGceqYmqdMYkwZs996dTEfOn6uu4rNKF03wA48CAwEAAaN/MH0w -HQYDVR0OBBYEFDxU26jYM4oNL4Ey5G4IPGiFFIWyMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU3LmNsdXN0 -ZXI2LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -n2m5XJSzf4OFXLVy2CeCMsxkxBCg6UYjRm1i8BmJ8XdSJrm8Ke0V2KRhe+0aBoy8 -rgVlQd5xD85GJNrvT0N7bA2tGSkDw/ucuzcqywzfgM3Sfzg6r+/cs5wSeFQTHTkP -2CPWSqRJlD052MeLIVUXs6lG2nZeO4TQA9W0qP5T/Wn7tQXh8CdEy2C7Xs+uhUL/ -8eaR4OTSqV5EJCu3EPsV3YY6pZTlr5AzjWIUN+1GS+xg9dz4BatX9n8oB59PqxvN -5+pgTekpJY6vsnyLbxb0WKDFM6EnHJ3hN8GIoI9Hgbak761X5KVo+DIIw6yq5Mfq -FSJ/tFhB4tQ6sbSodVshgQ== +MIIDUDCCAjigAwIBAgIUD2dmtYQ4bszgnVm0eFN5KgkI1UMwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE5WhcNNDIxMTIx +MjEyMzE5WjAQMQ4wDAYDVQQDEwVuNy5jNjCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAOCHCnhqzTDE/msQvKb25wFGpbRCDNlBrnlRtt5MjJYcQ4/ULPJ8 +PweU2mv0CHK775NZGqewkY2YFhXixES+zEuqYcvzvY4C9wvnL6UIvI00p7f7U/du +VkyQiS3hHLt9dWMh9RLFiqTrt/dqz2Vjmr7/T4DemVgusAb1g7eBO4vmptskQgA7 +o4Hru2XuQnc93FLMT1oxenr5IAENskOV3aRCmhVWdt0+iu1I/VsgiX10rOz0PKet +ZD0H8glnY5JifoMHHHiBDAV0hTB+9+kbOUptr9X+J5QrPQsNowfwOR2SVNI1YL3k +oa/vHFlaWSqoy4VU/0ShYI4beyxbWrR4nd0CAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +7EPqyRJ1Ul9ecuASOt4bym3sZcUwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRYIcbm9kZTcuY2x1c3RlcjYuZWxhc3RpY3NlYXJjaKAl +BgNVBAOgHgwcbm9kZTcuY2x1c3RlcjYuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQA6K8hKkVTCuy1YAYb8nLo4M/OROfCfVD/CjOaR +6NgtVlttfkTNKcS+L3RlvNvAfus2HldZXhTT7bJpKdN9GJGRgroq36Fz3qm13Q8H +rRuRDAt495f11Lf7pc71/MiW/KBwiwo8pdqRnc22jBphU4rgSbw9Pq2VFSo8W7W9 +msrUobaWb4NnyMY7Vi0+WdvxSfhQiH8ECTxCVw6jAxCfs54Zc8M2ELFVHvIvkijc +hz0maJNxjtp9NwZkmWBeAjR6zhGIhWQ5BqkUWgT7uhspQnU8pxsulmga50DC3B63 +PPYlSn1CXlaFgrziTYtBtA18XMcawjzP54f5oROhW3toABwP -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c6.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c6.key new file mode 100644 index 000000000000..06a993ffc9e4 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c6.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA4IcKeGrNMMT+axC8pvbnAUaltEIM2UGueVG23kyMlhxDj9Qs +8nw/B5Taa/QIcrvvk1kap7CRjZgWFeLERL7MS6phy/O9jgL3C+cvpQi8jTSnt/tT +925WTJCJLeEcu311YyH1EsWKpOu392rPZWOavv9PgN6ZWC6wBvWDt4E7i+am2yRC +ADujgeu7Ze5Cdz3cUsxPWjF6evkgAQ2yQ5XdpEKaFVZ23T6K7Uj9WyCJfXSs7PQ8 +p61kPQfyCWdjkmJ+gwcceIEMBXSFMH736Rs5Sm2v1f4nlCs9Cw2jB/A5HZJU0jVg +veShr+8cWVpZKqjLhVT/RKFgjht7LFtatHid3QIDAQABAoIBAC88Sbgeo+9kKjAl +E8M+g0fR8I91K9XTCx0r00B+Mz9rT81k8lXLTMlQCAmZC+PzZMoHf2OS8nQq8E6H +aAocZ8dtX2i0bQcA2a9v4iaiqWQ9lSFQJxCRbmdqKunzh4NsN/N0k+XwmCPtEUVk +e6BVbZtMBBJVAka6f77fDOhgsEHUje2lMpANHoCdkeB0JZHxXjhOITIU+OxHYCYQ +HKu27OoW4B2vCM6NCOBkLdkmQxCPiR3BR7dymDn/EYbBW6biJbfrJyNRcrce70to +H07CbNaa4BniwLc7++fP4guYLJKwRadquhjBnKVrej8ilwi5PFf1acKM64C133Ox +BYPLSzMCgYEA4gf3NMeF3PPNW2YRj4uHDkU9BJ2infYFaV7W95/gUw9Tsb2guhVB +pacLWGnxrYdAmgV2oDsAwkct6kILXJ/C9filu6dd8aoC5KOSK/Sy5bYMGc4v6W0l +aWJgQ18zuvlT7LsMupsiGrNLF4tvpvp7O2lzMegOOCkmkxDhYxAvzWMCgYEA/kwK +AGVhVFUiLnX4RXHiYUDT1+ISFmFjvfWt6OdbsyuOmmw+KaHAPv5+qdQdDuoTsrNO ++qtMAQGEEk9QLGmd9Abebfaa1cuWyqokfs8LmS3zClUt5qAR9UUaeqzhz3XvPy7C +pd3qtZJ/Ph7KbopyyA8ALLTltUXuv56elocSa78CgYBVFaaK6aiMWgItq6hknqNC +TfIAMM6sI5Y56geFw1sQKS7aM4WzRpkloFH3ADqHlehNkkzrWAqbF2xDSIU6YrFj +IixRDVmhxNu98YzumvhU3EiF1kXW2s1AdFXERqwUcBKVhHvwKsGVx3Q28rge5R9K +aBGC8dWYcnUg2zPctJUp/QKBgHf04KbtK+t/OV+XUKVZooNj9EqbHxMln8plEUOT +l6M+QcfAldZHsgtFO/Ta1VZKvz4F7ZS7p1+2GloUxeodbGBHuTdAJCwEHL6/YfXb +r0nz7fkK01s+WRpNHb+Ou/qEVHaGlVVvCJ/cWl+wYKeicaDDyr5MGyvWsFf9+Ctj +ziG7AoGBANgyU3slzoX3WyNWkookz+r28zZOFfnvrLOUbW3Be0wAQ6j75Nu37Ypy +zR5DfdIV/no9OxS51m0iAwuU9y0iTBxhIdPtxNl9h+MtOTslog8IIQMsgkxGL1BI +rMQxE/QnP8fSicIYGSyhWiOd/j85OWjn39nl+ynVhlxXwVFYeCU9 +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c7.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c7.crt index 38ad5dec9beb..d54471078e33 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c7.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c7.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUJTRrnhJztqw6LFCszsDD1c/yPR0wDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE1WhcNNDUwOTA0 -MTMyMjE1WjAQMQ4wDAYDVQQDEwVuNy5jNzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAoN6Ca8GpbkzxmORqC/sl3GucqCU/ha1ToDUuJxwzS6QTlJJ83DFbqyzP -pIxx5YAhYiEfruoIVW4hF6L2KrQbKfAVWsfWHFqBpMycmy64yFCOK91EvHJ9Rs75 -+KloOtSHgB5d4akgKOOBVyLHGeH3Dyl7nrUfi6FtCyfVCAjA8VkCAwEAAaN/MH0w -HQYDVR0OBBYEFL4wJd/SFWld5TliWX3H9JPUcTD4MB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU3LmNsdXN0 -ZXI3LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -gte0h+EhO4I0QoohsX1/q1IsTb/vKAGDaptx0LgXK7WkhOhyY9eJZZWp7JoZvhLw -4eKqbjyYAxsATDxq46iAWy1dz7FL46A8+e42nO5a6DSm0suufkz7sFQFM6gBrjPT -3zpD+DhH7vNm+lYNMzq/TpH2YoQlUrivrGS9PSWafmI2jZDVSBpcVbfJoaKUYPuc -JRj2g2j7rEqWg5upfSCnxocQJXY6v8QI94ZtgbXwVmSZ4+TF1nJ28j1ogAKNjUjs -ZvUWAbqqO0qzhtK1+X+w6wKNpZguyJ1tQRJ8L+hOJLbVMQIT3s5mNhQyf2qOxdFA -pKlth68w2dZS2wGBAuob8g== +MIIDUDCCAjigAwIBAgIUbpWMIAefnXl5wozU/jwyoNcXyP4wDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE5WhcNNDIxMTIx +MjEyMzE5WjAQMQ4wDAYDVQQDEwVuNy5jNzCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL9eD9JtlM24/qZIH2+X3Wx9AMcZQk3FZIBKar3UzJEm2tXViktj +eIR/iwU/WUoXnx3eemyId6g9OOlHPDYLnigTZSfZMOBncgZfwLc1y1lxPnBHve06 +4D85p83ntBtVQQkkSNGwraimYJEU9t+PQKUDr1QQ374Z03h0WRazg89stG32lraP +NJP28EcuhQSZWpWU22W6ud2BE+vjKYdhWlWgMITHLCnD4WubL/7wXJpYleu72tn8 +lujUpvEaOOFk1QzFjbHi7roHqGZRiowB7My5sOcU40g11VldQvVs7/qPxpcuN7YT +lRwm7Qd9V2rO72vaNcSUzEXzFXa1ZGeQCUMCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +DawjJcqikrmkIQt+4lnATdNReWEwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRYIcbm9kZTcuY2x1c3RlcjcuZWxhc3RpY3NlYXJjaKAl +BgNVBAOgHgwcbm9kZTcuY2x1c3RlcjcuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQBTbx7HdSGs63pFzH9erp9H5lfOHrdcfRLjeFgX +Umcfzdf9WUG8FzpM/kHjPjwHK3oY0yzHbWFu3hZKruvTMRU4vHxDzfyPTLsj0HfM +QYu9TEpUFjvp/J/8RWmwqMOyeubGYyNIRzfju0JGBjCJQzhwcGMMcxtMEnOoMkq/ +vjxeVJUdzt8feYB6Wgldf4sdkJSiYkBAZ5Gd9o0waw41fuTm9kLI8DIdC02p+9un +TddPV5rmxOv2SqhMbOcPGhKJQ5Dny5NTTxCHUCXt2U3XUXBvock0TIenj0Y2fkER +nK9gAXJS/mm3FuRmwywxwt7bVoNsLVVoMnyRgxGR/cjwFngC -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c7.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c7.key new file mode 100644 index 000000000000..65aafaed4227 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c7.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAv14P0m2Uzbj+pkgfb5fdbH0AxxlCTcVkgEpqvdTMkSba1dWK +S2N4hH+LBT9ZShefHd56bIh3qD046Uc8NgueKBNlJ9kw4GdyBl/AtzXLWXE+cEe9 +7TrgPzmnzee0G1VBCSRI0bCtqKZgkRT2349ApQOvVBDfvhnTeHRZFrODz2y0bfaW +to80k/bwRy6FBJlalZTbZbq53YET6+Mph2FaVaAwhMcsKcPha5sv/vBcmliV67va +2fyW6NSm8Ro44WTVDMWNseLuugeoZlGKjAHszLmw5xTjSDXVWV1C9Wzv+o/Gly43 +thOVHCbtB31Xas7va9o1xJTMRfMVdrVkZ5AJQwIDAQABAoIBAEClSt0ADaONWeDc +PlpZSACNHcUkYESbaP75pKNqDoz+PjC8SVJ8UMsZtuHX65be8e6KKHsfgqR/wgjz ++oAbXpsVKjfS4/L6nwdOM3DwqDgXzErrwVoxaG2TUX96RVpya1zmN/82GwP86GQ6 +W8dbwi+8Ve8lICA84zYuv6WukNlxvcDniGulHh1BtQnloySzfysCjI9ptzKvYftm +N/h3GqZqABUEqI8+fJemvD/e/UZQTZbbLX/HikgrHnZ77BT8UflFUEdGbOvo+ZoO +hVud1d+iw/LMBLt6Gzc5GI/7TNs56clkY3c1yAJu3+aeTOv9D3n/McLnm1HeSzyy +QAOT54ECgYEA+eZq+EBx8gC0ELd0rl5vhQ0cqxgdrCo6K+6ui7RnvoikBPkU0NKi +e35vvgazM7eITRAe0al1wStMBlmb/xLE9u52kmfb2OQvHWNewxiAlv4mEphJs7fY +yp7Qlr4Bx66Gn+r1s5ZdliiF/fDTgYT+30ROhbtzFzBhdhyVhwZRP0ECgYEAxAni +NntqZtJmIFruf0650me2w0AF/rifwzJ5sP/uqxrDjOiTf8zyzMfLFt3w8byxvEfr +A8RRHHEYeqVFps3MgKJBCp2EpiFDUAnN0WfV6kNEIB9chNN0onVlt+rq1170uuko +SGplPDkgHWfKC261/nk4mLYaVsS5AYBhvofs64MCgYAvta32d1dJvJikGGJOZ/ki +jb5La+8+T1GL0aXBRlAIME4gJO20cIJU0W7D+MN9vCxpigpYPOIeXwXEsJb3u72e +5yf8C5gNcIMOOUp+NM9YBG2AX1Ky3QamqGtqXBw64zcQkOsXvj1fzZzHPpMixyFh +vsi+/2qZosL4qbxiavJYAQKBgAzzwjhrSwpkGE9G8r8CipB2PcMEteCwfg29hgDF +yBUR/Z2PuD8XYWtaP/bQzxyJpQyVi9eB3wIMDtv7k3CD5GETV0zGP39ibzoYi7qs +K5wAub2NlOVJu0kIjKHsKXv1UCIRSZIPRYUHGTIYK2c591hK1gxw66iT8s1AmDSo +cmTVAoGBANudebyYdAU6VxsRSYKFeK/SDcYDWtqbUj9xNxPPu0WRaGm5XcR4MtFd +D01bISxvEOMh+i3SEjAe8T3++oeAfyL4kVvbjeyXkInskZSJUDRHg1Jv+eqhbssx ++wMWPXMBepr+DnXJrHPJHvLGTkZILIxJOxt9tjI3Hu6wYcp6JzpE +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c8.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c8.crt index 63d393928ee9..a8c704de6635 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c8.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c8.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAPbvvAh3R1XWARmT+6y4h/F8+sqHMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNVoXDTQ1MDkw -NDEzMjIxNVowEDEOMAwGA1UEAxMFbjcuYzgwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAJEYBwUOXOWE0g3IFtEJu6bR7NCyKdZKgZqhW9CAUUjsJMppUfetDm1K -RmNqn8cRGmjFkbGtxxJsf+cVR4q39aM4YdCxkMCOmagY8Ay1L6TBg/i65eGcxiDq -tBrjKUJABe/4Fd99ehLugyXm2e/kRBQoYAQnlq9eI2K1lILJfl3hAgMBAAGjfzB9 -MB0GA1UdDgQWBBTUO6itQmiX4Bhq2pTA1j1OYwkVmTAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNy5jbHVz -dGVyOC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AAgAeinlmvG7soZGQ8ai3gyEROpwIMpN3yBpgbVV9TKPAcb8Y6t+QRFHztuCFvWy -N/mijTdB6nuhX4+pHA4pX5V8bL1bjmoaAznR371y6k3b8nRGinZEbAQbahMqefZN -js6O4XFMJ3hloSmhJT/FnoaRNEZtsdJqdsTlZOS3fA3asxN+INgUaZ4UU6NqjPk3 -xQJD9JV1KFIEakQgWUGhm3RJ33Mb2rlMUZYng+p5vMviMKNsiFrGYJbALHWFhou+ -rR+iUylabeSK4UzswgYJxVwcnly8dsiJayn+7j/CDvwHb62hThcN+Ejlufk0v1Pb -xmXE9ccZAVthc5SfF6Fjrsc= +MIIDUDCCAjigAwIBAgIUd0dYYAaYciFT4f6HIPdzY5E1UkUwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE5WhcNNDIxMTIx +MjEyMzE5WjAQMQ4wDAYDVQQDEwVuNy5jODCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAMitRPTUaCrKC89lmDdq6Rtja5r9lPcx54aO3GCKHAI8Hsqqy34Q +Bd7k+YSn3ZIqvn2k6vlc3k7weyq1/C/sgqXPxHLV7tNm7xM8mKli/WhXBRv24Xj6 +thj+bb7Kv08iBKl4dXs9z6dX+35csx/NLE5iRcai1KiINGH1GAebjCQwxYQDguwd +iLmoVq6KMvbzNsjMSAHBk3OyttedufuFqwbYv0N7UrI85m8gSPfgD7XH8rLd0nSH +JhT7vNu1u7O6Qf32F5F7X/H5NN8SC+Xknd+mRUixwHymLzZG2eghEJnSkhFe+ivy +7arJVjqUU4VN6HZWtsW3x6t712ptpQsT+gsCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +iNiPJ1drgAGqO8KPGFlhw/5hoNYwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRYIcbm9kZTcuY2x1c3RlcjguZWxhc3RpY3NlYXJjaKAl +BgNVBAOgHgwcbm9kZTcuY2x1c3RlcjguZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQCjNIgDJJyGlxdDqSTc5YipGyfy/dGz0c0KW1SK +oA/YoK1mLTBNe1Dh32F+kweOMD16EBvzSzgMmiNaFNBxzx5mtBXKvbdU/mbsQOuP +J0SESSq5Rgz6MX1wp/k3y39J+KGYmCRSzrDvojJUSDuKYUetZIKcuujAe/28vN2o +VKrTfSvnXpkYlKkJFEwihnq9MC65BAjLQU9b2TtSpWEaDoFEG/0L2twT1VlKwlZz +LObLrFL8I+zFzkhyz/ljEXZ7PlI1afOjqj6M3isuAanqfwl+FCr9k8gUck9AETHr +xN7Vgd5J7mKEyityAHw4yN5C85QgbRliMFHeLk7uXwvpEalA -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c8.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c8.key new file mode 100644 index 000000000000..d0842904b645 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n7.c8.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEoAIBAAKCAQEAyK1E9NRoKsoLz2WYN2rpG2Nrmv2U9zHnho7cYIocAjweyqrL +fhAF3uT5hKfdkiq+faTq+VzeTvB7KrX8L+yCpc/EctXu02bvEzyYqWL9aFcFG/bh +ePq2GP5tvsq/TyIEqXh1ez3Pp1f7flyzH80sTmJFxqLUqIg0YfUYB5uMJDDFhAOC +7B2IuahWrooy9vM2yMxIAcGTc7K21525+4WrBti/Q3tSsjzmbyBI9+APtcfyst3S +dIcmFPu827W7s7pB/fYXkXtf8fk03xIL5eSd36ZFSLHAfKYvNkbZ6CEQmdKSEV76 +K/LtqslWOpRThU3odla2xbfHq3vXam2lCxP6CwIDAQABAoIBAATeX0vpU1WmbT7s +4hKGH32wnc221URiC5HyKVqWPzO2rbCREDrQvLO5rmwPfoqrSs/VjnXaUPJtVy+k +Fqg6/rhzH/sBQbgTST2YPMPvSOKdKdyR0sqSmpRkp9j3JkkPNDnAWGlTCJSwx8M7 +EIV2/tov/C1jifFEzLCLt7eHiJDgKwmASOKMLNGqVNh4jiY2UMldCMI8PT6uDKoN +nSbrrtpfwf1fWW67qQh5P1yPlsMWLYvVGrT+eWDZYUP95vm30X5ccfat6LmR9POU +/z+VHPW3BV016XdyvGY1NlJyaLt7Sd4cth6ycKYtFrVMlLHEjEpLJPxyJI2n3quO +x5He+mECgYEA6mU1Hyr+MLGaTDdO5itvhnxhtw3hUxUn/VBFWYu988XPhhMQAecu +4DRBmAVkTIiZj6FPO3AS6TaFEBC95o6E/bVY7eLT7DZyp3ntWJP+5iiDmZIzvDLY +3I/DQPe+oJImnVFLGUrl2Pov8CCeg7zMbWPMcR41CRpkuSxQAB1m4YkCgYEA2yxw +nzFVi0HU9l8tWoSYx9trMMnaifZSzNU6PEkBEg1qaaGNUb724IhuA2nMDXPfZFrf +AZZ1JNO3Dxua7+77bnA7N/3jw6zs3ktkANTl88xkMAXS60Y6MAsLZsKKf9OAltGx +8wHOZMWSfoMM/2KMvephK6MkjkwcijWCkxzsffMCf1JnQkWy16pGxfgFebdxjcO5 +9qBkQNL0nRpxHfBdMGqDIHICQZ5pqBEqTeUkAB7n5xSXoUNEI2HZyO3c9OYZNKf9 +GUaId/iTK7u1IbwCS5qeo6VZwNZpCdSAtiQg9ON+fhTQf5ZkvmmJi1DgJtophbTv +YPBjrkGFBeQTsAxaSdECgYBI/7AzFXdsfmyz8ldkwq0wQ8Cb619CfCxv1MUktxwI +Kze6Hvi2tWf8uPQLZv1zrZhGEI4eNUKnFWzvNz6Pk4Vi7zaKG6H01m++SlXwgPhm +Dzl/VTE+85EF12fgvgLrOEKn+QfP7FYV8kgsovF9ThGaX6olopzWWDtZPSFUZlnO +SQKBgCgya4UeVNv6qQNeFO9Zv67MoJE15VQwxn/fOFSbqhGu7JbemLE2m5LCCEgY +c3yOkNYj57GIoaJKAB9F5BQcrMsxaKp691AeoJeeuMzDr6cQonOT3q7spxregi9Z +/N2ejgsLA/5kL/Y1f6W3NvJe1LWVdcYG9XpWA0p8XXD2MBU2 +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c1.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c1.crt index f2d915e5cfb0..f339123eab4e 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c1.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c1.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUE4wv8m8ca7CSrCJ0W242hYULIcswDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjEzWhcNNDUwOTA0 -MTMyMjEzWjAQMQ4wDAYDVQQDEwVuOC5jMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAgXuRBm6AhPl/n0w+6htbZqVgDzHBd9Cnp13KVWFj/aRR2v6ppf2/ZWSP -DAp3zcZALQrPOnwlNcAlTdoFYbTNhdiciWb3+jMNSuwZaiNODykWoVUXeF+dHsWF -mSjeQydkxPK8AfPwS9/8/wTvR2qjFPzIKYk/JPubG07c/BxC+QcCAwEAAaN/MH0w -HQYDVR0OBBYEFMLuWtBmdv4emh//95cK1tzKVxLVMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU4LmNsdXN0 -ZXIxLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -bdgDse0+Su/oHRZcyP4IaDBUz6L8qp3CFoJsztJN8ja5h19AJ+PXI0amhZ4clwa5 -qho2GqLSLPtButFx/9y28kSCpme1qgXK+2ySwUxOOJDbKCAwDQ6DLZ/umcyxFzxB -fEdO4QL7lWMfI7hriyNHc5+ZjqqK2i7dP8zamTJ0zsAa6Y7L/jxvRIUd69O1skHv -BXyeY2bY57flre4JCO3kEvhVtmQgpe6Iggl0hDq4SZ/3GlA9QoLz2ajpCxvQtyzN -xWuoYf5ZT22yVdIG7Nrpe+eLUjhT1xHQXv1bfpyNwfGlr8OOs0jcdKvENC9GqhS9 -BchqHw44qkBP+5rBljxhAw== +MIIDUTCCAjmgAwIBAgIVAO7Ic7nbHteW8IZw0EotVbQjYrU4MA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxOVoXDTQyMTEy +MTIxMjMxOVowEDEOMAwGA1UEAxMFbjguYzEwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCfpWzZKC5fG0Jxo5JpNNnbNriA+SGxLV4lATgxD0Q7cQ7+Bx85 +ceV33mHoi4Bjzx+HJhoMXJxQsPCqD87Q1UY93ZPp4031v4hvrvnrkItx7FpKojH6 +aT7Z7ASADIcNuXjLdNlivCOa+s98wDn5gPUXdva731nIjiTEM4ZYxfK1MAqZDqiv +2TBDUvVOtAArvsCGQb4NTKmoOKwt2FwXprL40ctttJnaKFhqVV4l62aY+dkjL8N1 +xDjXshzVI28y/8DT4QUnLsKm1lnvduIF4NFR3LLfTnQLTwy9vsTDsb9+KszEsXbn +r84aCQpuDdWR05rRXqHTgaKzjp/S3Z+JLdkLAgMBAAGjgZ4wgZswHQYDVR0OBBYE +FB6m8leBeukPyTIpY5ALAw6uLLgCMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGU4LmNsdXN0ZXIxLmVsYXN0 +aWNzZWFyY2iCHG5vZGU4LmNsdXN0ZXIxLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEApK1WrJI64DE4e1T7N4Mshz/xIc2BeT9V0noc +VCqR+s40yqphq8RvrzAMv2J0xLSR2gJlL4FRmhXnoPzulLIpnc0Hcsol0kA5hNw/ +zAAup/okb1OKaTpcEB7AlQNc3q4th8GgULqLHKunq1H3V0J1T4Ix3SHSmya9ZOUc +ZOFotU5mSOAUAu+LO9oDoSwfaCOCrtC6hSN+roTtlyBY3mRKAGhJZor9CRiVBlsF +mvpOys4eHr2c6i0IZhx0noYYw/ib3kyIt8vKnRvYLNYAOcIG7pXpXg4XWGdDncMY +ag4JW01WI8vdT96rb7Ku7JKSwvW5BSgL07PPD7TOv9mm/mCRhg== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c1.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c1.key new file mode 100644 index 000000000000..f091a69eb774 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAn6Vs2SguXxtCcaOSaTTZ2za4gPkhsS1eJQE4MQ9EO3EO/gcf +OXHld95h6IuAY88fhyYaDFycULDwqg/O0NVGPd2T6eNN9b+Ib67565CLcexaSqIx ++mk+2ewEgAyHDbl4y3TZYrwjmvrPfMA5+YD1F3b2u99ZyI4kxDOGWMXytTAKmQ6o +r9kwQ1L1TrQAK77AhkG+DUypqDisLdhcF6ay+NHLbbSZ2ihYalVeJetmmPnZIy/D +dcQ417Ic1SNvMv/A0+EFJy7CptZZ73biBeDRUdyy3050C08Mvb7Ew7G/firMxLF2 +56/OGgkKbg3VkdOa0V6h04Gis46f0t2fiS3ZCwIDAQABAoIBAAwPRwWwegWdMuEQ +ENikXBX/MDjYmP71WmND5abEbHcWYfeL5NlGDVDcxQX4DnQmX/n+8Ct2431aSxPN +qzst9pG90eqdInUp9CMw/e1u4dnH7O+Ds8V4JdNNzO6PWQm5tQee2r0RUUSSexIF +bgZhriII+TfVS3GnAIgBtxMFq6vxcnwnpHMATRmk9l+3kxG4cx0GpBxxKJKBjP1q +8pYGSdIh9yaMoiRoZpiYIFR9C53JH6pVsLnbjmF/MTw5gQjwCs6ywcOukNLHRmdb +JRtEMEkNVterFhCeuwBYwNdZooLZh9sL6saqnqCsYDJHfnu8RC2+kvenyV8eujih +CuwCySECgYEA2UTbnHpX+g4FZvbD+ERhgaR4Q3V0YAu3Oy8vBD9fcKWLqDpfhsN9 +d/QVx/CbB7QG+5SfN3eruR6eZf4pRJQP0YUWJR+kBmeQ3ufTwSXzzfzxN2GdSoVe +k2MySOmMXgpSGkpphlWYOn+aANT6otpUjqh8/X6RgIYjZ6tHeZ+K+1UCgYEAvBru +sKYOaOvn27xcjBO41T920ldQVzYUHhrdMOiS1Tq9axXnNNGWy7nzjzZ73uuZKM5W +4wboBFKKXlePO8zegSByvKvk9fSHIDqCSScmZmnLtAtY47H0j72AFLPAnzQ0zuxD +FpCScSqVmlF0gwLINGPB9V33LpNROde3DxsNQt8CgYEArHDRWJVlpApi0UDDcxVa +ylluNiRPtDc4O1oMl/NwXWiWunNyA/S+bWklrGXTtb512FEyOQTbH3ckwtCBknNt +Rp65X8eIGKk0OyaGU4M6yN+b15e7Dj1iPdp6O5JZnP3WAhXTQRJIWw2Rfl2Le5uL +hxpE8bD17KsKCKrRdL9iwG0CgYA9292C2hkDCBFHgbotlo7CEgzRP0V8aJxVCiCT +ZOhkltyTyrAt/n7IoG4oOpKhzHihormQD0Vdpoc6XzELSWQisrkIsr6o47FMN/GA +fQ9zvnbuzox81JjIbFkidbpyoIh9Q9UMJp8B+C8agI8ARnnwQJ4YQr524ptjmjzp +CsxhoQKBgC+toxkdegZqwuDmywpchfKzo4chhwl9H1casyChBHoGt3dp4JKQvH75 +XrQwZd9PEo9/KEQQkISAT85MkPAv8yLJTuyjK/g4xOWJslyBpkbZNca+/Ofhj94K +hcHT4BVicrlNVno99/kopLEN3CQ3IZp1H60j6CaFkEg6IbDpISt6 +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c2.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c2.crt index 9a6cadda0968..acf4ea01c4c0 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c2.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c2.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUePVFoDIs9eiIrAMKc06YoRk7oIkwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0 -MTMyMjE0WjAQMQ4wDAYDVQQDEwVuOC5jMjCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAtfvCH7wJN5iSoyeWtfeJFIs3BrCtS/cb4LI41nam3g9T0Ag9WPhf1aPP -5QBgDQ1umY0kuZEhojduZHgZVoG7mNVNTgqSJCJKooKR+SrQ4gqaIlgIwSpKwo4u -/za04p1OqRhOJajOMCuC4zEz9yPt1YAfXCkYryBlne5m7Y+mVncCAwEAAaN/MH0w -HQYDVR0OBBYEFLpfZ8SVBLNw/xipDeUe27M9REyEMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU4LmNsdXN0 -ZXIyLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -dc4bAOe9GrWL3GWSbNyvJJTxEMtlAXTrAlP2kBW2TpiGBTXZ5X8+E48NHAn7SsIX -W2DvqglKam+ooe98XoRwX0UPTikPNaC2Ud9Snxs4OqpQAxE7XPUYV94FsyRXFA8M -fEr8REStbQcpFLlUXrIAZjmGlRi6DufLxoGqgmnGS7KBEt3JwEvh7DHkSka8YMx6 -Kd8YrtTPf6ZJuUk/gcmZ5oK/ImZK43asW3b7obgS/wRrzV5OVqx7bvSBd9pfOXbe -TXwxWpciRGOEHoanIqOWSH2xoWnuzCsL1jsku6d6aX7i2HyddeC9Ler9YOJyLlaN -tjym0gGIwk4PRCcZ2RJD6Q== +MIIDUTCCAjmgAwIBAgIVAPY1UvKtFS9Hcdc14T7g6xd7qYD0MA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxOVoXDTQyMTEy +MTIxMjMxOVowEDEOMAwGA1UEAxMFbjguYzIwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCrWZeeupWzTp2TobV5Ahw4YoE0htlSq7bHOuNuuvMx14kROFMW +FqcdBNzC2+cuw4NkLkJwAvTOG52a0hKtj5qf3QCiBO+tWWphX7wmy630/GVGXUZB +NfFr8IhI3ZdAczTuU3z4J5YTMadCFxxM12peUAQ0TCfnU8wzTo1K3B1Vp1fHNTBN +tVUZgUKpIOlzhWj2V7eRkyFoy0pmsyemjrp1DwlYMv2YnGVr45YzCjU6DkOzD3if +Pk7bkZgAD0ZtLaT8UXUQaPjrvXN7Zcl/4roYIF5eImn/PiOWdtQW4+QRMG21/Kr2 +ck8f4KfFrPXFmWcwal+jaf3zpyGaZ8xNUJglAgMBAAGjgZ4wgZswHQYDVR0OBBYE +FNPs2ipWYi7kSy4yEtEWRSaWmn1GMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWCHG5vZGU4LmNsdXN0ZXIyLmVsYXN0aWNzZWFyY2ig +JQYDVQQDoB4MHG5vZGU4LmNsdXN0ZXIyLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEArqadNmn2Bz+wf6RbZARIfaqoufbG9xa2Xb97 +gRIi/d5bpT3KmUB19uNBJP9IZ7iYDxSua3obhrYF3u/AZG4XMZGdeHACuLAbZQD9 +YXagUAKkmtyw2kZFVnmCddVnK4e4zc1zXPeBgPPQofEoAKUVoyb24OHCtvf4XisN +FwybC1LqszEh91VNp20y7vEEyIQWsTFeRK4Ftsn7Z3zWXI2YMJspBH389s83IRtI +Sbau69ESCgcx8qjTjsz3wshE7V/13R81cffxm4XACT0aIJxTWqA9yUXqnWL6FcNI +r3l7ZhXZ0mX9k3UE+h5Nvs9lQP1TJvaw7WNjhxf986aBWtBDcg== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c2.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c2.key new file mode 100644 index 000000000000..85a9d658b6f7 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAq1mXnrqVs06dk6G1eQIcOGKBNIbZUqu2xzrjbrrzMdeJEThT +FhanHQTcwtvnLsODZC5CcAL0zhudmtISrY+an90AogTvrVlqYV+8Jsut9PxlRl1G +QTXxa/CISN2XQHM07lN8+CeWEzGnQhccTNdqXlAENEwn51PMM06NStwdVadXxzUw +TbVVGYFCqSDpc4Vo9le3kZMhaMtKZrMnpo66dQ8JWDL9mJxla+OWMwo1Og5Dsw94 +nz5O25GYAA9GbS2k/FF1EGj4671ze2XJf+K6GCBeXiJp/z4jlnbUFuPkETBttfyq +9nJPH+Cnxaz1xZlnMGpfo2n986chmmfMTVCYJQIDAQABAoIBAB6aHx6NkzvVUAY0 +TL1CV7joOswfQ0Pk//0/8qttfw6JQGUPaqNYw1rG6ghxDq5G47nqGxp3JLKEsxaZ +6xUNPx/K8Bh/ijCq9flTCD7jhHtq8klpUilGUYomCKj+zksH6E2iIoENTNu7s/P1 +PawpkskqQD341e2WBTOqlUhn3GNV9wy08wmvoFIb+7I0WJELVFgFcU1WXljJphOA +y11xTKRlnn3aGbFCmcmu9DGr6p3Hl/fVKxoOSjdLM2pkBxRR3FLeri9BJycxE/Px +m6u7ALkdfu+U5y/iPdIE3NRbsjSM/djEq3bA0YwZ8Lc1LK+UKGXFF9lHu1/y8fF+ +Al5RokECgYEAvnUKVYIs53ZwU+qZkojKHX+Hu5iNQAO2sNpdiXa6IpMkmLEJezeW +WHyit0+OkIC6zS3YowXi14ch9ZwzeF8Ww43DGgiL4F+xSV9sG/QECci0oNPYLfgr +dN3Rz0aaCAjst+n8k7jN6P384ntl5CACGTd5fyOtbQMAqHd/MGUwfIUCgYEA5lE8 +5MbQ4P7NIkU53Pu6d1ISgCvnfw4jv8QcVpV6BRAXt6ZfbDFm65WpIwchvle0/tPt +pDJG+iTdz0IiIn61kbm61LpBMd4B7N4N9iz2uhs1MNCDaasGymfKf+o6Yjuowr1H +uiqeTRGjn9H+znCWfCJ8os/c1bZdk48cL6ItzyECgYBNkRdDWf/1yLvYwWwITOOb +/euG0It+bcTC5DxCFvOZzzmRYlDNOQdnRGN0qtcSLQtnMuvPAT+wrZ18DUC9HmUE +AcFa4e4t1Gs8ybsa61eVsIM4GO8rUa5JK2oes89ZxGC8SnSOqS5bXM3YY49MyGhy +YrB9wkcIZben5PsM97k2jQKBgE6axTyTOjC/5YTfuXrZEu+sPBIwacasLblCFiXa +yBU4Q8/y4ThMuAKJRntk8LhEuMoLzFwwqJawxF77BClfs1oz2DbIBn4ddezAcGVn +PSGRLZsmXuNOcnfb6nmzmA90qwkjjnpUF0jYT065XcxZEpL1NjJe3NC3d8JnokgD +grzhAoGAD8e59av/gr76UjoSWe+GyZDPaAXddcNVnykdmv0v1Y4qVML4/6H9UVQF +n+1a0npSwIet3HoCSa/nHrIf9g2pEMpEfS6UMMIwI+XkyPsNFD3HVgzcvRSd66bP +nQ85u7DuxfL18bJ45WIXaDxe8fmDYm/7Kph2fu7c7WZ/rDheHKc= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c3.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c3.crt index 8b94517e3ae7..70538a32dbfa 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c3.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c3.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVANUiRixRh0fDx5Rw/fcEb1n1Sg0cMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjguYzMwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBALo0zeVKn2186RU4y35d1zlrEBBw/neVSSJME3AOj8VC/QgiQmqYMpWy -UPp83aXzmlD2ZEle+aS8X7opg89GPCrZVcxK8tM4U4HTEqLP2VXUAp/I+/Vtv9fR -1QQDB0XfkQx+qDUdwTzS4lv4cnFzsmVDgVLR0NzjD+3fu3bKhjDZAgMBAAGjfzB9 -MB0GA1UdDgQWBBRaBpO1OJaW85kYzyBLyLGRaE2HezAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlOC5jbHVz -dGVyMy5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AE88FTjwLDZ8bsEI/tQtTfg9G7iSTlQZsKmIF+nRQXUU6b8EMtMDar53ug/yzzZJ -fZshpjfUmp0w+pQVBt/xrrWLtaXqe+rS9rNzsByGbmTvkl4t36nBv0GOxtoqSdvA -Cp7QcxVBvJkOK7z63oyxAhq+A1rnoxeUGoEyjuOjeIoLT+oTPLUSSqSmMwqxSIOx -Q948cGwSiA9QtA8FcgC1TLcHFgTT83A0dBkYTPjJ+7gQwHEGL3kucZVQBBLBpju9 -lFleod93BW+Mdw34Gnc6WAqBEyNYIiOMkkkDXzgDdDMZazgVsPxiOej/7FcS4thD -ibJVseFjLtnYOLPbv0Wwk50= +MIIDUDCCAjigAwIBAgIUW2M0qJ2l4fpYp/3ECOSNaqQB+A4wDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE5WhcNNDIxMTIx +MjEyMzE5WjAQMQ4wDAYDVQQDEwVuOC5jMzCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAKbE6mxo9qIB7bxzHAuqj88uTN/zmUaaPn6FoyLzWi+k71ddfMaS +tWR0h4yYZB1TV75tB+9RopZUPvBILEGbRqwzBizx5fwewxZtBDEqFxL44c+7+4r9 +vOdCdNmpFa7T1JZh5H++VGCuCRdfQHqkkMPTXZIoPeebgcGgd22+XPSk8saW69z8 +Rd5G5oAQp1AglS118jxTmYerO3kK2NzbzolQccsJ8KLWLNBYw2t/ArIVyE+Pm1rd +8WHSTfb+7RVCsfnqp8s0NljAiio1W0gDrscS3adWZQplpvIPEkOWEYLDidkXfAhR +f2G03BIpO8BbETDmDzBKDvKD8MFoV9K2Qm0CAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +pTeuFmtG3bh3rS5eS4kPEXcHFCcwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTguY2x1c3RlcjMuZWxhc3Rp +Y3NlYXJjaIIcbm9kZTguY2x1c3RlcjMuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQCtDUmfijBjwIxU8FLNS1CNwmS2g5sr+bSfPcn2 +sQ16DUvmVSbTwz34bahSd+fIUvYgWkD/N1eJBVDWSyMdR/HzV8IM9OTtI1jxOhB0 +ecUVio5/XYasCNe8fiOgoxukFeeL9gK4ZGpIlZDCpCIQ7k55LK1gzT+fTyh9JoNf +t0mytq794dqZw4FUJy6McSSO4vLOuvaZpCK+MRN+pl771TuDt1bh/41BWB36XqTo +ojS07eUKkXNab6Q8flkgd0v3if8BLkmV366yqU6lDZYEkA5KQpkjYvsztjGZ8LwS +gvoR2KIQnHcSE/h91yFqMDM/XWvBtGBpP5DAuHE2XqEcqjNS -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c3.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c3.key new file mode 100644 index 000000000000..e3e2b611f67c --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c3.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEApsTqbGj2ogHtvHMcC6qPzy5M3/OZRpo+foWjIvNaL6TvV118 +xpK1ZHSHjJhkHVNXvm0H71GillQ+8EgsQZtGrDMGLPHl/B7DFm0EMSoXEvjhz7v7 +iv2850J02akVrtPUlmHkf75UYK4JF19AeqSQw9Ndkig955uBwaB3bb5c9KTyxpbr +3PxF3kbmgBCnUCCVLXXyPFOZh6s7eQrY3NvOiVBxywnwotYs0FjDa38CshXIT4+b +Wt3xYdJN9v7tFUKx+eqnyzQ2WMCKKjVbSAOuxxLdp1ZlCmWm8g8SQ5YRgsOJ2Rd8 +CFF/YbTcEik7wFsRMOYPMEoO8oPwwWhX0rZCbQIDAQABAoIBADTEwCx3dfrDQ3dK +sQgb+w8iZl7ClLBbZATiwkDv4YyL0tPBGBc5Gr5jhPR+z8bbtTrjAwy9twV8OMIN +BDvqJw2bw4t7KzuAfFhfPGFWISD2sXTq1Wgga6J9S74Hm33LN0wyQ3wMPGSvDN9h +W2SzJ79e5Q+3PgdIFGGxJbkK8BzUhbowDBEnfiQDGlfGXIBPugya3AI3yRqqdAfE +7MARckbMNYz6TYRtEMX9Sbnm7IaGfJw5FZ1+o5up5DpbWzgggQqPTLcwyCFqkZrX ++qab0dPvvSH4vz3G6QkVxXFpkZySQ7TAy0PbXswdVjyHvvOQcunSpdk7CetmRGUF +k9eIw10CgYEAxS9P41YMMpmgvIFQVaW+vaxvE3WX34zSmiLJtkwbEijBICMQHcX8 +ugioPvuL14XaAY6Iya1JdB0Brz0XYYS0RuOITaxCFa9tnqLfB6YPbe/3sI20rD14 +FAwUs9KzuugdWIHi81uREbhfshZWHwYTlrNMCV9bPlRrLZ4OxxsjjjMCgYEA2IMe +6JzRl3P9TbkqaMRqSAmmqEvm+sxfV/FSU0d7fO7LPNo1t0ZX4l3XjlXxb1+zM3fp +FO1KNSt23DnwwoTecwyaSMH03T4S0vgdXEEs2aBDJhEm8ogwUEvzZlbsGlu9fAc3 +GffpvUGug2zrDkSKCYrHfmRfIO3knoG0JJKhDN8CgYBiduu3pAJXSivftds9yZRj +9rVIzFHYieOooHKFm59xA/TcbWXg2DsJUsVhHg+IwghI+roHq4agetOahDEmuzOi +qoAn+TrOxVlB/CbPFPdjeI8BFOIBmHhb2mSEH1Usojf1OHbx+umIJb9zmH/M4iXp +/QKdM4bGqzUSBHlwp3arzQKBgAlA6WRFt4uCkJPu0woxQt2bU/0jaVbUv0px7PPU +PUnkptkN9nfD0OPC4QCbWbtJ2s3bVSHo1mAZYHYfH9Qge6VtJ3bumzmNep57rj2Q +J4uRlu0clI6PFYmJBXkdQ/AIdbvETy2T8/B7yyqg0BcHBONNhCM5xPsST9AaeQmL +Hsj/AoGBAIYMVeBFz2q0adylh4JBuHWbZ5IEzhEJqDo5EQdVxUlftgCVqaVKPSrc +YHZYB62Xoz45WTY03g9WugSlfSyS9L9GPTSzIdLo7Wa3fn1AKvPC8/y8vCj+4rMK +MHlF10cclTiyQdBSYel/OvtxFzWCPEGI6wxv3rPzAG9ViysJ2SXn +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c4.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c4.crt index 1cd6fc2f321e..9b07a9754ad3 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c4.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c4.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVANfDmHfIieWZe2vAwvx+WCiN/UuUMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjguYzQwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAIhWp/YCB3cVIrTK0QP6LC5zBZ8avn9isi/QeLXwxw69k+qhHEOReoyb -nPgXyr2wyXLuL6PwyBMfjINDIEZ+N+4TsbYMsE8HiPNYQyZm1mpBI7ux2FbgCbpu -JRfKA/Pztz4arlBXcHG/tJKistvF+SSrw21t2bLtVZHXBFR0c0NzAgMBAAGjfzB9 -MB0GA1UdDgQWBBSapaWx6oupzaeRg6DMkAo0HwOYrjAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlOC5jbHVz -dGVyNC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AGkVwo1x2FNSMDj6bUxzyCN/MvAIKSe4oRXBlv1ZAWZjG+DRmJk+QpPmkxgtooue -6TKRTYARolTc+gJtG1MqyWoJ3QSekLvVg2OAKk4qq+ijIL7zHaKTECPlzjJppddG -+/OoCh8HDLnOcbC9UU36+3M6cJARFTv0mJZob0kaG5mo67eJYnknJzz8Cvq3r0bg -xk4CE0LR9+evM/wMz/M9m+jB7HdgRcLoTU2xh0v6coVnJPYa00JL5YfRJuy6MCJs -LHveTDYh7HMdxXwnHhr5FkdW06e08OJzzNEc10nTC+zCpgaGCTJxFM0mZHRskqMH -QoDJUQE0/8szz0KfXFb2Vqc= +MIIDUDCCAjigAwIBAgIUMP7z94WOPxFCwz4QHTE1TxRbSwUwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE5WhcNNDIxMTIx +MjEyMzE5WjAQMQ4wDAYDVQQDEwVuOC5jNDCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAMGX8ioBo+vf/fyopLs96819Xpr3llStyAufqsiJo8c32cIR2Ulw +uoXksKRI+cADl9nqk0oL70XApIC9FF8fCwwDA9KAVMHr41BTzmcYObEHIg27UMKQ +ZbWrHOCI0S28LZ1LS24KO5PPOZ2zr7s54O4I3CyWUNExHabvusDOcG50GxD1FneC +e4ZRaLCpRFdzdhwN0MHvpXBvdDlyodE3DZ/P2FdUvBLcNlwA1w0zUUL9anByhD/a +XcuDd8H9isnsFfLmhLAoZHhY+ZvBeDmdsVRDaOQAA9//bk8G4LigcHss3XCUy5Ps +p/aU3FBRmEHtlB3bNZYxm+ARrpiA/zCm45UCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +zBU28yVxkwV8wCPE+vC7vzQxlMcwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRYIcbm9kZTguY2x1c3RlcjQuZWxhc3RpY3NlYXJjaKAl +BgNVBAOgHgwcbm9kZTguY2x1c3RlcjQuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQCaMPliOGCffwkT2tjztg0frDi5u7qSqggVc5/a +6nnOUC96s72CtmVIXO9iXA5LX2QJR2FU4U+1HC4hrfTHLCO0T2kjqkb6UT6fnDY8 +hniHUe90BiGzOIBlQFF/rHU31N7R4KCcRg2MLXLDuRtGTSmyYyFxMoRS3TIL1M+P +xawNm5eThjg2HPTQivUJLHEQGrGCjONzCA3wyLo5jzZtpFQAosDyNUb5D+m7b7ko +ssTs0xG542V0Lzw8CkOGVF+fCN41fxH/SvWsuuXRSp8hU+A0sO8y6vyF7ampa1xm +eXeO7a0U2czhiBNQAtqRdbRv2weEgWtCIBmo/xmB9brsX85P -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c4.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c4.key new file mode 100644 index 000000000000..f9ad911aa4bf --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c4.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAwZfyKgGj69/9/Kikuz3rzX1emveWVK3IC5+qyImjxzfZwhHZ +SXC6heSwpEj5wAOX2eqTSgvvRcCkgL0UXx8LDAMD0oBUwevjUFPOZxg5sQciDbtQ +wpBltasc4IjRLbwtnUtLbgo7k885nbOvuzng7gjcLJZQ0TEdpu+6wM5wbnQbEPUW +d4J7hlFosKlEV3N2HA3Qwe+lcG90OXKh0TcNn8/YV1S8Etw2XADXDTNRQv1qcHKE +P9pdy4N3wf2KyewV8uaEsChkeFj5m8F4OZ2xVENo5AAD3/9uTwbguKBweyzdcJTL +k+yn9pTcUFGYQe2UHds1ljGb4BGumID/MKbjlQIDAQABAoIBAC0/rJRw6bV6F010 +xXwWTwUqpnAmKncEX1uIKEzTDvrDbRxVO6pUtOQSvc7IV8evj1CZ6W9HNtfg3RJS +H27UPKrEEaKj6CujTPA7hmTGXsWn2yfGfmvbgEpkAk3g7LN55ld7f9fqgF5d8yOP +GiXys0Yvv/aT87PRfRLFuarTFahbBsqFa25iERaGjj5xukAxA5UuLrO30WIilC3v +Yx+uswTZDKxLViol7OKEkjq7rz6HAF7qrt5cYMKTnOc11wprLCa//s0MUUhTxYdP +esFYsCaQk5jlkY6/c1JxXroQVloppxsphCuDnya2dJt8XVZuM6d2R3U9B3lxb5He +3l7fTUkCgYEA1guPRcDKNeoDEcvOw6rnSh8Wi6hJzIA3pJWjFPV2hmA+HC6uVhwC +ByUHECcJYtMZViyPNU4HnGWFyXclTUHXMmcgXd1x7hoJyCzZDQ4s/ozCkuTcLehy +eaqJStHFpdLY98JXI1Sc+Y9VqFNNokirVxSpAt1bkkryOultUA6EDO8CgYEA54oo +P2t0qZuCgKh9K08BB0jYDZ3mmyuUugFoQAfPDmzs7PhIxJkpSJOjXI2fhRy54dQh +pipshh7SzjuOoolqPv9SJFUM7TdI+n+R6hHQEUiT0YbKnhvmXZkUx4wFmgyzvPSo +Bp8cKaJ7aZG0snFC30BywmO6hFf5TB7gEoJnn7sCgYEAwQ0aArqYZroSXKmqahYp +l1OkvP9i2FxIU1xXA19EsHv7aOW9fZKzTYha73mV4Jku/+cenuAqwydZ3FRmw2B/ +3EjhC2fgCvVXH0E73qeLc2a3U0+xOoKtRQjH3UCH+EDMSeHxP79f+UIK6s9/dMAA +MYFD0n6dybto8DN60TrWZOcCgYBpHb7h8+3V5mTb9iJFXCuyW+7CV9JO1f1CaMsk +bqcg/HCfsKSQpJwqd+Rta1ple0IkdUMFhjMYvLulp0I9JjeW/CEVn4sr0rKODwZ2 +hrm4bwO01wX7u9zc11eotDgC1QtwmH4D645ElKCEsAKiFGtwoXi4KcCOvsXOOiIu +JQfYawKBgQColuiDPPQ0nJQBWP3jMeQ7M8R9UI0Y7XqNRvFkzf61WumpWPnu8xPb +lt9YrTPkoSBqrGdzvw2K2f9DwtDorgtURSkOZG0PpqWAIn7IDt8JmxqZ4+WMFIXG +F6QQrGobV4I4V2ZJ2FyRf+9DDQ1JFDdGWrSEGELbIvorbUkbMXjz6g== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c5.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c5.crt index b80546ca1cae..2b6583ef9069 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c5.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c5.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAPqm3dwF6v7a1hqPfWXePuwO/oYGMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjguYzUwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAJHAR+FS1lWCkwSuqgxJOTy58WVcjIkF2R0mCs2jmCTjcptZV2JL6VyA -3G88MjchazG5Ebtjl+4CbwyfUV3GR15sCTFq2EiGM9sCzuemScmnv+dR4u2P9zr9 -EKSK/CcNBFbf9VGT4WTt5ebYj6eIVjzxSTt7OuIFCz036nL975DxAgMBAAGjfzB9 -MB0GA1UdDgQWBBQrezQ7l6dt6YqmK7niD66JMi+bqjAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlOC5jbHVz -dGVyNS5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AFEcqSWmXLaIGLekW9XTyVwTAbU7be8A271Rp8IIfWFNrnIEXz/Cp6JChCpvCRxa -GjHaDFimp+VRGkWm62Ha1Acbbf+ln8YHFKnaq2adRCZW2pF5wuNMqtHlMS8B+2W3 -ZZ9fMXrd7kXxlzWpHGH/mBLASfMIac+wjgctbZZS0cJx6VbZmH+XSNlqJ3C2tSYa -7CaJHZUVClXL/A5m4u2LyDwqMQfixfon4/9U/NIwbO/pUWZ/wdqq1BA56Hd5DjVr -C4S5MFB6N7jETb6NtHOHtezewOxAofSMIv4e2N42r7ydGI0OKEu79LEVe8pyXqlP -W0zPUJ5D9UaWxdAo81onXzI= +MIIDUTCCAjmgAwIBAgIVAI1BC3zd4ooAZPipb/bZxsw8FPh8MA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxOVoXDTQyMTEy +MTIxMjMxOVowEDEOMAwGA1UEAxMFbjguYzUwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQC8tusYf2XErNSFHOJsYmEvxyTqHpHbxx86SleofuBgYNBMdBLO +P2vKBwAoNwfP617tGGlincHC3tStQAs8dzwETJSq72OVjTISG3Y1E4hQCbFeNpfh +4A1pZNtejXMkBkeA1m9ro41Gffgt/sVnR8yg3j9Gw4Mt7wh1ppuvXd0HxWKnfJa2 +GHeQ/EFOqAoEmdihCKFw31tmLqJ8aeFSy56mL8DWPmqHOWyQkBn6iZ+KRDa5GNUX +8vRp5PuPyI8wYfYqAL9tSenjC6CmCvnrtOHKVyW2AryAEdsUxYwElT4fA7OtTE/d +Rz+0duHZvyUo9eYq3qsNkkTzg/2314j/WDP9AgMBAAGjgZ4wgZswHQYDVR0OBBYE +FMRGtEPGaO03ND11stIioToFgGSoMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWCHG5vZGU4LmNsdXN0ZXI1LmVsYXN0aWNzZWFyY2ig +JQYDVQQDoB4MHG5vZGU4LmNsdXN0ZXI1LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEAOwzjwhLdzS+wcNicpI0bDrmbL9Zs05DFBbdy +AxT1Qs0JIsxs7nPFPrltvKg3h384Fwj2M6bThGSIRpPp4M5NdkyvA59tHQNX4fea +YVfnaN5+Jymn8dyq3SA10F//y1lNBk8qvQhZXHE5KaMcJNrbSQpSfTPs8MTEPyJV +fPCYP8FXAGxJvyhzFgo+CBTkpuWpWzQmgdczGcoblGNZtoINLYgpa585b7lsDzYU +CUDS10Py2hKY/IMuRqWZ7EAG1l/i7wY/NMwxxZapbXzoqo0zJcvADsDpr+bGopDO +oZk6xnlUm/nq5lV/vqA7i8RiDqi8ZmSKOC3l3MKG15fOdwTCTA== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c5.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c5.key new file mode 100644 index 000000000000..38755fab0be0 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c5.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAvLbrGH9lxKzUhRzibGJhL8ck6h6R28cfOkpXqH7gYGDQTHQS +zj9rygcAKDcHz+te7RhpYp3Bwt7UrUALPHc8BEyUqu9jlY0yEht2NROIUAmxXjaX +4eANaWTbXo1zJAZHgNZva6ONRn34Lf7FZ0fMoN4/RsODLe8Idaabr13dB8Vip3yW +thh3kPxBTqgKBJnYoQihcN9bZi6ifGnhUsuepi/A1j5qhzlskJAZ+omfikQ2uRjV +F/L0aeT7j8iPMGH2KgC/bUnp4wugpgr567ThylcltgK8gBHbFMWMBJU+HwOzrUxP +3Uc/tHbh2b8lKPXmKt6rDZJE84P9t9eI/1gz/QIDAQABAoIBAA7SgTGp4/joKEMS +Szh/G6oQhrY603MDN1ExQKRT3tTDFelDXqzDb3htR1ZhSCRNFOYjejeMM4lWTjcT +Zi6IIPcneq/YYKaA8hNZA9ZUY+sBb+R5/kD4A6WpDc/l5UrJkXktno/CR1jnkQQv +1BpyGcDy495vgdCsSypOfUDvauputHIgkuNq/WB36UjGoKs9sdxquOvPdauJxYlM +T1XZAMCfFo9Vqcg3IZUKhDrXgj4nHbY6HV/TRLjt4tYMxsDfItfz5p1onSclaWWH +LAVG+/DgB+JE7ZW/5mfZgIf7zrE52QBOgah91s/w8Ik0/fQvuivp9YcsEplI0h1Y +ggvpBFMCgYEA2Ckb5jSQRrzNu6yvbD9g7Wtms3fn3wEgPASHicrGSeIkAsRRyL8l +yij0NtvxrxReZ3GznXiIrMMLQEy/oK01kdeWczPsSSoxPix3BfUvZqcLybu+xgKu +c3oxQcTz7ewps/HuIQrerW1slsnEzVgaTPeVf9ElxeH9ifNK7NCnJl8CgYEA337Z +Y/FQDXIrM8NYpznnB02TJGDwcx6jLCcs4obikBdIIk97iJkO/pN36rX38cU6iVAB +/sGTFVZNfxaXzJZTeKImVZuzfRNWvVcCmzE7GDnmPU1nQW2sZ6Qh72mDY+QltLof +y3SpgebFCpOR2KPXLeUGTlDopBoIlVRFbD50KyMCgYEAtFPvK2f0aJ6/fu571fE1 +mjs8DUJdlIOq8YbRz5jzPNXENu6yT6dvDiyGN3HbOLTkYKXRMvBIDOAV9clRH+j/ +xLA2mdraJttlAzoy/SxjI979jYjriOLZyApLJ1P4LycST0Tn5Hbd945OxaiDXs89 ++bGd7Wh8e42hZsEpY1BZ8T8CgYA4DOQQQJDHDn7vOGqfZdIe7sGcOgOTm3dot1p2 +ZUWkxXH9yNk+ZMRx7CIC5ygLpl8L2NjnPzzX+RDucogXcwv0ERZ0Gdxl3de/HHc2 +3J3pcXvgm+ztZ8f5JhysolWlU65xKMyYheWNAocgI+pL3qs0g1+VfI0buru0V7Ih +d3hC6QKBgQCNdD52ua61G3DKWSI2H6ovkyIU7v1cB38oWuVg4HhtRPzAGcv01e1i +cRpKEQI7liztUIoDYyq8UMDdMAjPg3dvrGRPG96xeLMbovuPsg7eqji/IDnn3pXi +7/QEM6Tf//BTYcOZQGUhRT0/fBUp8mZVbM+lkRKW4Frj1QVTMcaSlA== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c6.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c6.crt index 361a392effa4..8b757353b96a 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c6.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c6.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrTCCAZWgAwIBAgIVAKbC2vR+wY4bWUJqzy62moQV3NghMA0GCSqGSIb3DQEB -CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkw -NDEzMjIxNFowEDEOMAwGA1UEAxMFbjguYzYwgZ8wDQYJKoZIhvcNAQEBBQADgY0A -MIGJAoGBAKPAUxo+4TTrLmVCZ1WoriuC80jKaYeDelj1F1AvT7eajep1MiDoQgjK -Wl56n2JeJiTPU49Dbw8IdgK+Sqezv1CErVx8VTsb/ljquF0yzWTzjoMPKa/YtgQ8 -KfdrBMcna50VF/7uIVvvxbLIsN1qQQhog29CSXqwyw4RjAVRCVvLAgMBAAGjfzB9 -MB0GA1UdDgQWBBQ8LqFuicvzgfROGbcN315QOD464zAfBgNVHSMEGDAWgBSrRunq -ZcqAopdSp1f7svo6+5eoXzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlOC5jbHVz -dGVyNi5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEB -AH8G0DRTzIWEbC9VZmnVxT6L9LHsc6z+9y8evNN5gRMAFMxfjaiTRjibqwM8+hjz -+oaSmBzkzY0t4WcljtEgvkzvIXEEVVeY/MBdppXOKbAh1wMxPDzD2SCUu+ILITIe -uCALZM8NucxqoOQeFaBXrtrn2y4mTzXVfoip3FZw1dP+M6sAEwfFdRBo051r5bzn -ZEC2LgpT5eOPWwFaMTj4I/qq6Ce96e/+PoY5ivqo16STPcurYVHd4yEr30lyDEPq -cc1jYEUNpVuklY735gfdPi9ec9y3ioLUCI025wLWlr0PKOTLhUOflkuZnC3hM6Jp -R2Ebzpapg4YR64kp6W8X7lo= +MIIDUTCCAjmgAwIBAgIVAORDwVyeob4Cwqck1NBejd+AsIMOMA0GCSqGSIb3DQEB +CwUAMBMxETAPBgNVBAMMCGNlcnRBdXRoMB4XDTIyMTEyNjIxMjMxOVoXDTQyMTEy +MTIxMjMxOVowEDEOMAwGA1UEAxMFbjguYzYwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDLGEkkMrmyYmJs78VjjYlHCCCOFqcQTM9QSH4O2HieIK5fzZ51 +gTE0XM4Uzwc5LJ+7sJaVfqNVgy2ha2NijrBsUXrCynnN9zEHvTsGhISnFTC9BTNa +2QBk0ZpajSVZhObB0mzCeBguSmqLwuiA674f2lFAA1n59cDwCpRHPsn1hQ8J8HQV +VrzMEpIwV9mLXEBdEbrw0lYzCT0z1EghSnxTgPX3LnEBbQyI2rl8wntdzzf9Ku+N +ZLDcBBkjekfAvHk3R2279BMG9YDhSk9QZGZ7Or70Ez0mjkTeX87wGoiuzRtkTXb+ +wrkGLULCNTm/RA1irHFMuqqdasJT2Vd2G4K3AgMBAAGjgZ4wgZswHQYDVR0OBBYE +FO8OjgrcTrFlCq2hz718QKZ8E7stMB8GA1UdIwQYMBaAFKtG6eplyoCil1KnV/uy ++jr7l6hfME4GA1UdEQRHMEWCHG5vZGU4LmNsdXN0ZXI2LmVsYXN0aWNzZWFyY2ig +JQYDVQQDoB4MHG5vZGU4LmNsdXN0ZXI2LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIw +ADANBgkqhkiG9w0BAQsFAAOCAQEAeeFm9bxWvvMIfR4GSQ9oyrnf1dqf7vZrjcx+ +2ZWU4mdxLLb+xc+aue6e5pZ8lX1teiekXm6IT+S7Edtbx13fPBkh1cx3R+boVhan +vSmnRReJ4MfaRnsdiG9nwRgQ88MpjXyhHcsrxxupgqw6Qkw4QreOLAnehDJJ4o9r +eg2TXaPWNMP3GNm5gzlnhzc44e+bgYYixEb7XeWEX9/oSY2sxq88uAiNG/nIvfQy +idzWoV62cH6OLbylzCeuH5RJtqyKUaH9dxkG8iifrCyW/xP06FmL0ISiEGWNPPrC +KDZCC6XKKtoFmjmuw8jeo2usgEeOGR1skWZLnaQoA4uOtfc59Q== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c6.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c6.key new file mode 100644 index 000000000000..3cb600ffb244 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c6.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAyxhJJDK5smJibO/FY42JRwggjhanEEzPUEh+Dth4niCuX82e +dYExNFzOFM8HOSyfu7CWlX6jVYMtoWtjYo6wbFF6wsp5zfcxB707BoSEpxUwvQUz +WtkAZNGaWo0lWYTmwdJswngYLkpqi8LogOu+H9pRQANZ+fXA8AqURz7J9YUPCfB0 +FVa8zBKSMFfZi1xAXRG68NJWMwk9M9RIIUp8U4D19y5xAW0MiNq5fMJ7Xc83/Srv +jWSw3AQZI3pHwLx5N0dtu/QTBvWA4UpPUGRmezq+9BM9Jo5E3l/O8BqIrs0bZE12 +/sK5Bi1CwjU5v0QNYqxxTLqqnWrCU9lXdhuCtwIDAQABAoIBAGH8nCFaVezCehFs +F5SiD4cVXTugo0cCgA2QhD1eZMeKJgdz+c1goSBN3GH+xtHnfUO9fdGv0VcrHKNi +YAeHp4gDQdLkhKMCbgX5XxaUbjn8x9CSx0XAJ+f8lte0fL41mG4QO8gR0DzZS5GV +3r6AvvC4UPYtB8bJMqa9u++p1B/fkAATPq3974Aync4lLwdpkA4gkJZv5S1J6ZUX +8iGOCAx+si2ZCJfMXauvBVlrq573V7XxT+tVpObw8NgCugVH10q1VIV+PPiV6cez +di47sQw2Q/Mh+Mp8Olgp6FQ65Qeah9q1/GnJcIg35ZFofrFEfTO1dZHdscIrcHZh +xG29EckCgYEA7UdWpo9DTiAW33eIRday10moKPNDnLuxqYxhpbsBaM/r5Gb0jvLv +9RQWqDvSPHGyOul5+RLzO62ODQCE4h0UFK6MZdoQ1U2gSGyo5ATwQwSIf7E6nSlU +gYkG++Zv06psMEBds8kQ5Irl/m5c/mwiyKmSFPsJH+qPv0DP27OApb8CgYEA2x58 +x5lx/y5ZL4xPTm3gFbcQu3KIVem3pe1lMVfeZRd/yUHcUBo6gx+7t0CASDpV7JYo +jc3yllfWoCDwIEhUNQVPmoPtqEGPyTZAOW+zoqL324ct6WPqHgpzTJoloXLMkBRl +prysPKi2D+GnhALLEl7pzARESisetTjaVRFsEQkCgYEAmJoJ44M98S/4Je51aR0k +1gscKIITmOip4XZUkscLqAIZH0MLxCgAJamuzLH0i0VAMF1aQdNrNZVwR3JvBBFZ +idecmiC+NMEA9bNUh5mZgDCZLS6pmMws+kZLhJR2lTYE8J9k5CzLPYELXDBUhJi9 +YjqUpgOxoaI0ovcm95Chi3cCgYEAuLA2zmo4mrSvOZOTGPElqkf2E3jTD08CmxnA +bFm6uwyWicU4dE/XN8RcjXYRak+5MsPSBMwtB6mb3rZEj4FHcLU8RmmivGoeTqOq +eH79Mz0XmvKSsSClKZL3Yf6fbzh60YpPr71hk3NEmZhCgNN15NMZTvvdr4IuzOLR +rFgV9TkCgYEAkBBUE11TJKHaCDr5l5iJ1o7ptMPi4NT/lOWDAe/ys7WouT2/Gsu7 +I2zHBIEkVBATOiBc2LL8q/ji0M2yr/TdgXSoz2wJWzIq/Nrrx4mLcesZ+U3la5nj +R7HD/7YavsGxBPMOsBpNvIz0WGKv9A5O2VkhaCo6TnXIg5cLJxBKoxQ= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c7.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c7.crt index 7cfb81868ccb..bde366c585e8 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c7.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c7.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUTU/oQlfBcPG83Lt2OtobAowVER8wDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE1WhcNNDUwOTA0 -MTMyMjE1WjAQMQ4wDAYDVQQDEwVuOC5jNzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAuA+oN7YU2z47w0L21xSoUQc1p7LlrHTbYZDmoCLKQqg0IUP5gvENBbzE -0+rb+ckhpCHPKZSL/Up0+zXiAPDodeZJRc72O4u/jXc9DcHhpV+w4obT0mZQDEtt -ygBDY+t0WJyFZZpgS250XhHMA60w0ZuP6fhAvsmQQDGDRmX702UCAwEAAaN/MH0w -HQYDVR0OBBYEFHaUc5wM5Ve1XrEx3aYe73WSwSWeMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU4LmNsdXN0 -ZXI3LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -WSFyo3bRteiJe+iOL07z/2PTuB6LfM6klZTllhGZuKDuXM8teKluOoJ/AgkVEb6/ -uuqzkAHncMVy+BiYqmweR7KKT2siQgyHMl/tSMysn5I2YVL/sneIsdreGZkAUQCI -bGCCPuWbCX8OJAayVDS0U0zSfwsAJoBiAjUDK6ZwzVdimX9WTCJcx/3eJgrdPese -Hv3XoeTWocfWQeA3qSRb6YE6yOhuDvPgM4H2jsCe2gYeOYeNnD1BlP92T7epaxx7 -00kMsZgY30VRZ6EQ7Hkzghc/9VXLd1jFZ/ICC9KJ2mSjCl0in/5JJ5+uCSBybbII -uBwjvpppSFUVLimeeIwCEw== +MIIDUDCCAjigAwIBAgIUF9P+tf5HB68Yd9n7HzLLzw772OMwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzE5WhcNNDIxMTIx +MjEyMzE5WjAQMQ4wDAYDVQQDEwVuOC5jNzCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAPQ6GN4QL6hINd7MjNW0oZxbD8QfBMTa/Pt2cBAQXEI/jtJfq5DB +LcsZzazdlwbS01Kh5zfhHtn/Eat2FIRssjTYEA+LvC3CeYGvbZuIn85ZkBg5VH0T +WYbz7yNCfHv2xOsmxCzVeWCElGNHA0Tt1NfpJxj8gTHkZHAcd5OEVo7lJvmyJQjr +/LduCVSQwRRM+/Ha/J2wpdVWL5l6wM0hT45M4dilvwt3avWf/Xny+q35zZFQGUOr +OPRju3zAuv9r0nKQSJCyQHSqs+sffBB8AL56jPWNPzZyKnPDgeFt2PQR1x/Dh1IB +IdxRyqPtDdKnDgi2lSTp/QsrXjoQ5lPDoNsCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +DMml0S9PDSZEJDX26+niPflDYhYwHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRYIcbm9kZTguY2x1c3RlcjcuZWxhc3RpY3NlYXJjaKAl +BgNVBAOgHgwcbm9kZTguY2x1c3RlcjcuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQBkNZbH+EJBklVCzsWUcfM+qyZwAGdtBMW02s/f +wR1htso4FkE1W011LgT+ankjWIJAVzqM16n5tle3d97iq8owAoU7utcZXZUoY65u +4lIWgd1bJMemaVq3SMDHpDZdcyXRMccyDlTHF5sD5aa1hXDOrRB0u+3bfaniAA3K +S6QkBOZnlbAlSbKQ7Z44LrN3D2Qx8Gpcx6F7DROFLv32RDsUae9YRfsXlAVqqfs3 +HOyHhecSS5LAIvM2uDLqB0drZJaiQE/ISQC4YZeFBfmIbjzKvPPK0BSIEQuaG92M +/FOHbxaNrKBl86euughfedJlMIA+7ggnufSxiiYWje4Ds8XD -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c7.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c7.key new file mode 100644 index 000000000000..d8c55aaa4b13 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c7.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA9DoY3hAvqEg13syM1bShnFsPxB8ExNr8+3ZwEBBcQj+O0l+r +kMEtyxnNrN2XBtLTUqHnN+Ee2f8Rq3YUhGyyNNgQD4u8LcJ5ga9tm4ifzlmQGDlU +fRNZhvPvI0J8e/bE6ybELNV5YISUY0cDRO3U1+knGPyBMeRkcBx3k4RWjuUm+bIl +COv8t24JVJDBFEz78dr8nbCl1VYvmXrAzSFPjkzh2KW/C3dq9Z/9efL6rfnNkVAZ +Q6s49GO7fMC6/2vScpBIkLJAdKqz6x98EHwAvnqM9Y0/NnIqc8OB4W3Y9BHXH8OH +UgEh3FHKo+0N0qcOCLaVJOn9CyteOhDmU8Og2wIDAQABAoIBADN2OH42V+x0RtGr +NXPki3dIYuKXDKi0YkFUfBmOcPpOrTgcC6lFY3Jb/hBSYvv/maLcVb3bFGaoDh+c +kocy5jwnWP8FmUHlLDhM8GIpIE9ZZPkNv4VZefBrXzUFUfjHD0hjk0vFMb3KQgBH +WuN/3+0dBm2H62tJbnaHMRUm+hxCuJkvXOx0JcB0GCpyP3rxrudktWIujfqwlKs6 +alKPgp3gp6rBRLhOx2/OcKwgWwwVk4iq6Jrdq0sz4K3hucOOyQpH20Bcx9U3iag7 +MahpWAFTL5dpwoIHTMDKxnm0dOVVkjIN+dzw5o6w65aylZoDf82PHTUZX63KlI6E +CCpBofkCgYEA/MpVsQS1J8DzCKQTmmV72FmbbOSrBaqQkTWi9i81UPf1y4KiQqrd +FgUopF7+yjqpp/Dfb13MTkF6b2QB2VR72ysQ9TBCnMHVgZuGRUrjC23y1SaQz04b +Zi+5trsQJeZezkotN0Qhv/a6pvO/Hx27ScSkO/LQqCJf0aPIUWwY/CkCgYEA91Pt +kPxE9Dmn2AQwer1dwJdf+quXpW1lckXwjEiOwSePwck4D1qrtOPmVdKlYZPAkXnf +eCMPO6gSnH2yJKr9+DR4VhalJ6k95roHFywFC58zo/+3Q6Ix4VKEEKPJLkBdPYHz +MH6uM4NeDksPlusbBfj+lP7GbLyiBIagZuNs1WMCgYBmynvHzGtBbAMnO1jBSozu +b5l00RL97dp1QYMvagjbEMkA5PDH/SSDZLufAwXGA86i/J5Ocay3EYcQrnDWB7WJ +bFyr5uDfTy5bUntpx8bFCgNZSyJNOayIg9WXiT9Kuf9JEP6L34d9wH0rkCfiJjvX +Bj/mJJlWFQwzec7l1Pff6QKBgQCbVBtkLfsEQt11nhPwUdN6a9c8b926aD0JCoGr +KLp29mijdML/aZti7KoxBi0VXhzXgGnnYmVjuweNPqfNvZHo+tMEhg5NHU4iBH0F +gYDdrqtY+PVYxZl8AD2u0Z48rPGh+mYH89dlxMAiS+PLjXYYRytj2ao2ijVakE5W +r8gfLwKBgQCD7/WHRDpte9jsTh0VFGyabnZvFjhkQ4bIWaCkuYKefQ8fUxgBNqAC +eIH3E6DFGGgOldz5+ak2BlQ0fw7Gvmf1v7RBTaf+SDdiJoGbZ13Zn2UDRfkD5svj +h808VYsP+YFGm6FzhDFFf6aNrx8SdsFBE9pkrd3BoeIpRj3xj0YKEw== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c8.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c8.crt index b85a2bc8ff35..423dbe572edf 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c8.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c8.crt @@ -1,17 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICrDCCAZSgAwIBAgIUX5JaUQz9mUV4BlmMMbsDzYG8Z9UwDQYJKoZIhvcNAQEL -BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMTgwNDE5MTMyMjE1WhcNNDUwOTA0 -MTMyMjE1WjAQMQ4wDAYDVQQDEwVuOC5jODCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAwvRhMUXpnmopSSajgAAcImV5vnRLUB88duRl/j6oM0Oq4xxPoKg1HfSj -TnJuLd3mrMuTQPU67KodJoZ1keFoeKQGH1S6pDT+GBI+8IcZMZRJXyeourDB5Akl -qRVKPiciepoVCHuePYZKVlsUnWbH1Vg9yMcYW6Jx6zbnS/meIC0CAwEAAaN/MH0w -HQYDVR0OBBYEFO5ttLIFOaMQELSrMcKok9nD4PqyMB8GA1UdIwQYMBaAFKtG6epl -yoCil1KnV/uy+jr7l6hfMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU4LmNsdXN0 -ZXI4LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEA -g2NQZrSAadjMDCpeTeSTUJFP9SL81fOJuxK8X3VCTZUtFJAUJTKgrjAZWkDXWcYf -47lzAoHuCI07/AVGkjWepQTzW5LdZWOdc9osIKRCnb9dofIrWrMYoa+1BVOXvasM -aErtrlFze3z6WAznQPjQ+R/phpwZSJksAGJ9VPG7AFla0k2oCjOvu8FJUHev+ft0 -9D/ze7TXrIKOdev75iO+F+xUZJnUzDk+rJeWQYC2onyHMUA/oskpkT9GqV4eIb3d -sJkJVbBH0V9iMahX+9FeYf12colMBUgK0lactFMTrqzP+v3v3vY39UBlDhR3x5JJ -3pPRrvAcnxZ6pobwgNS6HA== +MIIDUDCCAjigAwIBAgIUceWfs3lGqwxm9derhIa59A3ssmUwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwIY2VydEF1dGgwHhcNMjIxMTI2MjEyMzIwWhcNNDIxMTIx +MjEyMzIwWjAQMQ4wDAYDVQQDEwVuOC5jODCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAMC9+zC4FlWRP5eEix6jRVO0+QW4AsHQGjaT//KvtygjU9FcE2g8 +lFDTESry9Yiu9wBR2KVR9nYkxNj7KvT1hnbhkgMdiIPEqLQyvuwFSX0BYtclWLJG +rzNux9QcBuF75ei8kGupH8fJUowY3ydHT1fcj7Ti/8jtV1Xn9OfQbpcsiENaMKeN +XAkvoFAmwsGK7292tBdgnutJ+/ltr6mJeYk0WV2lUgkn3WxeEw/572mZUimebLxO +GfwJko29JGQ2izGPVLa17s7mr47roRJ0S5zOsTrG8FhjXrqy+bfr4gaVEuzC9q5r +ZFH0Kzebzv+KP8twvY02d3i9YgqJo1IUh90CAwEAAaOBnjCBmzAdBgNVHQ4EFgQU +1LOn90joG1Q7EP0SpFuI6/y9JZ0wHwYDVR0jBBgwFoAUq0bp6mXKgKKXUqdX+7L6 +OvuXqF8wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTguY2x1c3RlcjguZWxhc3Rp +Y3NlYXJjaIIcbm9kZTguY2x1c3RlcjguZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAA +MA0GCSqGSIb3DQEBCwUAA4IBAQBMJqKwX1e+pMKe+CWgiZhV2J/ayMF8haK7u769 +V/UEZSNRebpbLqu6jlwkCFyskiei7igTz5r2I7x0tCTGIbf2qYNM0vPoFfRDS8h7 +DEnloaARBO4G2QWBccpT/ouU0BEUkjqFaYBuK71kVaUbCJEzE4XbLSPluForyeBH +nKqasqdxigxztgI2Ou3j1qfj6u2M1zBtYO4l5DD/MNxtYJQs+vpYqtNQyRUnFs8w +Yk2STZNEtZIIbSEsJJo9HVP6yycQHMK+ORczKmT/zoo6fgmsyRR2tkBfadj9aFaM +YiQzbOunQf175TPv5YVTCH59GA+nofrKJ4iAWo1Q0qizdLUu -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c8.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c8.key new file mode 100644 index 000000000000..4f046382a60f --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed/n8.c8.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAwL37MLgWVZE/l4SLHqNFU7T5BbgCwdAaNpP/8q+3KCNT0VwT +aDyUUNMRKvL1iK73AFHYpVH2diTE2Psq9PWGduGSAx2Ig8SotDK+7AVJfQFi1yVY +skavM27H1BwG4Xvl6LyQa6kfx8lSjBjfJ0dPV9yPtOL/yO1XVef059BulyyIQ1ow +p41cCS+gUCbCwYrvb3a0F2Ce60n7+W2vqYl5iTRZXaVSCSfdbF4TD/nvaZlSKZ5s +vE4Z/AmSjb0kZDaLMY9UtrXuzuavjuuhEnRLnM6xOsbwWGNeurL5t+viBpUS7ML2 +rmtkUfQrN5vO/4o/y3C9jTZ3eL1iComjUhSH3QIDAQABAoIBAFIz9KDnsmbWJugJ +WIg9nEDCK3775vkgNz3Vy1CHDreosYPWIFTRmXQLQQQvUPb1oFTbk2mW1mW28tmT +7dNBsBKdy/dWLjX8tvL7vZ/N9XTCFZSq0fRe3zQxaDqXpXufqwOjAHlaTTqZK4Ki +zPHkoPtZD1VXhz37s8bowfPoWnJQhll0LItHSJP6fVBWxc39plgw5Q/ETmL467dt +zHiCKzC6m1BTikfqm0CGUYC4rHF1wKIsQG9CnPMa+nEhO641lRPy+TlEp5HjTOjD +QBQcXpx3xW/zofNeRnJPCO8ZJGEvNydejWH49fkReznUxnsrxVe9KshDt6jpnlNJ +2z4UI0UCgYEA9tJyWPg4NXUvkecy3Bvdn8pbGEZCjxYsQsU7bBvZo0NKm2z1xSrC +DWr1E28ZX9qFP0MnGxBHJgE6zAJg5ZED7GYHMmii883Q9UzhXQ7Ela1wPemIcxGB +GFaqGivpMLyi2gdgBpXT7++riUC+X2QEwEhHC06c924aJYCyYk9Z3a8CgYEAx+i8 +VnxKrvOXRE1rmzUlksfyMJsmYG/xuh3YDL5vCmgKebUEVnFFR1OGA348lnamXist +Vk+Tsho5LbDVEjeYU7utYbEAGKo2ywJNTQ9gzzcNUml6VZwYmRcTydR5lGO7bkJv +LSmmxycKNlEr7LyXhvjgmoThXIkzpHcU7yYuAjMCgYB9JTT/MVPmnvUwqCYcfjB/ +zW0kwLMMs7RbYn9/kBtocT0J01RF1Skw/nMmmGUoObbn3ZedXEKG3Ya/W9FfdIil +uxGZGH7O2SNCzOlTcqJwNdE8QwWBOnXXLlHvFSaWJH75x1WD4UHlXQL/g4lC+oaC +K+OBsylZN0UgkMFhoVQyVwKBgQCn0RCJ6b0oMQUDKZrzqCHqnj8J3rTQsCjUfMkp +qZO9wKlKklMZ3eyye8M8m3RxOxleZBCjLBpooOrvGEtGB7WwVRSvtc7G5d0pfr01 +u3sheK2OSU2RIIDlwyGxhNSrZS2q2cpKt3RyKqu//VRKX+c0PykNjg9Bh0v6/Jxu +RjbJUQKBgQDPeTNf4e1cMs7CpNB1NmvLyWRlbyRvOGtmtprNmavu7Y4x6ZXtlSMS +u5Abfd6uyrt1ViKS0bOz6yQik2F60VdWFX9RoyXUIvtVX6kVnET7m5kwymmtsRYx +P3e2FLa8R0zjoj7XXby5WMHjGIDChuhJ8+9CKtAUwoQAWL1U7T5OJw== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/readme.md b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/readme.md new file mode 100644 index 000000000000..c9ef227a2e85 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/readme.md @@ -0,0 +1,58 @@ +# Create the nodes certificates + +cd +export SOURCE_ROOT=$PWD +./gradlew localDistro +cd $SOURCE_ROOT/build/distribution/local/elasticsearch--SNAPSHOT + +### Create instance.yml + +```bash +rm instances.yml +echo 'instances:' >> instances.yml +for n in {1..8} +do +for c in {1..8} +do +echo " - name: \"n$n.c$c\"" >> instances.yml +echo " cn:" >> instances.yml +echo " - \"node$n.cluster$c.elasticsearch\"" >> instances.yml +echo " dns: " >> instances.yml +echo " - \"node$n.cluster$c.elasticsearch\"" >> instances.yml +done +done +cat instances.yml +``` + +### Create the self signed certificates + +```bash +rm -rf /tmp/certs; mkdir /tmp/certs; rm -rf local-self +bin/elasticsearch-certutil cert --pem --silent --in instances.yml --out /tmp/certs/self.zip --days 7300 --self-signed +unzip /tmp/certs/self.zip -d ./local-self +cp -r ./local-self/n*/*.crt $SOURCE_ROOT/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed +cp -r ./local-self/n*/*.key $SOURCE_ROOT/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed +``` + +### Create the ca signed certificates + +```bash + +rm -rf /tmp/certs; mkdir /tmp/certs; rm -rf local-ca +cp $SOURCE_ROOT/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca.crt . +cp $SOURCE_ROOT/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca.key . +bin/elasticsearch-certutil cert --pem --silent --in instances.yml --out /tmp/certs/ca.zip --days 7300 --ca-key ca.key --ca-cert ca.crt +unzip /tmp/certs/ca.zip -d ./local-ca +cp -r ./local-ca/n*/*.crt $SOURCE_ROOT/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed +cp -r ./local-ca/n*/*.key $SOURCE_ROOT/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/ca-signed +``` + +### Read the certificates + +```bash +export CERT_PATH=$SOURCE_ROOT/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes +export CERT=ca-signed/n1.c1.crt +openssl x509 -in $CERT_PATH/$CERT -text +openssl asn1parse -in $CERT_PATH/$CERT +openssl asn1parse -in $CERT_PATH/$CERT -strparse 492 # location for SAN OCTET STRING +``` diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c1.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c1.crt index bca151e28b18..c4f91b15c6d0 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c1.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c1.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVALishM0WVKx4AixPiFxwW8AT9G75MA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW4xLmMxMB4XDTE4MDQxOTEzMjIxM1oXDTQ1MDkwNDEz -MjIxM1owEDEOMAwGA1UEAxMFbjEuYzEwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAM/q6yVK17PHtdsO5pM6DNU6pnOY/FQO+c1JpD2cpOk6B8yokEtFR+a0Qsds -qQewAGBG77u9jQVerJr6fkPW+AeJT7eEBl5rqYDx82XgeJS6dAJRvclrxsOLBDDW -sImDIMes0AZaE54P6LDGBooH3XhidTyFj2Gp9fozVY8PWFl5AgMBAAGjfzB9MB0G -A1UdDgQWBBRLyFFKNa/ll8mohWv8TJyzCzyOmjAfBgNVHSMEGDAWgBRLyFFKNa/l -l8mohWv8TJyzCzyOmjAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMS5jbHVzdGVy -MS5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAv/Gm -maKqfIIGPLWfRv4/GQRssogAOHdqlZcYojxQ6Ovo6ped9qxscYXGb7+8sP7vIeR+ -ibXEoFvbObfM4EyRONo414PTyZnpsg/Jqe3GAwnd4UNrXR1LLcFyxaUDoDvAtKkh -nI0iE+0yGwrH8UpMuLXyqqp/ivTlTkMBdFVvenA= +MIIDTTCCAjWgAwIBAgIUBtD5eWhDMSkYinFLJGVs93evIBYwDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjEuYzEwHhcNMjIxMTI2MjEyMjU2WhcNNDIxMTIxMjEy +MjU2WjAQMQ4wDAYDVQQDEwVuMS5jMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAJz9boSX6TZSTf8SmP7/YGDwrTKqNjU3GS7ldaaUOtOTOBuBYHzc7WrO +7DlQhNVqBV2euMyXITSNNqcIWpjNivS8oz6PExrEapvjzJyQH1J/zJZ2knW/08U8 +zeTq7Shh9bl7NJn4l2d5J9iDzHvWP8zxYzbNv70Z/12usEqrXdtoJPuvD7LEiyXa +o/hBKDVQ7Mv2bQaxVJYnsMIEaX6gXCy3cH+9M9v9uGjhJPDvpPZV++d2RWNvvTdc +bCMqA//15EmwKykjK4k0UWZjkISGNfAGXva9V6v6JQGBZshlsQv1CdBCtbC6LMnQ +IeBQuaJLhZ1npDtGHBwcqep6GWSfL/MCAwEAAaOBnjCBmzAdBgNVHQ4EFgQUImFC +TUp1Jq11LbYbLcGBdVXzQzQwHwYDVR0jBBgwFoAUImFCTUp1Jq11LbYbLcGBdVXz +QzQwTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTEuY2x1c3RlcjEuZWxhc3RpY3Nl +YXJjaIIcbm9kZTEuY2x1c3RlcjEuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQBcI0A1lAzDbo1J2Vg3uAmb3Takkq1yqBNCkDeetV8i +Z9Ko9l+3VZpzgE1t+acUQOQwvSRTq4CCPP2Kbno1oN96PFmBv7vTKFyiI9R59Eww +eJL5R7jNj2c4runmvnqRHEOKXbnfMYU7aXghE35YYJcbbiGyaW64+htAdT6zAbO0 +2/I4ZtGcx9MxfN9ZwaRFPKwFMufmyP+nfqMLw3fM1FWBtxjlOY07TKSvwi+HTosm +Y9lWWxLBrkJKeS+XA3Dlj1nTcQnBtSVp8qQ6ac4eKmFaLN/Ig/WA6uT2/IWazXzR +i7YduY2GO0tJrHAN/G3vVsOeyy6l6zGwkz+/p53u/96p -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c1.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c1.key new file mode 100644 index 000000000000..da1100be6c0f --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAnP1uhJfpNlJN/xKY/v9gYPCtMqo2NTcZLuV1ppQ605M4G4Fg +fNztas7sOVCE1WoFXZ64zJchNI02pwhamM2K9LyjPo8TGsRqm+PMnJAfUn/MlnaS +db/TxTzN5OrtKGH1uXs0mfiXZ3kn2IPMe9Y/zPFjNs2/vRn/Xa6wSqtd22gk+68P +ssSLJdqj+EEoNVDsy/ZtBrFUliewwgRpfqBcLLdwf70z2/24aOEk8O+k9lX753ZF +Y2+9N1xsIyoD//XkSbArKSMriTRRZmOQhIY18AZe9r1Xq/olAYFmyGWxC/UJ0EK1 +sLosydAh4FC5okuFnWekO0YcHByp6noZZJ8v8wIDAQABAoIBADdDR0pJp4DFzEsE +IvNwtl6pmm6BWH+3G5hUkevHtkWQR2n3Sx3pL3Iv0RwD6h6uKaFhJDBw6vFy3FBE +MpAjmsy4Msbs2o7LobOqpvZeDYifEEfPCZOFqWh/FQOGZ2GVykg2xLeGuNVl4s7m +5m6ZZvAf8QrztpE8u5YgzlIarPl0zEbXnlF6rY9YZPb5CyFnJg/u0AIZTpBjoB8N +QG1wv+fiI7R4RUT+p9y3QoJK4MS45iUK6U1jehhrOyyzOuJy6GnS3qCHUSGdH2K9 +56i0qOKngBX1PjWgxkOOh4YTdgABOk3STBm0tjTh3Oyjs67cBItdGS7Bzd/JS/xu +jY8kiwECgYEAuy6ZMOi/QMIqpsFpIfwrwuSax6XLWAYR2IQTGPSlkVTj8hzzN1+V +eYVIJAMSXhnY7bNL8reBsyMNsQBRilnD0alhGAqAENcXVc5IlWAzT7H2pJ5yKOBJ +nbGADMWdfIDMkvlEXxoNtnGT3j4TLO0c9HYWDw+EssqpAG0RqAbYY0ECgYEA1rUv +M8FU/1p/+0Srkecun70A4D9OJ5lcK2/t0Fe4jXMGCE+GhX5Wv3CUVFYuBssowXAN +quMBSWXlzjNxFMMMmdHf4ZGTtLUEiW7md5hkMUkhskAWlS+dfOZx7SVZK9PZipSK +Pu4x9MMpfaQ7GtPbPNkXV2LlHQtUgVKuCoxK6jMCgYBoYUnn7kKImD40k3JPE1JL +8jMl1hyYz3mwJaaIqhSSAKbEYsR2QoUWKO7btiLN+tfvZaaaiR54iLmf1K+j2P7L +sgqE34Ye3fMTVF9BfU3fyTvi/MBobvpeR5dCiAiUXza1Vu97oQm4jYmJy0/iI6+b +2yXd7CdxlHW92RSZgIe2gQKBgDSOQTGf/1hKJM2yr3SVbDo5n39Ev7K9vWv8h+/Z +qRkmsLFUFg+TL1TJD+Xc+oM7M4y2CwJrcqMuyCPDP8jfPbRhRTYH1moDbUre1Jct +vEi8J/1j5qM59ojeN8DexYcO4k8jsPtafuv04bEZhEY62Y15blc7o+2KGnNVc7it +QqE1AoGAWeWK+I9g27UzLcQx0voBX5cMiM6i8SLfyfX7Ruok9dpwlvOuXHr77W7F +9RB1r9ZxdTg1cUAsLctN7wbxs7uZTuYA2LtTa4KinDjKnWG2vWI0TSDGuTWccsBF +EswgAhdEuEOpNCCNCokzmqYX88/WPwD39FBidy3K+R93iSXeOrE= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c2.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c2.crt index 848b7c5ef185..3a6dcd481b33 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c2.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c2.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAK+lmh0imRaWbjXWB5mEQMqEUG5pMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW4xLmMyMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjEuYzIwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAJQtUzqzNCr2cWeNAQnn9hwkDhrBSPyFJhQlZ6Otkavw2Z0G6arPXsgYw3Sf -ZTBE+Gq5KidVb4NK6HWVgoPEzlN/lq9cVdbtr+KAU3cMn6DK2cSltzbTBkEXa7Lk -5H0Jt9qLquHifF3H+s4gsAKbqCCgz0WYqbvvSapssKsOOWtZAgMBAAGjfzB9MB0G -A1UdDgQWBBRuBoJoTcJq4QacLWQDLb927B9j6zAfBgNVHSMEGDAWgBRuBoJoTcJq -4QacLWQDLb927B9j6zAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMS5jbHVzdGVy -Mi5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAVjwG -V7GyCbpBNfXgjVNeO8EygBUjND27dO15a2ElwcNAG7mi+4f4ewIVlf06kzXLzFu7 -ig2rV8YiCDua3FctIWDhrWrvvk/xwoTw+o8z3RBE/4q8m/60BWQATHdmdq8jBBxw -oFhMS1yKN5mnGdEkfPuq0t+wK29BPU6jVUsHJyg= +MIIDTTCCAjWgAwIBAgIUV9sRC/CyQ3RlbBT1rkZbXzqtlxQwDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjEuYzIwHhcNMjIxMTI2MjEyMjU2WhcNNDIxMTIxMjEy +MjU2WjAQMQ4wDAYDVQQDEwVuMS5jMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAMkSIfiyeCl7wpDKvW5alZ+nYO8yy4qS3btwtHnKoQsqbtoUL8TqVUpS +4umNva+56qUKzItdbWwMFMpy96dsbqfFoy5Qx8aSSd3ZXAy5vK08l4fN3+IkPeGL +dhyBpXLxbhtqSM5doLUZklfq0xuni2KRTWhGeJl05ZUxij1Wsz80OjopuDUKwxBa +8Q57TIdnoLDqFBNxEK5jo2l2NQaDxsZTBP4hXhoPw97ujHulEOOXVQnibwfuDPtn ++FBTAh9kYsKeuG1H/2i6NxpdtxlGrZ5fAE3Bp0p7S3sK9xL8NsK8rVlgKVRKGjJq +rPucPoBf0sy+xik0rP0Vw3w4yJhN3jkCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU6ZSh ++ZcljelRUKQDb2DNfZUoXOcwHwYDVR0jBBgwFoAU6ZSh+ZcljelRUKQDb2DNfZUo +XOcwTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTEuY2x1c3RlcjIuZWxhc3RpY3Nl +YXJjaIIcbm9kZTEuY2x1c3RlcjIuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQAZVr5t+GL9R861bBgV8Ns/dMzNFXzh9j7VVvvCLwKF +lDcL1MmTFVwFdUA2eX3XhjxRoXs1cqF932DzEfqH2OrU3m97GMZjpZWor3adTl43 +JevnOQxkBQsaBwIHgTnuzHGqdSNFHXrG8qhJvtIPBZE87x9P6AWKoOCvDjNNtmCZ +MQoZ2SsKtMlEkrMYRpidWPAojBu0KSpMadSCGMG7Qm1UtPj25l4FHfyr0Q5Ym/16 +fmz447yOcgSsJ45cb+BcrIF1eobVkVVuZAAdb/ELANAJcV+vSzvOeoVMgG+fOFUj +F1aBQqTtydi9dnovBShipLYEQqU+A0h61KKfiFFxkH7P -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c2.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c2.key new file mode 100644 index 000000000000..0df30bd8abb3 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAyRIh+LJ4KXvCkMq9blqVn6dg7zLLipLdu3C0ecqhCypu2hQv +xOpVSlLi6Y29r7nqpQrMi11tbAwUynL3p2xup8WjLlDHxpJJ3dlcDLm8rTyXh83f +4iQ94Yt2HIGlcvFuG2pIzl2gtRmSV+rTG6eLYpFNaEZ4mXTllTGKPVazPzQ6Oim4 +NQrDEFrxDntMh2egsOoUE3EQrmOjaXY1BoPGxlME/iFeGg/D3u6Me6UQ45dVCeJv +B+4M+2f4UFMCH2Riwp64bUf/aLo3Gl23GUatnl8ATcGnSntLewr3Evw2wrytWWAp +VEoaMmqs+5w+gF/SzL7GKTSs/RXDfDjImE3eOQIDAQABAoIBAAJOovUnlxliwijH +KsVJZM4gLYyRGKTOapzr/iYnnRFzuzHlaWbpCXGA4fmsFrEdgQPDy/UNbWP//30T +NKRHKj3ilqzBYIl2xEI3bEb5GqEiQNreEeOWZt9fqktJcYyQGKNX5N35VQ6+7F71 +4tY+ZesLn8fChz+7h6bemqd5hzUCbzOxdX1LuK7yDJ01OPUqeVolqolHRVpvLjPh +dLvCzIcDC00d6aWIREpQ6Fm2FNVFajfyfjkhI51ASJ2zMI2BV40xvU+9kcoBys1s ++RrCtKCUymoEulyLHVsaCBhU+AyrCtFk4azDdRACFl7MQ8ImJmlCBqCIYPHzaU06 +xplRCwECgYEA4tfX5xUFJIUrYL+JzEliVSBb8HaeXqisJarH40sOoY8BKHqtB80U +Dz7KDxal7Aq+uZsYcGP9ol9Qy+x+qvC+vOfPlvYNCvPHCzbJUZVb2lp6VgjHkPtm +8KyAPgWhXT6UMPt8Ymx5sD978jYjXcdh06upOcKvzwmq5aKn2OBq8Q0CgYEA4upD +zBoptiV9VKV3kn420ilmasfIRIFzMHKKtLjktPCoOQ3vT65f2d+8F7yt7/YJzVof +CwYy4vzOXhkAgoUmplpenj0O3BcijjKig5A8vFvr0M1wbQZoIjj2kh6x3mffIPvz +EpnPys/RUvsLEkiu08Gdwf9ePHoax9gtly0dXt0CgYEAtVnuB5YseFfmqAA5ABMj +A/9MwV9zmSWsCqkQCMy2s76KJHx+3J+9i7qc+5cVqzGj7OrrjN7DpQ4ioAU/9xVk +x81jjsLDBqCB3ps8M3b+6gAvkZyPP39desfN9se1i8A95TMI6/7hAmCejLNvbse5 +k+nG7qCQwSpvcAqruZ0gofkCgYEAloAABYAYyDlnPdwdSWky3QMebZYiCuW8y5Y8 +zw6Oij/Poz/NyJCD/FJ7Q1ceGjiaWbv7OOmcmJZKHXkBsI+1icIIXXVrxKgHgD6v +MQlabyecaTnI5lbIfGJBj7qSCEvmXs8ccerYOdDKKfpdZnwUkFgrVP8VtpDZ8FA+ +NLV0dHUCgYEA2gvZSxYorAlIfKM9AgABN7qvbQJ0gIt7GrQmn7fGJK4LmKt/Ima4 +0+zL1w2WK3pR7CYS73Ij8w1mVJrqHunKq96/aRR3TMLiGAIRB1VkAIX59+U1S5J0 +DhFct+iEMetHo11OwDnx+LPpy091LEcYf7Yf1xIOUBqeuz8yXBM9xeg= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c3.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c3.crt index b655bf344d89..0814f70c7aa3 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c3.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c3.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAIxnFnx8Ow1xmNEHSzoEFNU/as1+MA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW4xLmMzMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjEuYzMwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAIVyhLjNcWHszffIzSX7aQRkgEOIzNFuJ6z/OjYX5SoQgjZ6ej0oeudbeOPr -/LT6NmcIWgQJHIa3w7Rygbk//+jyLXshG7bI/3o9js7UPVzuoePt2y+nHFYHzL5u -mA9cuk40zPXwr6W7aqwTKLhDmJDzywOpcdtycluZidy8MJ6NAgMBAAGjfzB9MB0G -A1UdDgQWBBShH89k+Y8GKljoG52TiS9pi5KrQDAfBgNVHSMEGDAWgBShH89k+Y8G -KljoG52TiS9pi5KrQDAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMS5jbHVzdGVy -My5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAb/80 -ShzdMSxzpLi3kQeOyisoFclkGcfdZAk+wluPJaZjXzu6KdaUV6J23pC5NJ1QP6+N -2xStsISJt+Cf08S9Oxae4NCAD7mw18oxQveJNlzX6UF+/IilLOGYI1Xfc44BImfJ -bRInBM8Yip1D3tWqZcPGS/wSlMq2IdyVm1tOPpI= +MIIDTTCCAjWgAwIBAgIUczFiOGoNnJjSSvoHyZAEUBe2CGcwDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjEuYzMwHhcNMjIxMTI2MjEyMjU2WhcNNDIxMTIxMjEy +MjU2WjAQMQ4wDAYDVQQDEwVuMS5jMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALxS8oBTVBW6XRUbwMmaCUt9x8JvEeNimlhT3sxpb+UfXXAmdTlwmvnA +iWit990KJE4DWNZTU6yI7k6ZbHgz8nUtFFdlikmLHK1QAB/9RxG+8SWU3VLnqm3S +WnHzXrQ6RNYnudq1Qfm4875VHvO+KOmyVAuOCFtDXFwzr6YOfmXSJ8gFB3QPy6AK +um7XqOzFGmzHMTW55IbXkGH/+lD3XuV9y/j55ef7kr/xfyEi1fX2YC6Xn1b7WfY0 +9jLJbeInlRXWkiGt35VH9YWlk98TCqA7PNxyI3Mwc4bnkfEjafAJPAK8WonPSlR8 +Nua6e1XEqMBeo96hcA8xuzI6vXAgcpUCAwEAAaOBnjCBmzAdBgNVHQ4EFgQUxO5g +H5XCLBK0sP4Tum21Zp8IFpcwHwYDVR0jBBgwFoAUxO5gH5XCLBK0sP4Tum21Zp8I +FpcwTgYDVR0RBEcwRYIcbm9kZTEuY2x1c3RlcjMuZWxhc3RpY3NlYXJjaKAlBgNV +BAOgHgwcbm9kZTEuY2x1c3RlcjMuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQCFKO57CPesXV0wGekL2d2qMclsCqitHVMmLgOazu7f +NSZ05opiwfKpbANwJmiUd5E+Svkwqxftn9/wJlhKSMGd7kgXx3MAS18U6whb1fEO +7lUVj+jRAaFY8x62ghV/PCK0J/jrKCvrYW+cayRBAHo0tS4bSw/P6tn9S+4tFWOd +Zpa2dfrG3q5wbJlMgpIqj/Rl3US/Z5ramUWY4IewcXES/Nv7M+w+P2DhuUz09moA +NooXais2eLMvhILWSJVQy8yTi+M5+Xi1raXwpSt86s108KgwNv1md2/jRDwEzDgi +QAqrOri+6VfPEeEi1mFJqzwYKuHFuiv2g0PUoGNzI9/V -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c3.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c3.key new file mode 100644 index 000000000000..c58c24a261bd --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c3.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAvFLygFNUFbpdFRvAyZoJS33Hwm8R42KaWFPezGlv5R9dcCZ1 +OXCa+cCJaK333QokTgNY1lNTrIjuTplseDPydS0UV2WKSYscrVAAH/1HEb7xJZTd +UueqbdJacfNetDpE1ie52rVB+bjzvlUe874o6bJUC44IW0NcXDOvpg5+ZdInyAUH +dA/LoAq6bteo7MUabMcxNbnkhteQYf/6UPde5X3L+Pnl5/uSv/F/ISLV9fZgLpef +VvtZ9jT2Mslt4ieVFdaSIa3flUf1haWT3xMKoDs83HIjczBzhueR8SNp8Ak8Arxa +ic9KVHw25rp7VcSowF6j3qFwDzG7Mjq9cCBylQIDAQABAoIBABe4QNf95tzXvMqg +VL9vudT60jLAp5mivTr70Vegvhm3Qh8SkPg7oSFs6UG4i7wY/2qeDE3ub98O3ikj +NODM9jBhDatWXD0YGvQPW/yre4yWJqxrnBbJnBM6B+9LTnXX0vzAQy3zX4xzCaxC +72OW43NP5TsRWCcajGMGlYyOTchIIO1X4kZj0hOdJxOgudjyWBLEXtHGvLnV1R50 +HoY7swwttb4+hG1hlUg9Ve/blu+ylQcj2twEmsbwQN5z+79ZwRG+JFApdGMiEkvu +hs31TvomNdg1n7GtWsEXLeSNTmuC1ew0wNxti65HIulHtiOxmlRDthClx6ckwYYl +B/KW5AECgYEA8eqADRBBdYrMKn3zDXJM4uLGMPt4fi/+uMbzjfsteVUk/qQXPyyE +2AP5j0tn/yzFUqlAlg1gyc9VVZ5cDWj6UpRewllCSolGRwrzGFb9Jw+qvuqMAspX +Jw+S5fhiTvG7scTgo2nbtIuTDjHTWhZO4ylACem9dCNf6tTUwjmO0gECgYEAx0m3 +K2ndvEYtKGjb4LnW6mu8kTHxLv5dMXk8joA9Mh/Kk727yUTSXnGkKgp/kDS+1sp0 ++s7mgXc2eIEMaENKfg0xOTQXJZEKLnC/VIxYqan9VWm0YpWuWOd6Kt8NtZnLzCy/ +uUJVLkxxuHjHk4UH8+W8unqAREccPai6aZIQOJUCgYAmM36nEXMUyac16o+wBuWn +SmI7p+o+GvwhV6FsQUbMCyr9XxIgsroDlpUZELJKdFpwlDOTvmcTVz9Fdu1YXMRy +t+VC0W+Lb6P+YfdDV7FAMtQms8kKQ8OBco8i65cymi+mgc1rYLMGDqhZLHsQlcc9 +Te1D43o8Vb22yocSaJ5qAQKBgFq+tEtub2tVwSZPaR/RP42K9VhgF2TAqormm65/ +sl3qoAHaOXIShoA+2vPRx/hQLD39/npeUBPPxtvkV1P6oCb5ttpHX+rZOnufF2BW +b+EB6LtF4L+rTLJq5PQ0kTeKCoS0M7EWBVeJs5a+WJJR1yc2C1QWh+WPQ8rH0+pS +elRpAoGAG3iu+AKV1kHAk+CxL9Dx/L5awRx1ZsfMA201UYVrXpV5CM5AXzlt2URV +ZsugHlA1oCOFbdFhjUCmeZiMikb0tCMZZHGv81G9n10BEtFor3vi1LNsI/+jXZSV +GBHonPDTsCVHWEbkSLVQKwqG/fTJN4sIXuSsf5kVYjkCEOXBKgg= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c4.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c4.crt index 3b7b3bdbf720..dded07f82572 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c4.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c4.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAMllmXfU0dH+YEE+7NFAwve6CVRiMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW4xLmM0MB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjEuYzQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAJVhOQxK4DAxYsFDcm42dl0DdpRCqxYFecP9QmC2cniv+sMUSRLsti8BeOKa -wlWL+NQK7CJhAQPuYnLEBiyhUIFz9dAVozHyOqBKFbf2L3A2nIPuom22UiJN79k2 -YXZgagSCdCdRX8WTvTRbbN8WYRfE/wLO4SBxwW2f501ET6pTAgMBAAGjfzB9MB0G -A1UdDgQWBBQIGCed24kXamJPxK4inO7BWteawjAfBgNVHSMEGDAWgBQIGCed24kX -amJPxK4inO7BWteawjAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMS5jbHVzdGVy -NC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAMNvY -Z5SwaZ484mzdmNvseZs8cxZA7EXeuOw5r9jRyeeYAT8aJFBBXgAYx4h8in9JbBnP -of/88YKTHuVYGmh9ad0wP7TruZQWF+siQZht2grar9sWo8XBO3FiUBHsEfCvSQKv -ppkkIu7YiXoBdpdpx+6+lYW14L+fgB7BrtRODHo= +MIIDTTCCAjWgAwIBAgIUU81CxPo5hUWGwt283nbyrHql0uswDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjEuYzQwHhcNMjIxMTI2MjEyMjU2WhcNNDIxMTIxMjEy +MjU2WjAQMQ4wDAYDVQQDEwVuMS5jNDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAOG6Z3WA1GJ/J8SivSFSMBJrb8LqYWrlfPl8YQsFwBS4XqewQJ/WhPS6 +QmrtP2dKFeSx4CzoaYLHjUpgZiJT2w+12d36ct8gnCvVolnI9upykhY/OpHblcA6 +Uy6cyQGY0XPW2n4hhFdMZm+RxG3whlMhOVB5U7zqJ0A2l5VXvl2s10d/mzq8VuZB +Jqjfpgjhs8hw+0CU6Ac0JuoIdO2ly774ltksbidGlvH4ONlMl9/scLJTwSx40sk/ +0rue45eRv8LQIXngMLZ546N2y6KtpxeUy4kk2nZ4h0xLo9xkvpb/Be96wICktVQL +YF0Z0yVY12HYA+5GRCVqw5ir4IDYMlkCAwEAAaOBnjCBmzAdBgNVHQ4EFgQUlyZi +9Z5kb9/9Pb1aE6VJhHL5GIMwHwYDVR0jBBgwFoAUlyZi9Z5kb9/9Pb1aE6VJhHL5 +GIMwTgYDVR0RBEcwRYIcbm9kZTEuY2x1c3RlcjQuZWxhc3RpY3NlYXJjaKAlBgNV +BAOgHgwcbm9kZTEuY2x1c3RlcjQuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQCLgXZPcA73Ohm7gjuXIPg1zSs7iVbdeth0bulbi1/g +zHfrpSnHU9+i66Qoft4D+Aw8Up2dwsQVfPAdpGQnQLcHB0c1WZtTNMu+o7/RMpGe +cOgKDy8M7MegROrlZA7U199x3lFaJm8Cbe0/Tc/WwqXJ9uN7tatUj0iJPGoe2NeR +T9Anq4qBBNOFhZ2332ItSh0B6dTnZ9vZdMRH5u+YbZBRGxmQ4xEhbLg9eDPeiGaB +blvCvnoBcZ1iH0iZYFA4nSGPf9376dS4caHvlCx4MRwXVgCOV8jNyx8eGy/4NPMg +/nDwIPvQ7wlitnAt/ixiMScT31SydRRvOXa6yEFGH9G+ -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c4.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c4.key new file mode 100644 index 000000000000..40778de1b613 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c4.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA4bpndYDUYn8nxKK9IVIwEmtvwuphauV8+XxhCwXAFLhep7BA +n9aE9LpCau0/Z0oV5LHgLOhpgseNSmBmIlPbD7XZ3fpy3yCcK9WiWcj26nKSFj86 +kduVwDpTLpzJAZjRc9bafiGEV0xmb5HEbfCGUyE5UHlTvOonQDaXlVe+XazXR3+b +OrxW5kEmqN+mCOGzyHD7QJToBzQm6gh07aXLvviW2SxuJ0aW8fg42UyX3+xwslPB +LHjSyT/Su57jl5G/wtAheeAwtnnjo3bLoq2nF5TLiSTadniHTEuj3GS+lv8F73rA +gKS1VAtgXRnTJVjXYdgD7kZEJWrDmKvggNgyWQIDAQABAoIBAAnSIFozvAnPr8fr +BKuEdeRMmaozTwBlnPsFzFBnHvpHzeZ3JqNNpASPGKtU8yM9TUKL8qLsNwWiRPrC +wu7PaIKAePV4G14AfzWbr9qULG9QLhxFrU4B7bv/+EqDi8NEBijcui4uqyFZM5kQ +ZdQarjLDs/NVCVhOH7MNglQpuHXpeusGK+g9VdlrUmAFmn11wQ/6ntSMvvtkwirh +ErR0LVKBDrh+9kT/iA18qUwxk5FtAVShbAESDQ9q+2sKD9I8Sbo9jdHiqc4ajUL2 +wdGAeFdENdH+AGb76UMwZp7Y6LVoOnVRabRn/sO8Yt1tdHCXQGCLSSomDBNZMJJi +odtBw9kCgYEA5O4erm9wX+UEZigMT9MWND04DKj9FSN+gS1s4RfXrSBY+Fh2TfiC +mw4Xly2VeARbjIcsiYQefu/GJ3HpjW2h5asZBvxZBXsPc3kqhQPi6KjfI36oPzhH +NKYrj3aFp5bL+fKgFl272XRHg1FLLZ0AI1Vf7RlkyCXlU2akUi4ckM8CgYEA/Gtb +ZQGIlwyf1J0UO8dH1ICJbJoGelnyUemRMzY2WOrI7cp7ODAo/BkVl7fJlMjHpozy +2NzmjmAlMsw9MKr5fe2kTbd78aR9OtFpWuNM0q/m8HKH4jPPQhcKydOCHW/OBgr9 +08/MQ0STzkdVkl/42S1cGmnuRfGFcgd788mURFcCgYAN0obNt3LMh3JljKN5qmAb +1VpuAUIOpa78s7ZwPNVL4bkdfZHfEVMpc4dfHGR686Bncr3wq6Z/uZB4Tztvy5/L +0zHWpyPk3dJIAZfcoihwLcoTRZF5B7fc6O+WYOye+s7kD4806oHuFkQ/HIhyf5lD +iOMa2L6qCwe2qmrff2NAJwKBgQDE1W66j/yUVTrZj/f0OYzwz57aOi2tATDt4SXh ++ndc1REEJO37Pcrn8yOcYKvnwr21SZohs/hxs9WuDAf3SIwvP73tAJS9NRiWYjA2 +uKLydEqw247MvLv0/ITkJ3QCQmo02iNfJV5SYkqkA4peD0q9CBsXWd19Wz5FUkK9 +M0bUFwKBgFC4Hfo4iDGJPg4hEZxYjHzFLZCayx2wioMrrXwzxPIQoSekUAZ1VW4g +d3ga/h5bzlhV72ByUNaODNznfTRsynEiLz5fWGon8k8+PQcgyOp74HULKXTpeeNz +fWUd3F+jydavZi2VeG8sNhBBeReZKQz+2q1VRAcMY5KEvUxBNaOG +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c5.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c5.crt index eb405cef94c5..ad238530aa4a 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c5.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c5.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAPqYnJv7Ib8hs7fwymtTbafjPHznMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW4xLmM1MB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjEuYzUwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBALGUvDdOnTe62apHl5sXV6Ys8GYqKDJ0e1cCQCKgxu3MyCueXC1xdhYJceA3 -PTmnTRg7KqYhJLZi1sujBOfuy7vsg5r/7L6EhWDCM/d2QfF9ZUft5ljsEGYNOmGa -kQnU+mFOuDe18hlp72tavC2tdPYHaTmd0t2f4J9ovxQznO+jAgMBAAGjfzB9MB0G -A1UdDgQWBBTgPsBAr0vFIhwmOhBkC1XvlAGtvjAfBgNVHSMEGDAWgBTgPsBAr0vF -IhwmOhBkC1XvlAGtvjAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMS5jbHVzdGVy -NS5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAFbiX -wn34AQsvyD0AQkMy0G6ZXH0UngtGfW6qyz0rFNNRtV7VnxYjZvuXKiZ4ixaHA+yv -heUYsReWoWzZAb4SYyxp4t5fuKxIF+diW+WelUV9FvTjD/Ynzp4jnxI2B1+2InNd -WFcODD+1pYaOoUoM4554Ir84GoybcvL2iWAJX84= +MIIDTjCCAjagAwIBAgIVAOT2bsITKOYGtEmGTQTFuQc7T0fYMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW4xLmM1MB4XDTIyMTEyNjIxMjI1NloXDTQyMTEyMTIx +MjI1NlowEDEOMAwGA1UEAxMFbjEuYzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDWDtiPIFavsh880iSg5HWxldIEAMO7AJkKxERF1evr892lfGwHn9YX +coUiTXgiNktRUv1wSfz4GEK0mNW6Gst068yn+167A4gsxXKUkrZiZyguQHxPji4Q +dcbx+7liBtKVl71IbbUUPLTxH9FcGRchq59hHjxi+lSTYAwOK11t1Lo+ycV9msGk +TJj1C519SAFaKMUzs//5de6arMzv4N6HBs69ahxbKUcfLJFAsuDhpzxfkrPUdBAa +dIQphnxJEmatUjMN1HTew8Z8cG4PmMPeWm7oXk1R1Bnm5EvXS3hXft2HMXNvKC8J +oGtbjVMOzvKf70ijg5v2wniwR9eTV9NxAgMBAAGjgZ4wgZswHQYDVR0OBBYEFPRS +GtVqDrC4WAsAjv/FKTQhE7yZMB8GA1UdIwQYMBaAFPRSGtVqDrC4WAsAjv/FKTQh +E7yZME4GA1UdEQRHMEWCHG5vZGUxLmNsdXN0ZXI1LmVsYXN0aWNzZWFyY2igJQYD +VQQDoB4MHG5vZGUxLmNsdXN0ZXI1LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAak0s3TOk0eS9ZBdDRXmUN7Oy5ZP3P/GbMUtqC7ho +Tkd7pF6tRbrx/wXuvcmpsgUVrjRFQWbx292SnkrMkEX4QkJV+sr9XLxWf8F/t7op +8aUcS43USf3DQ/TF+2WLQlcJOnN9I9QP+MBJx9m0xqKl3YbW6uxgcen3SgRs+c/K +5CBZXURTuUD3nOKiqouueVuTPULd0OuDzlK+WphjviP62SklM6m9QoYYp2aznYa9 +MMeXUQsmhzg876yHSGgngqSatc+AxfOntdWY58UJtpY1DHutqJp+HQvDWjLQAfd6 +xx68PdjrvzCBMPjyKytfqvULyUiqDOf0uMxe8ADRvZdVtA== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c5.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c5.key new file mode 100644 index 000000000000..29e389caa8ec --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c5.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA1g7YjyBWr7IfPNIkoOR1sZXSBADDuwCZCsRERdXr6/PdpXxs +B5/WF3KFIk14IjZLUVL9cEn8+BhCtJjVuhrLdOvMp/teuwOILMVylJK2YmcoLkB8 +T44uEHXG8fu5YgbSlZe9SG21FDy08R/RXBkXIaufYR48YvpUk2AMDitdbdS6PsnF +fZrBpEyY9QudfUgBWijFM7P/+XXumqzM7+DehwbOvWocWylHHyyRQLLg4ac8X5Kz +1HQQGnSEKYZ8SRJmrVIzDdR03sPGfHBuD5jD3lpu6F5NUdQZ5uRL10t4V37dhzFz +bygvCaBrW41TDs7yn+9Io4Ob9sJ4sEfXk1fTcQIDAQABAoIBACmhKoGvVtyFhii0 +b7YwqCj4y6bGpUo1hA4BfIueXmr9euInr/TNo3vc5EJ3EYGJ0J0IyEwxv3ZadYKc +5Jf0hRQAlcUnkFVLiwy1+6IkZD7/8+MG3IY2TwQZe67s9zE2xXadU40F9PzyD3iD +aRqBsKt2LqFbGYBgUdl1iPKqgVmN9dqJ0GxKoqlLdhLQiP1ptkpawjrYKKQpvxMM +vmqErkNKAYdhkVJbv5+QIPdGybAzh2vgfFuUu6xWKcMgy/96kVGqRw18egX7joQi +ACUz9bF2mp1WyN8MNeNzxaYRkvsVt5sfpKIEkbTV7PAWn6yvGZ0PgE5GZAFIH3ZK +9LSjI+kCgYEA9s9/o7ryCz/rPNksKZAfxqh4nKaotNMHI9vOKHKojnyRoir3NVY+ +YJWX9Zqg6pZaGQkDcQxxm0rumGRK1BiJjYL2BsiLklTKnWyr/oaOhR5Gz8l6ti/I +cq3bRIlpkUPVHSp8ewKRVSYUwKv5kQK9sVcls83hRYzhH+Za8sGBTh0CgYEA3gcp +sc0z5/RcZCrsvktIrGB2FudcY54sxB8X0RBN5iFo+c7k7xMybOnQsXk5YXDJXVEe +SP8VD3e+HkeYz6RdzyDJkHjVZyysp5i4WUGiRkYchJQrn3Omvq3uBYKAhFpww9SA +gw0b0qPPwt5YVvmflcfoCKHMrBW9TGhhthQsamUCgYEA2E1p87h3QFy4cQ9zoMlq +z9P+QGvfFvqLG2Yb+17hUl7h01oMZXY8HLF+CAWIWY+DTsWTepQJTKwTBwN+gPsw +MhFARRmlGI2vNOB0rBCI/ylVre1J2jluVvL/Y8V5+5GTSIhfp/7MF+/T6DfP9MKO +NMnHhRk+AJJDCo37zxy/khECgYBF4XVDC/YBVIzbzhLTVfiaoIWqcclHZVpro+mo +GHycKPnUXkT55k3hr7nKkf/4ZjpKj6R7jyV+x85j018KQ45vCb/HI/Du10XEPjIP +jbPYcGq7Y7NTLWBW9YFL5KY0gg6p5DFKjg6qvno+uCKPxWNwDgo+5UX5B6Kb8OKI +H1NXbQKBgQDtjEtI3/9q/ud7OCtID+ahuel5/zCAYrCrGKfiTDB2ulyuTjenV79H +dhXI590j55276cpb6Nxzm+6sTy7ZZWy97FQQgNx0Un6zYPqxxYERi1VtrVRfjATI +fwGGBhcEYeEs79ntN4xGi2u0TgZINR2OLbr5OSENelhNCTHeeebMIA== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c6.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c6.crt index cebc88f99790..4250e14a619e 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c6.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c6.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAPQ03RwYzZEGZE9BmLB3yibXdFlrMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW4xLmM2MB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjEuYzYwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAJGKOOMmMfmt+9VRZfYsorzOWLxWRtqD9VjRimbBVWBoIFYf+EVyEC0E/sg8 -G9eWAcSEUJ2oDp5dnMuXzEZrA3aaYeDZjtOYuHF+i+PFQQFlpY9A+NapdhJe9CU6 -cvL4ImRyatPxw4A6QLLetWHbDDIYA+6iYtmkKTkMlY6bboSzAgMBAAGjfzB9MB0G -A1UdDgQWBBRs7k4fmkFw8RmVU1f+p9SY5qNwnDAfBgNVHSMEGDAWgBRs7k4fmkFw -8RmVU1f+p9SY5qNwnDAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMS5jbHVzdGVy -Ni5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAEXhO -vittRGJEOl5Yt9pEmw1UJ/TRum2osbMj+3+7QGlCpU+GC6ziMXbwkPI+AksCySBX -mXZlgMK3j9oESC8K5CiWA8IpY2HcUJnKRFac6NIqYdbStVNAKJBMDvmyhORW2ejU -K2MBzTHUyJtCjFmIlFW0YzWFgMUJ1fqHWBwpOZ4= +MIIDTTCCAjWgAwIBAgIUBB/n0nN8jZBmOmWXXA1IHlR8F90wDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjEuYzYwHhcNMjIxMTI2MjEyMjU2WhcNNDIxMTIxMjEy +MjU2WjAQMQ4wDAYDVQQDEwVuMS5jNjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAKDzXqsQ7aKfuKTuRtruWq8lt68cLo9l7/9tVbXgUuuaHNBJoqv+gvRU +F/yGTuEccGJUfozY9aVNXW8DpQ9mJdcMNbqhjzAoSBB6cKvxyOMqR7WQ/C1lPunB +FboEv+VHok7KRG33V+NksMUrPhR58SqdEB/ZmmMxC5VvvX/PGjmlVpOHxbxWKD86 +Vg3prR5v/QkkINte4lSLSbFi4+JyjUpAphC4gElM/EtbGYPwWOR7H4lc38Y3qK4l +2gbB5VDRLUsjivrKsfPXg69j+hN8pIzeMViX7eraEyHfFExD/g8HGKV9o6oWl0yq +Ts6+HZMVg7bQzSgAz1Q7uuguyj9w5XcCAwEAAaOBnjCBmzAdBgNVHQ4EFgQUTc+9 +x9MZ7tk6jyBR6uVXyCZrquowHwYDVR0jBBgwFoAUTc+9x9MZ7tk6jyBR6uVXyCZr +quowTgYDVR0RBEcwRYIcbm9kZTEuY2x1c3RlcjYuZWxhc3RpY3NlYXJjaKAlBgNV +BAOgHgwcbm9kZTEuY2x1c3RlcjYuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQA0IuVSgXSUrYWvHL+gLy7GXTrmbp+VjjqtT0DAh71X +gvyNFxmSJ+3T6pHC/QkUxKgxOvOg8yXEPbNnJxDkbEqhYm5p/UDsQC59Ra6zyI9g +p8KMsgGISBvYjottv7nmNFx9dJNQG80IPvT3b6tcHIpzpaHnlTrdF5XOlV/vpJV3 +FCXreCuqFQyUvUCOEO/tX/1OD48eg72u3gqZbr8GbLZR5OiZKrGqCYY7+DzCuxM1 +emNs0mYeX/KunXz4dfRmG1EkYaKQKLd9R2vQDRUnSFJuAGv2q7JwxbCN+9xGZnyP +rLEK8qG99Tp5pAR7ak4dCtLbzM5LIzQFihp7HT5aNcud -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c6.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c6.key new file mode 100644 index 000000000000..f21807aad5af --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c6.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAoPNeqxDtop+4pO5G2u5aryW3rxwuj2Xv/21VteBS65oc0Emi +q/6C9FQX/IZO4RxwYlR+jNj1pU1dbwOlD2Yl1ww1uqGPMChIEHpwq/HI4ypHtZD8 +LWU+6cEVugS/5UeiTspEbfdX42SwxSs+FHnxKp0QH9maYzELlW+9f88aOaVWk4fF +vFYoPzpWDemtHm/9CSQg217iVItJsWLj4nKNSkCmELiASUz8S1sZg/BY5HsfiVzf +xjeoriXaBsHlUNEtSyOK+sqx89eDr2P6E3ykjN4xWJft6toTId8UTEP+DwcYpX2j +qhaXTKpOzr4dkxWDttDNKADPVDu66C7KP3DldwIDAQABAoIBAAgmCibzBnTzTS/c +eG3hH2o6qKAO9zR/XrU53dsFbm1LMiZBdCDtE8g28bfxPNEc1XvE91s+KMGjtxVF +w/eRnlzS222FfHBdubhL2qR560fE/jhtDLdnptXC1szCLMz44XjHuMsweJyknmZ9 +F/mFavZTWVujcrOO6h04uQldfOBqL/LOj8+9/pSwGtBs/dQFh/rIcx+NgUuSIFbg +bCuqJjMLoyVHbdVz3Ov+wYZeL+Ed9aVAtEIKAH8BdDRKmQ8hiO5XvlMVc/uGLeH0 +YlEV7tNTb0bSeD1dxpYnVR2ZvlWDLwQS8T11fWqI/Irlqw26NfKmJO4IwDEO+YzN +IiINrxECgYEAwgyZ1b+p0oHN2rJhurouGxNzvxH0sMTjufDppwZvJLC4vZ6VZsWc +z2VfJhcE7Q2DxgINPCQWpmce5i74ajk7+VRQbd17YnZQU2umZ/rwtvfI6uOXMJIZ +pkP3skjk6zsTOez5xUKYwFKfHeR2qgqQ5z1Ztn88sHvw0r1nPYHRY/MCgYEA1FWq +TVItASAhEBI/tdLyabpmoQ25wIX6R02buOr06fk3MHAWNMJcgrShQPTjiElSfO0g +68sAQQD5byf3XGfHMdAx9nkkLQD80YIPpH/qN9zRLZflf8orDpbF9DoVyAuKlwYi +3F4FuBzi/MR0+fvYHi7FqippNu6T2TdhUxdyDW0CgYBtdkA9FD4O0kw2JtP8SlQB +0ApF6TIXmSlA1YZoQrflgY7k4BU1ARpA39U5nliYBDymMLgYid8BBUCn5i0Zdqm/ +LvGj2AFm/Ii5VgtrsmGhRMtgvPunWXMAmGycdIQ1sYBZ97S5sqSKMNEzQpIxv34Y +A0KdMM8iWvfxWQho6NceQwKBgQC4Zz1jsT5Mf+622mJiMjU9BRkkPZtYTAKz8f6P +dd4FZN49PtrKLxHQobZ5hzQjpCnzPK0emko6KfJ2SqtUoPwpkroRZ9u3CeiGuwGC +x2RN54PyqLwzYwxJuz9nfxpzhTJzPm0AHNeyscOf6a39fG6Qp+YahoKhpkjYmHP5 +iZ8PcQKBgQCGZ9R+gkboujCDNsD3kgjLQgRuFuomF4BXLx5ZSUH3cINCBr+CC4f7 +gEk1j5wxNx9ntXWjVeLQtLszccFX2x6cXcquF1UH0J7buWgExg08b+ZfZ+VrhrMJ +OT7bh6kzQkH8HyOtWlUecJsMdgP5gJfVHZfUkOjOjZ89Byj492HQUQ== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c7.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c7.crt index 08c2d4e7af7b..1201b1a904b3 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c7.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c7.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUccM78mwTgQTTvYEwDeDeYmTLBRkwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjEuYzcwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0MTMy -MjE0WjAQMQ4wDAYDVQQDEwVuMS5jNzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEA0dKhFm8ywJ2tBETCUsdHzEVE+uVb+oLVVxswOTBJ28U6s+H/KSk+f4iQqtND -+Q4EI6u49KHvH3/koHgRuQwmcij5dCUuAtsBR4wZJLXUgqlBNLtLqejLF17vFTRn -i78xiTQ9L5CBioNTYTGhjxQ3tUbFCD3NLPLd25uVNR9Vc9sCAwEAAaN/MH0wHQYD -VR0OBBYEFPGksbL4MBmo+fWjS5FugVp5PQPyMB8GA1UdIwQYMBaAFPGksbL4MBmo -+fWjS5FugVp5PQPyMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGUxLmNsdXN0ZXI3 -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQBfWuwf -la/PtbA9kFYzzHozSWubOKUoSTzT4yyWYMrONU6rDll/x+nRUujrko3ecGcQHvns -qvfLRxifj+TS0SQmGn2edgkfhNBke/rYfGA3CzuTmlqOiGdIz3onNEf/W97mfNu0 -ssOitDOTm3FipxkxvPlvfIOBuA4vUUX4MlfPaA== +MIIDTjCCAjagAwIBAgIVAMjbD93GO6JCsPWwTeWoG8PeBC+HMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW4xLmM3MB4XDTIyMTEyNjIxMjI1NloXDTQyMTEyMTIx +MjI1NlowEDEOMAwGA1UEAxMFbjEuYzcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCZ3K6CqfRGpCWj3is7VzxG77lNl0Brjbiazgtd1nNNWwGcf8MX2qwr +mnuZ5tXlwSVt4EtpuAZvg3WZbuu022sT8jElLATaAtU6G8iR5jZd5Xudko3Vc3aC +3XQJtGDyZWW7r5v0YeJrGFZwQcrKYNdgjcdiGD78hTe7BTqJ+s/6UOKaej4H+c9y +erX52DSIJwkKBdMbGxhkBQlZgwlTiiiUuy96ZaJg5qiurq/D9iSCZhaVxDvYEvzI +1UD/oZGOeKDH/8GSBAFxiaGR666asGTy9fD2nGECISS2thjDN4ZDCuk+l6oUUhrE +OfSv/1DDfl1pscf9q1MZ2/jRr7BzbTKBAgMBAAGjgZ4wgZswHQYDVR0OBBYEFHTP +8Vh2iWOK+DPOmQb35601GP8IMB8GA1UdIwQYMBaAFHTP8Vh2iWOK+DPOmQb35601 +GP8IME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGUxLmNsdXN0ZXI3LmVsYXN0aWNz +ZWFyY2iCHG5vZGUxLmNsdXN0ZXI3LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEADxOvuT37Z+4r57P5++++ZtD7L3L/WGgjqChdi6YY +Dw7ON53MDnTlSEy3XhK4BZ6SY5pJdN6/q6dFxScHxjC3xsUVTZD582n8JmTmTDu5 +xcJ3mHTTZj99PcpGO5n9h2qKH5T7Nef3/OtGQGYzWGxbYe7wDqpMvPUM9iPiNiDH +LOPDBSuUCJm5c+ZW/vRcjUapdE+VOec2vqT/c27jmMgEa2A92c8EW7YSn4PJvt6A +sdi7OQPOFFXiyFlcP28+uatNpCfHvzzd/jDuu3As/gyH60f2bNbsYm/T4rDaepdU +T4OcJRrN8kUR82Lb2agYi5gdqBVGxETwdthBtyIVUcmW4g== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c7.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c7.key new file mode 100644 index 000000000000..f68c2be6664a --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c7.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAmdyugqn0RqQlo94rO1c8Ru+5TZdAa424ms4LXdZzTVsBnH/D +F9qsK5p7mebV5cElbeBLabgGb4N1mW7rtNtrE/IxJSwE2gLVOhvIkeY2XeV7nZKN +1XN2gt10CbRg8mVlu6+b9GHiaxhWcEHKymDXYI3HYhg+/IU3uwU6ifrP+lDimno+ +B/nPcnq1+dg0iCcJCgXTGxsYZAUJWYMJU4oolLsvemWiYOaorq6vw/YkgmYWlcQ7 +2BL8yNVA/6GRjnigx//BkgQBcYmhkeuumrBk8vXw9pxhAiEktrYYwzeGQwrpPpeq +FFIaxDn0r/9Qw35dabHH/atTGdv40a+wc20ygQIDAQABAoIBADAMqVj332fSKkkP +ADxDZXkXsyxGgGE2PIDnuMgAyThOo+WuRoxkB2GEmHX7DzB1jY1PPu3QgP4BNVAJ +wkVUQo/TkkJ0CT3WJaXEeHILzU2sdVrDolSW/tsmcm73bFE2ljszP8ZlbTKZwqDD +AyQJcyldjXcWcFEm/z0CCIJ/l8SpUrvyKgeyncXYf1dbKcYKSTnzo+UPDQaNtAPW +qKXXnByF74qY8hawo5cykp9HlW905iEXE2aVZMu9lCR9cZVOX9JWJKFOuQET2j8Q +Tr6hkzHjkLcdB7fiD5EDAg3Ej1tasJ7qhthZZ7ytrK5BIlQwWFHXhrVSeeYMNBbR +Rnw7tesCgYEA0dwWsIdRqT0B/1RS/+ShNzXwtfNrocizBP6YrI4MNeliiabwNYyP +BsPfWw1m12lOCLY4E3JxiQ/xmwwRVvsH4WEDKONYhF+ZRo+Hl4I1JPaujz043lLV +IMdx+l+zrE8/lOjwxkKDY08Q+QMjqOLU2qMcM+7PtzbVXWIagO+WxMcCgYEAu7DG +YXTdsPA0KOFEEtUqzmq+9xvUCU7xMjKEltdJugFbz8DT7C4y3gPf1nm6xvJTpHQx +YrVcXT/eBvod29d0XGc0vxCBRPK1JMfoctgo+Q7TR2X5cuzqUgKb8cy1PRZl0a9D +8K+TVsL5mO4FpRjCa8CAPrKJ1vtSYWKFA9E8dncCgYEAvLjnzqhiT4eFnFAAtA6u +zC+94GKfkEYkd4FdWxAC+xh+fLubmQxmMvdCjWdn7J/OhRcZJqPsZg3ogD5B+a6W +T6Amb0bD+4bdGA9LKHz3vd347OdplMV41JmHbllSAsrhqtAwdpqp/UzFPqoMD3yt +XlEAs9M3CNRO3EO1Ae5z71ECgYBGrkOG3+O8hjieTHa+QNhl830GwcwP1VKvfKbB +x0FUcr6Idi1Ye+1b/kmFE0JrOUaWX8tZSMMHus16AybLa0++nH8YlLaZqGze9Qls +dkq3wAWAlb5isSVszN69KEH4Rz8Av+ft87Cgs7wbA0fM7I4K1et1sPaHpw1W7yr4 +TQtf8QKBgQC7v4jANltbKnXLZ08fAZXHLfiosSw+a6OXVwZazr8oL5/y3DZeHirt +oOrNrLw6KKseLy0TxcbpU0LhFyxhrssefSSMQ0g+uCMW7L671e0jxhvbCwUNemZb +6OEtEOL7xoyJfVDcD77aOpXYpIXLauwoYumUwM5yMMcnG306yCrsXQ== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c8.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c8.crt index feba2f614aa2..69ee5e9852c4 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c8.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c8.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAMUWjLPS533+7BzHoMpXAZQIJc6xMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW4xLmM4MB4XDTE4MDQxOTEzMjIxNVoXDTQ1MDkwNDEz -MjIxNVowEDEOMAwGA1UEAxMFbjEuYzgwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBANBMTFKHFmdHOxdIKRfUV8NVKIJe/T8QSiHNiXe2HA1TQ1qp8jFjXLnnoeWw -aDNUqz7dKtjw5Xf66QXjqAamakTHzbtbdifoo+276v3of+9ddYYjjm6n3lnvK7zi -nLjLj9l8kjuEp0rTgqUU26VicJVs76dlHsA0OxShdbbdAWJ7AgMBAAGjfzB9MB0G -A1UdDgQWBBTGQzXKW5ORg+cp8KQkDmOlQzOIEjAfBgNVHSMEGDAWgBTGQzXKW5OR -g+cp8KQkDmOlQzOIEjAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMS5jbHVzdGVy -OC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAmbRL -wgD19qMd5bFbK1sYiasX4DAN04U2Bx21FRbuOXA03vGT+FVnVLnbkJumtIqBCGCZ -sebCxLEhE/fmSX12hKQnqFR/1EMKvI5bps2996M6NeMTmdGCfJECBIuMN22DHIcc -5Ek5UCEQJM/L7tQpdWBRl3q9ca4x8HeHLlDYSvc= +MIIDTjCCAjagAwIBAgIVAKN8FU4SXaTfBwYMLhpElFUOF6OUMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW4xLmM4MB4XDTIyMTEyNjIxMjI1NloXDTQyMTEyMTIx +MjI1NlowEDEOMAwGA1UEAxMFbjEuYzgwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQC5WPnWSCxq0yjLmWa0z/3Ud7vRtyPmNIpQNnAgO3ct7chNN/C9oVkF +xVUTnVZBDBYpVvelFGIuWpM+aRcEm7Nf/2NWUc74Ik76n3tZ1U4xuVRfILLvu7Eb +OusH1bro6K5yfz33AJ+iXrnyovMYW3wZ1pt8a/SLplogLijn4/rlhcamCZdNfz4h +FEZWJ0MK/8SYwqAmBvmNnk2QLbp31qse/U9LEUQ94BcnozAb2vNkAHOf9NYuvskR +jKKJU1KI7QX5JEFCuFQKKKWOPgcA17fNh83qs9QcZOQqWtAR8/cCpsFm/3r0SDGf +IglezQ/IIqHMj6IHKlISqYck7W/q1nFjAgMBAAGjgZ4wgZswHQYDVR0OBBYEFHKw +SgT+VNu7YKA9acIRNJOkKXZ7MB8GA1UdIwQYMBaAFHKwSgT+VNu7YKA9acIRNJOk +KXZ7ME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGUxLmNsdXN0ZXI4LmVsYXN0aWNz +ZWFyY2iCHG5vZGUxLmNsdXN0ZXI4LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEACaRtc3Urh84Lqb3kZ1eacjxOK8KNFa8O1gDIZLHS +c2pRnWfs7oRxoD59TBPp9e69lQaNVwynj1Jcp7qYZUrSEi7UMRR5AUzsP7gY76I8 +zTkzp7aNAKbbg/Y1c+jMYDhsWPgpwp1tC0t/VhiD3BbmT5hBjveFTQaBq3laXCYG +lClJ/5XzdBEEZ/Il06OA2e88jaxRVXXNWkeNYXKWBIr9YioIeYsYKDqtU+V6sa6u +YQfR1RF8wGWjGBbST6WZlGS6wFYoTunS9hl7zWPLqmMPocQrkT+NIdKxk/qk0+zC +RKz4G417FTsFh+ViD+Zk7EHb0m0sK5aRMGpYJ/TCveCuGA== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c8.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c8.key new file mode 100644 index 000000000000..49da22c6359f --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n1.c8.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAuVj51kgsatMoy5lmtM/91He70bcj5jSKUDZwIDt3Le3ITTfw +vaFZBcVVE51WQQwWKVb3pRRiLlqTPmkXBJuzX/9jVlHO+CJO+p97WdVOMblUXyCy +77uxGzrrB9W66Oiucn899wCfol658qLzGFt8GdabfGv0i6ZaIC4o5+P65YXGpgmX +TX8+IRRGVidDCv/EmMKgJgb5jZ5NkC26d9arHv1PSxFEPeAXJ6MwG9rzZABzn/TW +Lr7JEYyiiVNSiO0F+SRBQrhUCiiljj4HANe3zYfN6rPUHGTkKlrQEfP3AqbBZv96 +9EgxnyIJXs0PyCKhzI+iBypSEqmHJO1v6tZxYwIDAQABAoIBAFhA3xlinU+yQuML +JIXk4YqZBoP5jazwCIngGeynE/z5E159S/KnUJG4X+WqjacEX8sIX3uppIRI2YKm +nHCzyKnlm4G/C2tIPs4EQUJJmGWBxwRSuBEBkHeUStQct1ZucJ4RK0qg7+Hq8+Ru +JY1MBl/xOXD5oGQcbh6TjCO6gL1ncJomz2vM9qewMftn5KaufU2xXl8sRxtLsfAz +7Z++VxL2c+257nJascYkU/z8pI1AivnM4EPTf6+oCEV8Q7M0/iKbx6QVmVL4oxko +8xaOctIuHA1lniDw4n8H+jjk44udD8UxZ8rw7YUPtLlWv2XGlm2Z1ENSvDHdzT9e +2v7QXnUCgYEAxrxhFWM7lA0pjYZPXwxbvXQgWHbd3sZxNrrtQJcDPFrsmQcsYjSl +Z5FmAon4L3jgVmwpTcJkVLaC+MPik4eVXHDxnAuHiATxlQkMZ+bsuyTpLxtW5Cit ++06UvMekctLC9Si0dcNhYGlHVqRwfaDFhWVmTOR3RPBAkAOopp8gCRUCgYEA7sEE +lcNTlHKSe1IDHYbIpjYEHuY22PrUAOSihhigrZESK0TNPhyhOgzKH9i8Xyt+ryrp +Y8FXYMa4uep73VvuFQc0LnfpXkVcKTgZbk9Oo1klongDT9cbryRlpKHBwMzJ2bx+ +MekZ5+441DcMsqs61ZQ8BpiqDkpRHcatvs1GPpcCgYBLTdw3n3dW3Tnuj4MJ0AP/ +IL6U5s2heW4yv9fgjphCSW23epldyPcX7GdL9P2e912CQc/Q0CT2WIX+PzNelZSK +B531dATyDHWB9IDH6h+vtgfJusFNCQEL5bXEZp2wiJ4pCTiAGv2N67aFZR/e6omN +x2NhJXzwGV1NdKV2JXudFQKBgDY3OnVPr5EMG1tJmk+AJdwoc/MYT+ghvTaetJFC +0mnXgYq1yu2JzsrB0Lw+mTf7Rn88fVcbtl3Rgm0lZA82KvA3fI/4xtOTaIQrqwqz +k6XgPn2QRyBANpck6rfhy3+egU+iT/3BcxKJs/nsrkN8QpXxXVgaHrO0CIt8bEPh +lUvtAoGBAL7QLM80kA3UKRsMqGEMGh9PtP4cr5bDmw1Yd2hdqARIPY5k5KO0O+R9 +6/b6exAm1I1QTXz3yKSfVi/dBgTFpueEG1YGO0IxS9jRvQqfzFrhf6LgeDhV+3/o +FBZACFarJ4U2LTbBCO24SrIbmUBrcwa1CpC7yS4iykhE4fUalFL/ +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c1.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c1.crt index be43ffd28eb4..6b5b3801fba0 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c1.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c1.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUGY+OrC0XCiA+1oCJX+CHvv7UH9QwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjIuYzEwHhcNMTgwNDE5MTMyMjEzWhcNNDUwOTA0MTMy -MjEzWjAQMQ4wDAYDVQQDEwVuMi5jMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAm8aLIeL8o3DodD9VWa0KvotRwSUtWX+qzAFgM5SqHHymlDnwRJq6gutqnCDC -os6TGihA6bnlVw4eRnjq8usw7cyJW2iIIFPr4d36DN2Ui/NGdICP4RMClrgf5sC+ -GE1jln0W7Jj3f76jB6HE3BbU6PCHdlAtgVcQ4j59y9dchI8CAwEAAaN/MH0wHQYD -VR0OBBYEFOMdmsBGJx8YHfiziaJxU93Fc6C5MB8GA1UdIwQYMBaAFOMdmsBGJx8Y -HfiziaJxU93Fc6C5MDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGUyLmNsdXN0ZXIx -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQBqgc5U -S4wYk1VmWTI+8ezAgtlnsiILn70rfsi0KjI1hzo8PvTWLIVvV7NBefMr34ugqstL -5by7pBAmKVf8Ut8148Jpbd48qxOLdwQVZ/Cthho+5u8i/eFmAJbATHkmyEj4Pfp2 -u9/QIikh8Gcm5sWf4CyC/7tDISxVHm1FsM3nNA== +MIIDTjCCAjagAwIBAgIVANHgGmrRMgAIpk80q1mNdbLG3L6wMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW4yLmMxMB4XDTIyMTEyNjIxMjI1NloXDTQyMTEyMTIx +MjI1NlowEDEOMAwGA1UEAxMFbjIuYzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCmijIm44nzN9i2+hEVqhgkIblphxe/5C4hYf2mRjKzo/BiZO8iiL+E +ayXF7xz7pRcwaZRCnxQWljeB+JTereVQEHNVSg0MAljWe1W9dFU088R7XVMjnXYW +4W8s0++A94BMj+qG9CZHOox2qIQaSAlutrNjvtxjWmnCoaghnKLAFTUsRihP3Vb9 +1C9eWLS5O5lgjEBHQSxnz1NhzqPT/WcjnD9a12iJfZq8zikWTRwo+wojOQ4hOTkL +9+7jjdlrIns89UXfykYV3HySdNu/Sk5dJ9y2bGZ2IwElpeW1C6qv3KYza251C4E2 +6WVCpyKw6A99fqa0j3HD3jpsdOYbR8FtAgMBAAGjgZ4wgZswHQYDVR0OBBYEFHLC +6+syAACyhQrRBjIn8UDNEQvSMB8GA1UdIwQYMBaAFHLC6+syAACyhQrRBjIn8UDN +EQvSME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGUyLmNsdXN0ZXIxLmVsYXN0aWNz +ZWFyY2iCHG5vZGUyLmNsdXN0ZXIxLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAMxH/Ravx0TxZovtApi5+UewvdUhQF815B6qt/wGs +Wlb9/WsBFNbzjN1xLJQZiUEcRn+jF9dX4WSN0WaUue9bxoTWovW3yNQXBXvwSuN4 +7VHwDz3BjLhtE1HjEr31WjScM8rHWMStgu1LZQFx2LODlPlbgYn/JcGNE0lEahKY +Aiwuk2BN4sFiOCSD/Q4+eLksbmR7sdBoqEPOQji9xyYgdMsd2RAeqx88JV9u65/O +PYo7dqZjwGfDvYvjUA4JeR11/Z5xXPAXBUBZaW/bSQDQ7cU7j7xGhWDmXT1m/Q2c +sANccCjZ1wE1YZGE3JP38RDwL22BUPHuh+N1Rqw3H8fwgg== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c1.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c1.key new file mode 100644 index 000000000000..10865550f4db --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEApooyJuOJ8zfYtvoRFaoYJCG5aYcXv+QuIWH9pkYys6PwYmTv +Ioi/hGslxe8c+6UXMGmUQp8UFpY3gfiU3q3lUBBzVUoNDAJY1ntVvXRVNPPEe11T +I512FuFvLNPvgPeATI/qhvQmRzqMdqiEGkgJbrazY77cY1ppwqGoIZyiwBU1LEYo +T91W/dQvXli0uTuZYIxAR0EsZ89TYc6j0/1nI5w/WtdoiX2avM4pFk0cKPsKIzkO +ITk5C/fu443ZayJ7PPVF38pGFdx8knTbv0pOXSfctmxmdiMBJaXltQuqr9ymM2tu +dQuBNullQqcisOgPfX6mtI9xw946bHTmG0fBbQIDAQABAoIBABKSSB/ykUxcTrAu +6C9SicEjup2kOTeGnkdOOl8rplVGg5SxXUJsXfEKEssFDGYahaC6MVSRkUeohjWZ +7x365d69CuN4r8ZGT7pRRCEtox/VzDQWluIamdPY3VEyLBvlXbsL9HMKsGcShh3o +Ulzs6hJc0Nnfa4uvxin0ZX+kDDkfpfV0L4n1FpNRP7VA6oPcvXjb0T9+GMUKBmlK +vH+f3qeaH3HnC+bOEeTMCUUoT6HdZFVwD2WMBDpRgE6M1TWeDNLOTojmd56eFSor +b/6ti+BDubFa1vtU8xkfTwzKQEEi9q/OsGTCdPE0AGAPjKuDJkYne0HVlkdIDAt1 +tnP7BMECgYEA53ZJ4OhCbjR7mlBrmcnI2tACau3je+jmsKukPoS3aNfiPfzvgFna +D4cjDkCwq8cPR60YDTW53cw45EOskUkMgglIG8roUmhXrsbDMxiVYJiscvb27qe6 +6uo6ntycM7DtnnfznqHz2bHws0uij/sPXFiCsC4D02pEggJAYueRJC0CgYEAuDH3 +GEtpQTPo/O66hdeyzPDFKyr1Rs8LZ0za0396URZt3A9lzIk1cOPcXuFKVq7BiLjl +0b2xBI7pja7yQMCnELPZ9FTK9GbVBNovAQQuEMtV31HRNuRGmgadwZDVpICYJa5o +FebWIDA60Xii3pAyXR7PaMmdGlAqkiv8GfxlGkECgYEA2nC+RG2t1Aff5sCi696N +mkPZJEUh/VzN6knylbX2yhGDyPCer0kdDK6CnP7cPSF+AC0Rl9Xuda44uuRLRknk +bgyIawJRwwQKTD31FpHK2AD+Lvl4Db9qHi2z6R1WpUoC1x1pY3qAgiD2gp+M6ckM +tCAsJk0m+y09Pl0VmsHkLnkCgYEAk95ow3Qmt1V5ATSVQMV8oNHZN2JxLbshKHim +Y4u/IkGjSDqEOsMbx9jlHLY3+4anj57/wHVMGqCwFjIQALOoatw3aNJ5a+Lxt8eA +XIyCfrD96x2/mxV/KD+PfclL+ZfSjM2Keaqq04Rkc60o8wtHo0IMt87C9FDPHDF7 +XVElyQECgYATbD2NsseqHG65WuxgPHW86kfBaTdoX/WR5JShyzowCoNMhovnaU+S +pG41rQ4LRxKPLNZcD5VZ1Y8h0XiWEemmQ297aOx2prUMiyYLELaSZCzs4//seA/S +4PLhR/M/kPJ/ch9pqhDoO7QrdXBKG4d16tOtiwwayq9PzrPmi+rDXw== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c2.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c2.crt index f77708c26628..e77c575cff92 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c2.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c2.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAI/exdUNVY6z8nhKNGOdfOBbf0KFMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW4yLmMyMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjIuYzIwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAMA9aZW2sNBxgc19xYUSmSG8/kZ3DHYcB02RS63y20ZvNwBIbzMoawQwD9Z8 -gfqf9zJ7JPnmkyvhaPygY3WpGzC30vxMG/XG6/pPWW0yxlyL1C60ann1r4BTdRbv -ms4zaw6h0v6oePN9/oZ6sgjNExBBsZEV9UyL63W0LEcPI+9TAgMBAAGjfzB9MB0G -A1UdDgQWBBS8HSfCvdf16vHBBjmbzPf+BtYLzzAfBgNVHSMEGDAWgBS8HSfCvdf1 -6vHBBjmbzPf+BtYLzzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMi5jbHVzdGVy -Mi5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAIAYW -v7RbhTCsN6uXSIR94OHlbQCejHT2WBaJNecKNRXc1aQb3qUDiNkjp5TX/bokHhlb -9lXlWP0CtX/w93PLgwQ/YG4HP+y18kPGvbm6s4E5hM5eLVPOj2LBewmOTYyJQivd -6XLTaFmQnsMx/X/YMZhhVvymzM2HV7117aAR4Dc= +MIIDTjCCAjagAwIBAgIVAKIsbqGxRYFv8xbGfaCCFvVawEFpMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW4yLmMyMB4XDTIyMTEyNjIxMjI1NloXDTQyMTEyMTIx +MjI1NlowEDEOMAwGA1UEAxMFbjIuYzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDhDACgCB/IUiLQX3sA72b4G90qkJMjVEpwv8kEr6PgLJe6s7doND9a +FA3ieSxKD6IMs//EtRgXje560Q9Koep659ybBzu+YfUnEz2SWLr74l+i/+rBgPSr +L9wRrR9kD3E31F1eIxZqqyKFX36Q2n5SM4iTqCh1CpO/9FeMG1FUqKetXaIfu2+D +GrPOJw49IXXNiLqORrL9Vw1C7x9yAYzqPcGyeb1UQD/O4HCRtaF77wnvgDgfUdnO +QKG+u4rGMLNqxHzyuJj+em5hWzaj1gCh5fJfwEb+L4qKKdJkU2v1zHU4Rlx0JCL5 +BtZ1ku0vEqIImAMLUN4ltWrVXOFJ1QozAgMBAAGjgZ4wgZswHQYDVR0OBBYEFGTF +ctJNLIn2CiBsSWeD55xzf+i2MB8GA1UdIwQYMBaAFGTFctJNLIn2CiBsSWeD55xz +f+i2ME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGUyLmNsdXN0ZXIyLmVsYXN0aWNz +ZWFyY2iCHG5vZGUyLmNsdXN0ZXIyLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEA1/VPtBHI7VU2CmgMImtG5VCQSRariQZlgQeyu+pr +BOkK0CAehOw1Nyg+RRQItnRft/KzMCGdbl06r2pysf3ZixUct3lhvxikDCb3DE7O +zybADmuemM3UvO2iKYNoa6OnI68Km5smfkVca1WGXgh/Uu/uMuiH8r7dTZcCm2dJ +BwRN8k2nO7SUEx3ccuO2kShEvdZfZ4IG58hdy6x8aoINbERe15wqEYGdMW0LYr5L +IhMJHD3aVbNnyQq2vkZF3gUdwZ2/Ep+R3DvIrbKzgG+5UYnl3pC/LX3WkkPGIOwQ +A1wTzkPsuguJEHmywALQLyfwQNicaToeFM1Ka5hzH3UzJA== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c2.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c2.key new file mode 100644 index 000000000000..a49aa7400efa --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA4QwAoAgfyFIi0F97AO9m+BvdKpCTI1RKcL/JBK+j4CyXurO3 +aDQ/WhQN4nksSg+iDLP/xLUYF43uetEPSqHqeufcmwc7vmH1JxM9kli6++Jfov/q +wYD0qy/cEa0fZA9xN9RdXiMWaqsihV9+kNp+UjOIk6godQqTv/RXjBtRVKinrV2i +H7tvgxqzzicOPSF1zYi6jkay/VcNQu8fcgGM6j3Bsnm9VEA/zuBwkbWhe+8J74A4 +H1HZzkChvruKxjCzasR88riY/npuYVs2o9YAoeXyX8BG/i+KiinSZFNr9cx1OEZc +dCQi+QbWdZLtLxKiCJgDC1DeJbVq1VzhSdUKMwIDAQABAoIBADmiaUaSzhDveBsF +WOsUZLqF6E5OWGBj5eyaMVER6lYvFWyrOf1l5BOdrzCg98a/2jCGqKon9tORddHe +WJw0e2iemNDbOWG4lKA9PTeNodWAVB3ajLSh4gc1apbcT33Atq4lpQPG2KbYzS57 +Fl7ilhdXO1MVbccLvmsamJE5LcsQK264kN+OSvJfuf0qMiSkDgcYGn/xze5Te0VH +VbdlVM/MAkJER/iH6mHroog1/tQR/vodEY/LKI6mZbQrajhSqdNlUSdvIbQEYJc3 +aboHXDuR6wGkT7l5o+/S9/PTKaKxkoj5jLZnSqB2LKqHUMwNS6dssNPxOO8V8xkP +82PRywECgYEA8hoFx6qMzP8ADKw3kFSlOxWiq7+Np+Zi240iw0CJpC25u0XJfvAh +WL0rNPJ7Wm3JKOilMmJX6LO/Df+8/lDUcnbyvOp8QWOpRVuINHtJDTn4/OLkJIDW +rLED6loJlmC6HDHKB4rVNrROJYhJw/nzWMcDtT5LkutGlugfQ4zcz8sCgYEA7fdW +4iSpOkzJr7iwiiDDsWWQIXnaQ/MO5HWTVKW1OLQ0rhuRGCAI5PeA8KR3bLpt0yz8 +han9uBeX9mwAxQTnkaNMbw9O/ns+LT0ZhazOA7ZKkhu92kl3p8VGfUpNSiUhJ5Fn +vtOHCW9qpDnarmcuDiBDJ/b28WqGtey0w0NbkjkCgYBUTbV3iq8O7zjwMMQEcUU8 +JtOHZOOJ9NK1yxaw0PddAUMssCdzGWEzSmQSQuGrYpFqumBUBCTCdlzwHf6MvE7O +MRdjq29rQa3+5dmvFMMiZO6L+OIix+55IgbQA1xVTgdgrAHC0JhgPTb9INE2+gHA +B0UGRtQALAdfoyLH3fRCMwKBgD1CSfyHYYUflWAGFHRKgPYe5h6oBrctT+0iE15c +Fi3VviX/9LcYMBH5YRkRzaiQyYrubsdd/nGwqS8HmJ8fZxmHxzpj33sFcPkmXHac +CKDOAZ5hQ9t4xDPNRhsk0IbAzisodGOcgou8QeCrAA+bgjCiv4b/PYziVnFo0RPf +OCiJAoGBAJO7z1UiSW90y8xfljY9MdVcOQhYkk36ESShLsMUQv3853g2/B44vUJp +wdsRnG5skEl2jfwUHLLXItXQxvnt4ydo764PuoiVu62Ki8m5qyK//R5Hv6v5kM7s +4YVSjQQJkxjtB5i7YVrlFxHS0xs7fMrcaDjMTCpCCWhcM9P8qqEC +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c3.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c3.crt index 9ab1ccdd5a8a..3c1ec05bf906 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c3.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c3.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAI32UnAStCMbefX4MfICKcGW3TPkMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW4yLmMzMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjIuYzMwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAKllF4c9YFeQZzEuE/YKlcqHfDiS+tqENJ0dFj3cr9Mjg5Ju3JjHL7llZNrD -7chh79YMinBbXi7AR/9Uz1dcyRpP3nKwiZZsn8oZyWCrmoX9EjeFPRBprWq+qOY2 -uUVVohJgGzZ6SUDxlawTyyOc5oU/wuYb4nmNrBsGT9tntHO5AgMBAAGjfzB9MB0G -A1UdDgQWBBSMdkINXUsaLlfrfH9RuxuKsODn8TAfBgNVHSMEGDAWgBSMdkINXUsa -LlfrfH9RuxuKsODn8TAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMi5jbHVzdGVy -My5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAOy2M -3ILUvZgwjWCpWc9q/2A2wy4ahFm++rjs9JzKVi+mSGlk60YEOFdJao/UxhFX8+qo -LNZQxFUStfQDmyiWM48Fa0JZyMKOSiRGdBbreSaHaDI3lpBhetrOCf+d2UsUFfMs -SW23cfeIDQwGD39xPjNRl+zPr36C5T5+1l1gBwA= +MIIDTTCCAjWgAwIBAgIUe615Wc5eU32waFUzoL1YQZdQmlowDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjIuYzMwHhcNMjIxMTI2MjEyMjU2WhcNNDIxMTIxMjEy +MjU2WjAQMQ4wDAYDVQQDEwVuMi5jMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAMwxAzcdhHMJXtF4ZOFQcNgk6cyLzbDBKqlDB2FhM42YIikhIamyeTm5 +S1Oi8VtbkFYt2Jx6WeXchB148AYCvuy3otdo662odmy6GI++E9ocW3p1VdlM8klf +0ey+L8o3xwtbSxFXPN0p8ajwzJjHziyhJVEIIlGbbzIMvR/9JBgYSYUt30tQDL8q +gXYre72/hzagAvw8xMmYSEqt0TK3dU34GLfGBnckDXh9G2eMBwhrKYnNlx7JOJd/ +PIQxS2u1hHqXuRjd/irsG1AOTskBp93N+0RxkAmAYhzWI9yW3VJNEEOKIC1nPI5c +XZxw1AjmjuMREMcIyw2DgkO2WQ2v2xkCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU9I+1 +tg9yxpvUzjDHVneiWOexSfUwHwYDVR0jBBgwFoAU9I+1tg9yxpvUzjDHVneiWOex +SfUwTgYDVR0RBEcwRYIcbm9kZTIuY2x1c3RlcjMuZWxhc3RpY3NlYXJjaKAlBgNV +BAOgHgwcbm9kZTIuY2x1c3RlcjMuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQA+xr75jIozxoRBzzjkvwmycHCRRgMaMEOHBAWDefGO +4b0zAe+eZSOQpP4/MippIDAl2ukAMD2LC5UaGS2cfu6k/I0+8Z3KUez8qi3l7j8X +sSBujQsVO4npjYN8ppR7cCRLyjjr6EFYbpeYTQb4+L+Lo3g9V40B4MTGIv0x1NNM +9IJNu/ml1T3t94e4uuAZxTSsKDxkBa50RwOeIQZZml+cosxUVfTra50BGVOROCj1 +WWmOvhna+CAmva6SdCs7Zoz34jucYbUjzM9xpJhP38MNpkD602fUdAw1dzReeiRF ++RVsGvVXubMkaKJyfWgVd3DyCau3smoBAxxgY1Hz4Nmm -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c3.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c3.key new file mode 100644 index 000000000000..90024890d892 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c3.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAzDEDNx2Ecwle0Xhk4VBw2CTpzIvNsMEqqUMHYWEzjZgiKSEh +qbJ5OblLU6LxW1uQVi3YnHpZ5dyEHXjwBgK+7Lei12jrrah2bLoYj74T2hxbenVV +2UzySV/R7L4vyjfHC1tLEVc83SnxqPDMmMfOLKElUQgiUZtvMgy9H/0kGBhJhS3f +S1AMvyqBdit7vb+HNqAC/DzEyZhISq3RMrd1TfgYt8YGdyQNeH0bZ4wHCGspic2X +Hsk4l388hDFLa7WEepe5GN3+KuwbUA5OyQGn3c37RHGQCYBiHNYj3JbdUk0QQ4og +LWc8jlxdnHDUCOaO4xEQxwjLDYOCQ7ZZDa/bGQIDAQABAoIBAFtaWDZp0c6HtDCi +HPwC+aFyDlmxeHJqUZRNhTlHfAGOvUXKaV2bGAo/wJgoD7RQSEiOFVmYkay5li/7 +YeNGijWsyMF30u0sL+Nf9Momf4o24qRsZ16jQsiaAnjY89eUUcxeaTzWW0gVUmVD +aj22MqMb626P5Y6+LCbntRthLStLsRQnqngk6Is9D3Ix4mvFppdtFjPNtyz9kpST +SeM8f867iXDHwM8BCemiOunoupMkRSwpDfd11gWMbGN19uYrus5XtpQI+38GkEO/ +Ct4n9cWVWnpaZvsytrM8my4eRp58ZQKQEHdPbEer7qBVz34m5mtnbH+JY7KRw47H +YRytunsCgYEA+N+jG/fHLN3RhRa4DZwtfHYYr7BtWY/MnEB5YFrXjZhNQy2xsxpZ +DgrJP1tVJA4TlSL6CPH06jhtJ5pNhyMikOiof3uPWcR5PjzdkDEYM+bS2Z+/sx2R +CQKpa2tHzpOIzGrPa1nQ8cOKq/MEL0LUQqbivPa1+HHHe7kxls6Aju8CgYEA0gnV +fuXlIUZdjPLj1VF0nXOClVE3Ko6f7bs3PmmJU8SabCt2laarBnW9AUWBr6CKMOuS +ant2mHFOVeQ1AaPbQhjMk49eEvmcWMmX5aH8GDEex6nLdsex4c4uUse5pilwTFMq +L0f7dES731Tyf0Yk+jMO74QDxy8euZ6qh8qpNncCgYEArQ28+p6hxeX8YgH5O6WE +z43C/q+8adGfImuGuuSQDGqCFxt3/CVvFV+UrtSQcIDFRyfKG1w7xH42EJqwHC7n +Q0I0oGjHtlDzSb7DBhfNT5HKvf7iKud9SXmrWwF/LfrS3enLj0cmDma9T3GtUDu1 +Ix+ufF1FDZv4bWTKQaYYKMsCgYEArj/eUPKkzqAzyFVRUo3LMzy7HulL3RCJ+Y4R +3MNtXIOEoSzFqo2k4S0Yak+hsXR8cO1egNyAnSOTG4rBtuSbaBBu2BRetVtDrB1C +2Q0zuVY29OoWlCpDltOYdrCfggxqz8dG/ga3w9OqETPGInStBK0LPQXGpJ6g3NmX +HCSzkS0CgYAR/omLieExU85fGvetxP0mbKjxaq1BDGNLSkvvQwf8lBeU0qFAv4Jd +naXvXIHNqT0YkJT/TYN5gQrGjubYFd75FzJPLgqWpy5KID1xIvNt+2tF/3o3gNjv +oojlJpP1B1UzSvotprxsdz6GW3+DxO3B6jFb854hPj1iCgCH6cAIyA== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c4.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c4.crt index 8e111943b69c..ebe2b268a585 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c4.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c4.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVANl52f14Y2XwcvVk/EWwSt6iKbJCMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW4yLmM0MB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjIuYzQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAKMaYHZ4RiQN/cACOxN9GEyNtxwXC1PD5rD1nPCH0Efyg5SLcDzbBj5YVS+B -Gl5HIQ3hwX62vnI9pRM8GJOZXX4UGHQ4gdd3B+qbW3n9K29cSqnxulIZiE+Ax6LO -XwwEzSag1nTEpJQo09LZPusyzXnicEJ/RpC0krAa9g6gnvNRAgMBAAGjfzB9MB0G -A1UdDgQWBBRsXxUJ6YradZoLR5+cQBXgj69aOzAfBgNVHSMEGDAWgBRsXxUJ6Yra -dZoLR5+cQBXgj69aOzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMi5jbHVzdGVy -NC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAlOSo -5cafQgnZyRFGdBdyovfVfJ88JzH4G1YC0As+6cnZmafZ/pIdUiaZOuDxg9AI11BL -LWlaX3c68kk3PWpL/vtJJg00QNhxzyHaiLdLbWuPoIcE7fDHvwbuOsXWQv446MKr -Q6mIe90sGfGCIcyBXXzP8mOTAyTcacHTS/ZL3Y8= +MIIDTjCCAjagAwIBAgIVAP7+3MOgJEogl1ohtIvDi3QqGQ02MA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW4yLmM0MB4XDTIyMTEyNjIxMjI1NloXDTQyMTEyMTIx +MjI1NlowEDEOMAwGA1UEAxMFbjIuYzQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDl6OPoUSVF4fSMHxGZw/wsKEdGZvccwB3grBiMhbfjoRi19OaxjLa8 +eN7pDoGOvsUtdw78U3be/ZbFmsQbD2xvxLQqirQsxWHDbHebKxbj4D2Ta/FtRBlz +n8OQIQwHFu7g5y76m8z8uh7VwoKEImECvfq52oqY2RE6KLpzW8zkCwRe7E/pzaUg +YjgyvxumCpd4koityR7qlSqbdksE/CU4u2fAiaPqhPaClQABdgCC0WwDBtIrWP4w +riHsXkhi/fVkSK2guWVqSKkpKLfpgXHx2NOeVDF1eAejCo2QOzgJegbpTKPaRitz +SMkJlYRqxETZgjEYQOQO3dpy8rqIgtQZAgMBAAGjgZ4wgZswHQYDVR0OBBYEFLZR +JfioFbITDCyh12oLrsKu0ndHMB8GA1UdIwQYMBaAFLZRJfioFbITDCyh12oLrsKu +0ndHME4GA1UdEQRHMEWCHG5vZGUyLmNsdXN0ZXI0LmVsYXN0aWNzZWFyY2igJQYD +VQQDoB4MHG5vZGUyLmNsdXN0ZXI0LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAB8h4bKPCDWFfRxTQ/it9RqTgwup6AaLvbfUPrcK3 +uZX7UeDOb2glqg21D7KN4MFiT+k/y3MWJk+ztjzyKgf/kDNo/k83rLboDtpQaRa1 +2EhjxiXQOh/JUHn34UL1M04pqthxZGeTiLVl1ccm673lurQ4Zyl/k+7XnMtyX7J/ +DaP7FAOiwmnW4TmT9oqvGSIDD/XVdocccFTHDIROQBBhNjschxKhucfakHVKlQjS +52WPrfbRpKggXKs+uy2+X+HGfzltM1xf/pa/G1voSYCOLwm+PTWkSjh3Yeg5Z8qX +vBOr0CcyXr9h0M8ESiM0nSwph8fRRlSiVm0lDDt9/nNhqg== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c4.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c4.key new file mode 100644 index 000000000000..c027d3635cd0 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c4.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA5ejj6FElReH0jB8RmcP8LChHRmb3HMAd4KwYjIW346EYtfTm +sYy2vHje6Q6Bjr7FLXcO/FN23v2WxZrEGw9sb8S0Koq0LMVhw2x3mysW4+A9k2vx +bUQZc5/DkCEMBxbu4Ocu+pvM/Loe1cKChCJhAr36udqKmNkROii6c1vM5AsEXuxP +6c2lIGI4Mr8bpgqXeJKIrcke6pUqm3ZLBPwlOLtnwImj6oT2gpUAAXYAgtFsAwbS +K1j+MK4h7F5IYv31ZEitoLllakipKSi36YFx8djTnlQxdXgHowqNkDs4CXoG6Uyj +2kYrc0jJCZWEasRE2YIxGEDkDt3acvK6iILUGQIDAQABAoIBAAHxFRets8Ri06dp +TcNAMf2He46fKQAfqUDwAycQrAxqnGRRBK/EMfjmhm9udW6oss6e/kvoQo1AlAjo +NTbLSw2omiLv1/S22AwH+vyfAQr8plh75WaYWCSvWT364Xif23WfzjhEn5cU1VTV +1zdZePtGcnsmWtxOnQQiOuzlfhYA87atzEX8deZ8AUn6jvkiRK4WyZOmHBg1vzjW +51lyou/8pSCcEAOE6qHh7x/61JYn5ChL4AXMqMJeit1p4o84vYkmYqKEGXNJoFb8 +kYHLGEZH7mEsuSxQUQNpko1IVSJQ+edx1jyAakveBOQK6FNLV8pmTAAiXr9t/v5X +GQ5OrbkCgYEA/rbOA2zy85TBxgxDmAwZ46k75AqwJwRgycguqJuwQ87KoFD4VO+z +B2DPi1cy8GB5Zzo/J/G/8+szX2taH3UbY0WLEpGu4t4OUS/eiuQM/jIjCa883ODx +WerY+ZjF2Uo20fwMmCZO/YU+XtQNz/PJE2+uZjWQ0MQQsH3YVbK1qR0CgYEA5xIH +Mf4/zgPsZw6700uqBjPzIGNqfS+V/+OkDKBXfxK4EY0Umss2sQRfAYojWa4IUmZA +AGcSMOC+aMwPCpXehThyUN8QE121xnY11A1FPp4VOR5zcMa2oOUEf+oZXnJvRk66 +hwl1xQpW11VOLu/ZnyXMNRBOE9jpLm1QUxKDYi0CgYEAhiIeOw+UoVEk+fZbYiZ9 +O7ZTI7O9L87OaXRRndje6dZsPiBlpdTUon4IzYgN+yfSxtmViKN2Qi0J0KXnv8kb +sxtcMVsmjbICOB7NQrb1YKdmbCMsGeUbNA7IVChviA3c8JeaykTiM/+G6MkivO/U +Y7fcT1bF/CpD361lzztdDsECgYEA26vn6z8qqVBRjigTp+xm1z7MBB5/PkPe9kYT +z20fwU6qbwUQcZh/hgoByBTrRnaMsslCTx+ty4ndewODySxxP3VKOFfXkJGmHeaD +hfSXv8/JnWO1m6FOPed7P5/uPjvMTarK6VW1AH2z0ULn8PdUjFgCqu+1SmkMRXbm +xhLzlIECgYEA99Z0+JqSvpmYnS6WaWZXgCjOk4M7PaPViBPYUm67d78EfIScVzJc +iPsgY1DAHVzqeeyWSWWvZGgnb0jVK73MUC8uWw5unjwxsRWhx3HvWIyoUwe8JYMU +P7fwfvowBvf61ShGp2n90khWoW72kaFrQTRPo55SfS0UpBF7BANAM1M= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c5.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c5.crt index 31d960f5e03c..151b471dc280 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c5.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c5.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAI/+n5L43cIAcNrDP3rvwdoNitatMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW4yLmM1MB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjIuYzUwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBANLAWJxq07CPGG6VYspUbzyEMn6pBE0z9Or69wrDOKM7CVM8CzrN7aw8zYDL -uOZy4cEJxLJKvtw5wQfh/JELcMH2WNLIFNABl5+m+FpNLBGgFMC48NUCfPexD7JU -PAc7oiaZQ/CFCP+5YK+a5F7M9/SwVfQKKnsMPK3v6zyZkFO7AgMBAAGjfzB9MB0G -A1UdDgQWBBTKJgGOBVnP8ZNUNlZXD0iNkRRDEDAfBgNVHSMEGDAWgBTKJgGOBVnP -8ZNUNlZXD0iNkRRDEDAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMi5jbHVzdGVy -NS5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAbcaY -9U5E3lyLp1n1UT9ejqHbJwQD+lfTFx9OPou9HifYG9qbNTg92lo3zl2lcm/pD421 -zvqQxz/Eyr18WE0qzzlX7aHuzpy7S9UUB8GnrbZ+oVHWiVJstZlTAOXO+gbPi1gx -UhiXctx+YQO4V07f7pFFLkzeOT94Aaq6yLDqc4Y= +MIIDTjCCAjagAwIBAgIVAJcoStt1AtuPNLvpl0+wWmEGynjPMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW4yLmM1MB4XDTIyMTEyNjIxMjI1N1oXDTQyMTEyMTIx +MjI1N1owEDEOMAwGA1UEAxMFbjIuYzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCZHYIY6C3Z4MGWABTJce8rvYEs+ShnRBMOScqujpur3iDYehJb7DdI +HLkI6JI6QRL6Kee+iuw/zubnfVxt49moRy0V5v4mK0w7QgS9WftICcDVXbFnPr94 +no13rw4PMfyRkW5cwSCel5AHjI5TH5zST92UdELS/OjsacQ03nG2qyvvBj1G+D7J +NC0alfSXuV4M9QvOO0wNYQihafURUuzBbqxT7fYawK7YbmhHWyK8FaxAPF9fru+p +BnfsFbdUaqtLKhNlwHrUCKbeOcWalzFh0qt9mXYy5wux3Bqooc/e7wtr06HzBpfz +gvXHtG5IQiRs3q/dJNMUJf1qPJYc4ljfAgMBAAGjgZ4wgZswHQYDVR0OBBYEFJeV +l64BRDPdj9N2YXawoUZtU0yBMB8GA1UdIwQYMBaAFJeVl64BRDPdj9N2YXawoUZt +U0yBME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGUyLmNsdXN0ZXI1LmVsYXN0aWNz +ZWFyY2iCHG5vZGUyLmNsdXN0ZXI1LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAkJebbN2CJjyyw5GKKtvW46NBpYxgEjbwEhYyNDaq +bUniBTwXMdJIngsMIWmZk3ST7SeIZOV9Vafy75jXJ6eTjp/6lejwEs9ui2Vnk543 +m685ztyiqh1fIlFHK0Yc70DcaJKEA8XYWGinc1ijpAoeqMR8NwwS060dF/IZzXAV +LRGtR2IS9W5UTM/2QCDj3ms5HiVt5KIK7UB9E4PudZebTU5veT9t05rxtV+Rk7OY +ErnfdhFuhx5I8JTf8lJ/01BOrZ9b4k2/OG9Cx3KngYlctC4cUO7m7FURUNBeE0pm +LdyC28Bf0bgRJWTuh1GFW8dEaXMQ9M5HiJ3VkUrm5LW4dQ== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c5.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c5.key new file mode 100644 index 000000000000..8677818ee4c7 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c5.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAmR2CGOgt2eDBlgAUyXHvK72BLPkoZ0QTDknKro6bq94g2HoS +W+w3SBy5COiSOkES+innvorsP87m531cbePZqEctFeb+JitMO0IEvVn7SAnA1V2x +Zz6/eJ6Nd68ODzH8kZFuXMEgnpeQB4yOUx+c0k/dlHRC0vzo7GnENN5xtqsr7wY9 +Rvg+yTQtGpX0l7leDPULzjtMDWEIoWn1EVLswW6sU+32GsCu2G5oR1sivBWsQDxf +X67vqQZ37BW3VGqrSyoTZcB61Aim3jnFmpcxYdKrfZl2MucLsdwaqKHP3u8La9Oh +8waX84L1x7RuSEIkbN6v3STTFCX9ajyWHOJY3wIDAQABAoIBADUAI1/2wqRYKfp/ +1bIzZ5xG8Td9mbBBPq1j+q44vAcekVUN0FwUZMBn/N7qDm/uxaBlsAvjb8rYllIM +MVnylI2s8ZzSxCe3NiCPBi/7PUGe6qqQz49TB5k7nPGQXXgCEBDdfAvMaDutcBRE +4aFBYJnmPmecZOM+22u2Ys4AL/GQPsTb9bv1Uroac9m1HP9QexpXgMcqoFgyRzvH +CcGaPNzp9HHKyg4LyyiAIqYeu2XePhlw0z4bCGwEjPpnC7rurnMvk8pQvbvK1wG+ +J01sLLYXQRKlK17KlqKMHaZZ8C3ztKUGG1jkJ/yD+GWs8ZPBJAzf9cU9BBo3cmMR ++8nBTd0CgYEAwxFWEy032KrK0P7fs7zBHh+BhdzRZOkF9meAhxMvZu07uDCJTkIA ++PjP75SIcAm+wLyclfYoqgrSRrW85VHPonpbiGxIMYk8v3ppDxi6H9vTuizUOHo8 +yo7FoAoEq4j5TLBbxKk8hP6DbSiXgqzGaBP/lRvnNHT6U6BjtpbGRaMCgYEAyPFt +n3ftfwwwuOJKOOIs3xQIWFQD44Kk3lmBjC73yJORV0FHNyos9hvMGBB83EYt6V40 +btYEvtDG2UmqiY0U58OeyrB5YtFgo5caRaF70KZsR1WnbqnpJSeu+o+KxSCuKjMv +aYSiM2eEJydQ4juvSWDGvqhnx/0u5ShEnxdH+5UCgYEAtlu3nxfPtJL+JeGPmcTk +uNd1S4JCktaxR0g9RIlv8AXppWhihDF6HUblhTDTWAIUPLRWom7MVyLkr0vo+my/ +8kH23hN0bjLBArRaEM3p+iyUw+ByyRWlTSupntKeyQm1HbdlIvWfaSlSDjM7aONC +XDwN3ASI4WZWOuWfMivD0YcCgYAVH1O/pJKhfT2N4bOdoSDIXkm2RzaVbsE34j68 +5p8DRYfMnT8ILHXHdQqSKQGHAhWHZ3CpifO7vyR36Md1aBAfgK+rbLuFyK6Y46RO +V3g1aZkOMpHEDzZDwpUhH3+d0x6brv+33spSDvqURwy4iz+UvgiGOBuYqSOJ0R7N +flwBtQKBgQC7upWH47PQ3a53plWK/r8RNbqSnpzhwni+1hy6AhJFXYeJ1z4MnpJ8 +sE4tJJvg/A+8t64/6x1qteiwXpN3YsOZFxEP2LL59cqUXwsP2VAkYiZdl0XrGuE1 +wB1x+o8XRlTfSbdZC+fWNZpysBxjJOD76EVlV4Qv1nF1aM9ma1Amvg== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c6.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c6.crt index e8ffcdd543f7..b8d44d75425c 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c6.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c6.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVALQXuI2M4/7RVxeg9uI+ABZLvK7UMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW4yLmM2MB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjIuYzYwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAICwXz/hU7DraQWX8TJWd8hva4KzTKOpq8JjnbCd1n3ovAqbDP6GkgVAJxY0 -eMFwP+u4jN2JZuTISWIUOmLGosyHpXsXQwbsvDLUC0JPb6v0/omkLFsrIXgBt2xI -y7Bt3eifKUmUGO3ePi+LbgKYq7YNy1uVaFiYLR3PRul+TCXpAgMBAAGjfzB9MB0G -A1UdDgQWBBR8+r3Wp4Dc7QfmCXkTGgz3gyfP0zAfBgNVHSMEGDAWgBR8+r3Wp4Dc -7QfmCXkTGgz3gyfP0zAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMi5jbHVzdGVy -Ni5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAc3WH -mYDHi4OWsar/4jcqq1S0RZ5lGQWtJIpVXjw2SzH7mzfR9BrD48fgwIrPJkpTXsi5 -ATFoEZfVQluifS7qjg5lPVmRkQ0FvNMxOXq97tyEi8zqeWa3TC/5brUx64aeyAB0 -rokzCaTAHY38r7b8eDIFpOlQXj8CowjbXx/rvgc= +MIIDTTCCAjWgAwIBAgIUR9zy9zaKszvdno5SeBTB8Hhcy5QwDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjIuYzYwHhcNMjIxMTI2MjEyMjU3WhcNNDIxMTIxMjEy +MjU3WjAQMQ4wDAYDVQQDEwVuMi5jNjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAJGNujkGJ+9CoUSssDOkZunkTx2LetJD5Fr+VJ+aC7Oim4919ZbOF6aH +Uwe5RuHAmc0+l0Qa/bPQhgIN/lDDerXVDwlWHJcNkRrN75tnuI5x/jQZXPTNPWWG +m/cSNjPing115PtjYGrammxmdkfrizH/MwmlTuBF9r6nvTQNcWMs2I36FVf8XzTB +EOvV3jYG5HueTLZwI8RPfObjDzoeEd+eybPfFL4bW5nPH94Lpssjzz0UQUrCwx5I +udxsO4t5LN8x+wPut5zCiex3AWlbl6Dqh2EwaqT3Q5uKmbWRv9QlFpQY40xpjLeu +WxMHA2hqe4YQWOvdQRliCw6pMZN0GhkCAwEAAaOBnjCBmzAdBgNVHQ4EFgQUI+K4 +6UV5R1OqMR/VU2R2T8NCQqMwHwYDVR0jBBgwFoAUI+K46UV5R1OqMR/VU2R2T8NC +QqMwTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTIuY2x1c3RlcjYuZWxhc3RpY3Nl +YXJjaIIcbm9kZTIuY2x1c3RlcjYuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQB9ri2rP34uKk07vLx7Rad3axdNAiMUzFyDVQA3hIur +TCZE6ukwkZnj4cCurD5OHW5R8bNgm94iSVaXk6JHCLKVfaxg2jJKKABn+46qeOLk +mKHU8uu5Zb85l7mnRJPHf9S+G0xgACxY2SYk7ota0F3Iy6pXCX8N+53oBm1mbWTk +55qO/h6P0031iO5yFzaADFpJj2z6G9OFSrU7LucebJTpC22utr6QaSBGiY7Fb+/O +OKJcFYfIU+YvrsJ5v3yup452YiM5/30we5euWVb6bCVlGp/fg4halvZb5CnCVM5m +RCTDtSnG4E6YuiZ3CKiLnrBzf0WrnyAWGQFAbDVTOFDI -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c6.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c6.key new file mode 100644 index 000000000000..7f92d17f5b37 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c6.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAkY26OQYn70KhRKywM6Rm6eRPHYt60kPkWv5Un5oLs6Kbj3X1 +ls4XpodTB7lG4cCZzT6XRBr9s9CGAg3+UMN6tdUPCVYclw2RGs3vm2e4jnH+NBlc +9M09ZYab9xI2M+KeDXXk+2NgatqabGZ2R+uLMf8zCaVO4EX2vqe9NA1xYyzYjfoV +V/xfNMEQ69XeNgbke55MtnAjxE985uMPOh4R357Js98Uvhtbmc8f3gumyyPPPRRB +SsLDHki53Gw7i3ks3zH7A+63nMKJ7HcBaVuXoOqHYTBqpPdDm4qZtZG/1CUWlBjj +TGmMt65bEwcDaGp7hhBY691BGWILDqkxk3QaGQIDAQABAoH/Ao77NwVkrk/9hbeD +YWUCahoZSeupvQh0eyT9V9DdY8r38Q2hw4Tb5ZF7ZCddl5pLGdB/2DKhQwfnUyHr +1n+ZKJkWKpPY7M7QPihgjdrE47wdO802VRdGfQJgu3cWfNL+9D2eSMz2ZOpyzHUA +n0MEr1xNCV5ehJK2Q6OvL7sMwzF/07IAZDI0hGEs6hhe2FbwsBBCOXNtjUzE7kte +ZarbnvfNUaJTMbxZbMTEaY4wPX6uAXv7LIU+z7BPloSXY6YJQTWOJ5dXE0IA5dNP ++0QTM3Qzv/v9q2vV9bwV95Xs0EAFh82oB9tfyW5w+CrmMbXoRXHETDgLdLlr07pC +yj1rAoGBAMF2uX7MtrCGpykBcFYrXp7ORmDozh4Wg6Mmc7Y/AvnlM3lpbbEBDYNA +F5yOS/91cGBvtvYs81Rm0XFjUbPdWiyKXv72kRep2SPg3kmMMfcxtOjZZZx97VT5 +hf/Pn/gjJQM+u6kEZKId1zNXja1cMoBrQpujQtL6ROBiS9UO+U6HAoGBAMCaZsqA +B003gLq+jjYbJkQe6TDBtcyg1gWD7Ib5DJXLb7G/2vu5mZZTAerLnXoSYqenluzI +Yj9l4hxJ8+JLIz64pOdaXevQ5OtqqTSf0RQYOyEn2YyBXvVlHnH4opSKl64J/UO5 +ClQzH4K1R+wiaxiVBHaev9XzXQtOkT8Sp9pfAoGBAKnOfoQoRNXuanxYcfSAPp3e +6fEXhgrssgAauQemiQM0RRj1C72H8gdY0yCr2VLRDqDmjU3IKI/Va1I93bGkwQhF +L5pUawe9IgUW06T0c8rHi0Wxwzk3Yn/qP85Hu949YrhVcLuqSnXtAT1kRMt8GCuV +ebJH+1hZKOlV/b91VCezAoGBAKKY9tRXt1JR3ARLYMV+sBRsQBMgRbllwqjPUbw0 +vpIrN7VIsbc3XdkRZJnyoZRvtwewrPJizMaW3YdiS1ksc6m8ucJygL3XEZtI5WX+ +0cR0HF8GRFug0Ci84DH5ZE+gnSKZXk1xBq5EpoJzRi+scttC2ozFwk/97vvfrTEh +L8FjAoGAeIgcBtnefZbvrhvou2siMLeYpSiWlpGYlcnBXw7whR1axfj1qS5l4JIp +I/IMu0fp80cGoMqqCvQ7wU5hz3LBN7lvG0aMlqrX/sVaBfmZOwToOOslFvFArCgs +tizcy4BvdZlFckoiAN8yXIBVnhLk3DdiT/C9iFJLekOU+YDJd7w= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c7.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c7.crt index 242a81a0d29e..de496e58e218 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c7.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c7.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAKWasxMN2xGHyCWP+sEXohZanUU4MA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW4yLmM3MB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjIuYzcwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAIW2QjCSqW9RSVtYf8XyuveF9dozsAKdDqN6r9n9OpUrkdozB/tguOPvy+yS -aHN/139IrP1QYCqaiaUBevUQzU1N7/yYnFXpdGiNUrkWXy6wTSuZhmBm3r2y2XwJ -rszGz5/uVpQc1njzZssON9Ug2mzxKvZvs4hUXnX2GOyujx0PAgMBAAGjfzB9MB0G -A1UdDgQWBBSLcQpmKblNBFdAbAfRm3OAg0CSkjAfBgNVHSMEGDAWgBSLcQpmKblN -BFdAbAfRm3OAg0CSkjAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMi5jbHVzdGVy -Ny5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAHVpk -qkgLywJFDpCTTt+vtpsaYlevMdrNAKI9VU3F5NQks4goc6HvzsDwttFh1BrWXitp -NaxEsbwk21lLFTZ7GlzuChFHQWsZIHZDiKStCEiN726fqcjfzYGF5wtca6idD6Fm -xAhf41BB/O9kzIJasldR58vtfepGXPLJWKTrshg= +MIIDTjCCAjagAwIBAgIVAJvOpFC8dTItEeqG65GtohGlA2Z5MA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW4yLmM3MB4XDTIyMTEyNjIxMjI1N1oXDTQyMTEyMTIx +MjI1N1owEDEOMAwGA1UEAxMFbjIuYzcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQClr0f07HkjIoauVMeOH7bXPqLHBMPtsS7CahwEsJlwpbEluObR4rW6 +dnOCZ6dbkhIiIDCePL4/vHsxjr/8wgp7M/S7itL54VK/wdpFz2EydhYfMj45AlFM +d9xBXMzZ3RxQD48En/YodVLyhR6/QC5Rf2pkcR64bsntJkltkxEBY/GVuRlcAvtf +/jz5Jijz8tqz3MheqRm0cDni7M+4zT8Ctu2RQxHPXbqAt91QXe3k+/F2fo+xNA0e +g4d6rAzAZ6qqXrHY4zK67S+7o4fsCdnQgVIabO3IwjeoIUj0+GOQg1rmaPuh1Xvs +b8AdxUT3VZmWIoLfNKVmEzEKF5jJcDAnAgMBAAGjgZ4wgZswHQYDVR0OBBYEFPPa +yf6p+k7sbzAaWZQYJ4pf1VoFMB8GA1UdIwQYMBaAFPPayf6p+k7sbzAaWZQYJ4pf +1VoFME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGUyLmNsdXN0ZXI3LmVsYXN0aWNz +ZWFyY2iCHG5vZGUyLmNsdXN0ZXI3LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAlY8eVTJG0WIT2qlwQg+win+bT4eqH/un/8kKKDwL +puglPK1qbWLkZ6R7qcuaxLHC1zMdUM/ffaA1xYUZS7udD098psLHVUuziQYJT6ll +fldju4I0hDoAudkMiD78weanL0FRsecHu/aEcEa6GPaqJ15gS4uNxxQc6sniaQ6u +FGHZxWOM7bf392Ipcp/Z7BV/y+P3ncb+R/a68/BswigjX59KDGaCfmWHxoQCLXvC +k+gt2ejm4I8ulidK4vko7MhzEL0RxncdGRWF/A3oqYnrysZvDfN3z6cEpEeM4S4g +wlqP0mpPl0hq4QVDYoM9i37k/FoTy8i62nnctrdJpMN+eA== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c7.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c7.key new file mode 100644 index 000000000000..fc447af514a7 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c7.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEApa9H9Ox5IyKGrlTHjh+21z6ixwTD7bEuwmocBLCZcKWxJbjm +0eK1unZzgmenW5ISIiAwnjy+P7x7MY6//MIKezP0u4rS+eFSv8HaRc9hMnYWHzI+ +OQJRTHfcQVzM2d0cUA+PBJ/2KHVS8oUev0AuUX9qZHEeuG7J7SZJbZMRAWPxlbkZ +XAL7X/48+SYo8/Las9zIXqkZtHA54uzPuM0/ArbtkUMRz126gLfdUF3t5Pvxdn6P +sTQNHoOHeqwMwGeqql6x2OMyuu0vu6OH7AnZ0IFSGmztyMI3qCFI9PhjkINa5mj7 +odV77G/AHcVE91WZliKC3zSlZhMxCheYyXAwJwIDAQABAoIBAAK5rV9T/HIdgUWg +0199Gg7UUu0fIPHp9jDweeGB3iRCNXW/YtyU9BRLGn9MoS4aav3wtZwEt8Ron0Yj +aPkJYA7CvgXPtMSzdh79W0/Gp3yjAJ4mv2tEpH2zgwX4Ns5mmq6ecuXKk8v6khC3 +K9hNf/WLOP7J9Y1nFR83gAGzZ5Y0PdtDA/EmpIqFofIewvUzzSYngZM1nmPgGWB9 +BGXbQL9eAhiV2nq6MO+ow14AGlmccf0Kt9D/3AJbnyX0AYLEGUYQeAbE4W3rqmX7 +Lk0p9Z8qZqRR1GMx3u1ZmE75HgiZl1s20eGVEBUKSoWFzlYUaOZGAAmZSZd9k4kO +2HYjgnECgYEAvnLol1H43VP6WRtg2xLgYrjD0IdM+hoKzhh5PO8voLAuorXCpAtO +iEfHXBUfCs9SUUF0DUSRwm7ej/xcXos+WQd8b/BPS9eGao95PG2DZZRKym8k59Hh +YD+gQYWydY+Yg17CUzbFC8ltIdRsH54xfOsQme/DGVayQ8FTyCfKsOUCgYEA3rZR +aNPBhckBZ3y2Uxy8ayrpiXtpCtNGQG43hM5yuqwegVYNPLUtMrufhNmH8lnd9z7Y +6RTYMmlwXTwEw99JmzchZ5xeqPQ9X53Hb/vZ2/udKmgzbq+S64qToiJ3j44RUtOa +DIm2RsqUiHudkp1bcvKTBgCzwgUgkISEwp/A6BsCgYBkINXYeOh2tOgyYQqTI2GF +hc6tm/UEqRx1LrjPKJHJ0dczBIzI4ojvbY2LBC/F5ypIPjldoGdh6hhpayzFKEAn +XqA12gpWvYGWFMiQ1KL4jPalZ7nnh5kOmWUQDdK9rDlrGC7iwTDn+Ya++oe+N9Wc +75lS1ZOlhsdCiCocWY9fYQKBgQCGe6VRHX1gYe49JWubaGDGfdUjtafKZ6WmRme0 +BE33NvP8uY+otuaiGfXLbOfQij+kiwjERlf+qPi29AUUmgKCfZnoTN5EO6fL0ySZ +FIWLdraC4wUbs12OadGYOCPmRa1kTG5bX9T3jmB+wdZ2sBqGoL/zAf2KDTqKydq5 +A2qjPQKBgCF7YjFnV/rMo+eKwJVSaKIVenGe5Jehf2C0TZr6f88pC+IuQBTLslvH +Yih1Gq5CNo9IM1P2NdJUOvvGaHH36wpOc7zB+gUh8F8KELoe2XdP52RiNmkGVvsu +JHXCmwL2Q7WZUQh0DkOtFFczGb+5R5g42JvYGmIZSBBYpXv5CWQT +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c8.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c8.crt index 8beeada43809..8a9668e282b0 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c8.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c8.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUXWB0aBshcvW7Us5kC8nYweCwm7IwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjIuYzgwHhcNMTgwNDE5MTMyMjE1WhcNNDUwOTA0MTMy -MjE1WjAQMQ4wDAYDVQQDEwVuMi5jODCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAr12D4+Xsjy/okhAJ1kDVb/WJ64ocd2GdWQ1iQj3LUrDtw/KFlWwCk7OaNflh -xxQKC7/Ow2Cjor9D8yj3/arHH8kKYG9pMvs8BTMVLhU8kXCLIziuF5s/fzBNRvhh -cqTdjcZn1fLW8UVsMybHgdEQWL7Y9pwxnKuzukIga2+lOU0CAwEAAaN/MH0wHQYD -VR0OBBYEFMJManm4XJjT1UmZ6pOR0RWsSVR1MB8GA1UdIwQYMBaAFMJManm4XJjT -1UmZ6pOR0RWsSVR1MDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGUyLmNsdXN0ZXI4 -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQCA1rWP -pNBDZ/vvZHe4FpXS5jWj1qxlcr9bGr9CCpWtb8cA1CKEi5bBAN6gvt+hoFw6/6XW -wz+Az3BteusKEFHm7nohr3xswCdffJDXY7olHhNu3j+r1RtW3YXxfMlFsCZxUikL -zOD5OVDBhNPja0rt4zUzoa54jg0xy+14G/dfWg== +MIIDTTCCAjWgAwIBAgIUB4t0CwwWjeH5jVZnP4k/zauS2wgwDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjIuYzgwHhcNMjIxMTI2MjEyMjU3WhcNNDIxMTIxMjEy +MjU3WjAQMQ4wDAYDVQQDEwVuMi5jODCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALLm191VZfD/2gOTWLWMMaDnkXcyvCXTGauIZ/a83mTOaf8WBPE7Et5K +u8jGL4yJiprdFUoTXYEEez1X6akWVSPX8dfGv4Wpx8+1Z+TEzPulBrVH2oPaChh8 +kzwFfVRli/+YUux4J1lvRpcS0AjtScjiUizbrwG25oCgajw97j5L+x6iNeOW1fDc +XMIvVQnNv2hUHX/othDXn4t2o6vmD6IelAtFbPnjJRjnTsHnuMZQW9u3DKUiR62T +uXksw4G3Y0R/vFfLD76eJLu/tt3RsL27qz0UtLIQNNK1UZc0CQ+nRxjj/hER8IRv +PsYBF0ZN/i8+2MVIZq4sVWAdyoyGu0sCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU2pjc +P2VyYCZHArlSAXLbq75OYzowHwYDVR0jBBgwFoAU2pjcP2VyYCZHArlSAXLbq75O +YzowTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTIuY2x1c3RlcjguZWxhc3RpY3Nl +YXJjaIIcbm9kZTIuY2x1c3RlcjguZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQALlCYwKm1wbih7kpcif2t4OEucbKAS0ebDjXxJ1kxU +hN0jbMNNIe4e993O2hAaLyBES3V4Atlt4st97ta/ktB9oulXw+NeGqZJj8b5vM9d +pBtfaC0rwkDhwr/ZnVRCYhOSrILHQvTpYDqt+5BiuqoZbAIIpvtBpL2VBsqBapfY +jh3JLMtD+Wr6TrMPVhezYzR8j1bcz3QpVQYtrvfA/SH7BRPZDtpdDwUA1YrCevJs +6wI/gGQfbCwpyNJRKT1CdmhoArdCmqcG5eeWJXH4MjxyL0CYslzihICfnl0G3HU3 +zAzVRLC93zxEmol9zs07NidxQu/p4kr+abp2sSP8aGz3 -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c8.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c8.key new file mode 100644 index 000000000000..f81b716b3f69 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n2.c8.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAsubX3VVl8P/aA5NYtYwxoOeRdzK8JdMZq4hn9rzeZM5p/xYE +8TsS3kq7yMYvjImKmt0VShNdgQR7PVfpqRZVI9fx18a/hanHz7Vn5MTM+6UGtUfa +g9oKGHyTPAV9VGWL/5hS7HgnWW9GlxLQCO1JyOJSLNuvAbbmgKBqPD3uPkv7HqI1 +45bV8Nxcwi9VCc2/aFQdf+i2ENefi3ajq+YPoh6UC0Vs+eMlGOdOwee4xlBb27cM +pSJHrZO5eSzDgbdjRH+8V8sPvp4ku7+23dGwvburPRS0shA00rVRlzQJD6dHGOP+ +ERHwhG8+xgEXRk3+Lz7YxUhmrixVYB3KjIa7SwIDAQABAoIBAAKZoUyJt7QTe98u +bo0g7v24L+CMH7jrAwX/swcegprzdw3qi4LYCXOqoWUPIFTKEPYkPXF4q5S69HGV +TAOVnAIr2st4i4MNTyahK9Pi2lnT8a+DzlS5QAlQEjG+oK4v1aOO2rGuI2BE4kFC +2Dww+rANPgWB4d7EAkCfVDvw6HwGR2MdyMbDNoyKB50UsCqHcnlKWZsoOy9oc7J2 +IHnEb9tthsnJRcZXnsaukpl6mq1DK9RO2/1swCP+tsUbt0XaFTKsQKfLlRMWdPZQ +OtFVRz9BL5DYj0XNjF7/BLCD1B2+2bAmw1saEJLpp/PzfhyWChEqQ6cTZL+3rBTB +9NV8utECgYEAuDUW2nV51dLCpCyJUnugD9DhwDD7DYWHfoFy9cFx3kqmwxGqtrMU +CXahQrzvH9dS97MdNG303vNpyEQxNxsy3TOc08m7FmTQyKgHwEstY3oDku3uhkt2 +sweeCl+oWObFSdnlYen9PQWxh+mSQKHs9Nj1jAQaplCM4fa4/je15xECgYEA+KBk +0WfvOfgMgqrMx8WDjlXH3CoERvHPfv8K0Y5KXbl287I7A8jrkLwdqAZCmcaU0QO6 +sDo0HFTgxDyDCeVqCWyMi9FTEPTfOB/C8oZAs8pBNs4972l4RICsH/fX2SBs9ZX0 +IEmjgNazRL2sgOJsavqMRYp46zRBCJiNW3bulJsCgYABjqFU021mO1Pd42p9wgHG +YHOoFkHpCVnt3dZE8Y+u216xg3K3/KeMtO4EDGjREhbndSIoEU3inJgmUz0/RAGY +ERmgSLJGLC//ojv+iiQmFR/2nABKLlUgik3xtIFNzxWX3DmuLDYxz9yCU50i4ruI +DlV1C4vWIBQwzM8fhwUIoQKBgC/J/Lb3iFcXI6h2FhiMEYZpXJooBVxxnFWnxjeT +Zx3uyTt8VbWbiBPw5K5oTaLrNa2bTuYSufN8KRvvGazsSN542+cAHDxVJRbPnBQr +1AH67WAQcn0zQ6e/J9aZOAcHIYpvAVa3cNMR8bFT1/WKEBbcA9xWCnoDOEDjucAL +8z3jAoGACSyWqYVf0ElY62Kw+ij0gW2MBVQmPr8VR30Yv7nbKpBZjeL4ekhIEgR8 +/mzZiUqO8Y2taQ1p6B0dRYek1kpdBhf6HTPO8YYpCN8Ff06PwEtzouak/55emHLr +/lW+E4AHdbX93QuWtVpSiq2mikssO5tCbZVwNraE61ybJk9myaQ= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c1.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c1.crt index ce72a9419b3f..3fff07781559 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c1.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c1.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUeWtVaCz/N70J5xpIy86NU8PUhSgwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjMuYzEwHhcNMTgwNDE5MTMyMjEzWhcNNDUwOTA0MTMy -MjEzWjAQMQ4wDAYDVQQDEwVuMy5jMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAjMpw8/41N4mhUc0rfxK+8ykcANdc3iJq9I4vyKR6vTHMJX0Q+kEKLpvN+IKN -Sq1LmfTJJMJx7vljjK6Euy0BncOFTIq91XehVky/ZPZOrLbJutPbRjCFGOwxdYqU -WMh1YuniQBV8y3gx/6qn0675EJQoK1iXX5xoe3wrqUTi4+MCAwEAAaN/MH0wHQYD -VR0OBBYEFEns7Lx7ZFHascuMFwRNx1HgOZjWMB8GA1UdIwQYMBaAFEns7Lx7ZFHa -scuMFwRNx1HgOZjWMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGUzLmNsdXN0ZXIx -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQBzLfJj -Y0VUqP0O+FmSmE8xeWchvSG7XAKJHkHPnUeFJbq/duSHVAqkIDMNYd9YvHHX9/R9 -8OlIfahOMy/j42E52PSmC6KSeotaUhsaN/RJ2WdgTS8+lnIimYemi1CbXpVgdZgf -lBAfF56ypKp+xZ2gSQEZZdD7bATa45LF5ZyjFA== +MIIDTTCCAjWgAwIBAgIUTtnXtmROb8uKWlmJr9q41UElqK0wDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjMuYzEwHhcNMjIxMTI2MjEyMjU3WhcNNDIxMTIxMjEy +MjU3WjAQMQ4wDAYDVQQDEwVuMy5jMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAJauAZu24APbVHtZ3qot6TmqQ4s7QEnKFz65mQMYBiTIT26Y5jL+Vy8V +veC57X7myrgk7drsG08+jfLTNuNjlqngNFEF18aGa8hKi6N5HbBL82/zCp9F3gyA +WbjYAJJn2oxEdOXHlK7A5c8gwByk76jTnW4vmBZTtvymAZdYAouaR7CPlrzkh9VH +S4AOJQd37JLupDRAVae+I4j9E55YeCG0ekyzzQWwOQ9fN1B97wJqLbE+XnTlnNrJ +QEJCXeVf58Z4tb+2BcckgOFNebx0qEBWt8RPE4fSSsCbQy1sVdy6Kmv57TJgojLs +IdSPUqT/7n6majR8F2PWEW58cGCz1j0CAwEAAaOBnjCBmzAdBgNVHQ4EFgQUak87 +JMKh7rAqm6fvREPMB8elvz0wHwYDVR0jBBgwFoAUak87JMKh7rAqm6fvREPMB8el +vz0wTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTMuY2x1c3RlcjEuZWxhc3RpY3Nl +YXJjaIIcbm9kZTMuY2x1c3RlcjEuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQABPvkNaD7cYchWR24StYKjuqGORp/kxjoboVQuM9xG +klCJ11L/P7RdsUl1Ku5YDp7OQ0UNROyRZgA6q7PwVWJBuZg0eSv3OpZhVoagKr9A +wurgMkw8jKlS5cG3bpZwMBvXhi5isFQjN27+biNs8WT+egXzNCBi9AvKvdZzuJ6n +2e1NUoFVApQVlnmijZxuDjsD825whGd1hBAb8NRPKcu0Oi4+Clho5jCUo+YQqioG +HC2ZoiweQcPsLun9pZOIdjKexReTTkEA8n/LjGvNg4XrBGJglRDX1vxjKZfeqh1l ++O6NyMKHu/d2nNmTWDeHbVMPf8q028GuedBRlnVqMe3V -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c1.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c1.key new file mode 100644 index 000000000000..2044add5984e --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAlq4Bm7bgA9tUe1neqi3pOapDiztAScoXPrmZAxgGJMhPbpjm +Mv5XLxW94LntfubKuCTt2uwbTz6N8tM242OWqeA0UQXXxoZryEqLo3kdsEvzb/MK +n0XeDIBZuNgAkmfajER05ceUrsDlzyDAHKTvqNOdbi+YFlO2/KYBl1gCi5pHsI+W +vOSH1UdLgA4lB3fsku6kNEBVp74jiP0Tnlh4IbR6TLPNBbA5D183UH3vAmotsT5e +dOWc2slAQkJd5V/nxni1v7YFxySA4U15vHSoQFa3xE8Th9JKwJtDLWxV3Loqa/nt +MmCiMuwh1I9SpP/ufqZqNHwXY9YRbnxwYLPWPQIDAQABAoIBAAG8u9ppv1qloR6G +KLtzKajQ8VQEbBDtulrhlOwLAncfs03yuvGEXfwpY5w/SIezOkvpN3zT4A5Uo4C7 +NeGYeL8Fc+6QAxVDRrBzSGARuc/m8EWU9CuZIjhB4PokM2cWUqS8Kh53SjWOYv5P +KFsSlvJhWwO1zl3jiGaQgdAwfMcGc2hZlEcpa3yFKh3Qb/mnorbnz2FQieNQmo1M +5/Y9+W/MZJgyu4Tq649lmCyfq4AsnSMaaehPmiX8iPY9Uw+4zqYupI0mTMxNVoNA +QXzl9jJafciQfmPWPPs5K0hxsTWMONmCKyKX4MhY/APFMe3YSOik9h8ImpOOhny6 +ljMkb8ECgYEAuvbM+e/W3iHM1XsrIft1DtM9sZ0dH7wuaeSeurv6ZDjKy76/dezf +32HKsU8SzvWLywh89mmeAd2XFEhE7bPpZygh5IoTSvd6TfLsn+8Us/7k9HfoGNf+ +DqZ5Oj/4vlcl+DIunLJKpFqiua97unw0PIiOxTG3U0Ww3KftW8aKogUCgYEAzlFY +bAiDnjO7bl6MwY8OiA0q0fSKEg5+sdB2Ctdpb1QIT+VSn5V68ghCaECfzQdGBYQu +bghUMFZomeSdudVaUuI0SCHVBqJ9eMvUvgkw1AfADEICPAgcmNh9Vx5zcDUANSm6 +kBKl2xMulhoYkzMF2X1jAZLacjVaUM9pF0TWgNkCgYEAthsSCBgeN3rAouc3WgAt +QmVq4AdiiLiolKgEG2phNyFCFjYTEu/IFzKPVSjp7v+oS/hWj0TMjJCjSTQjP0xP ++RRT/uCmbgyi6i3FYPpkNfX+WEg6n4gyg16PEbehtvP3ncpzMFlQ7skBWs+cOkYk +Mxnnxhbos4lb/XDrdsrp2WkCgYEAvULj4R6iVoLQQnvblUe9JdMmX7l62vuO/2Di +mClpH+YvRdNxStFeGh63hFkb/oHYmwDZcriTOlBZ3ipZAfsUw4mcOjtG5rIydZhO +DRHzR93gL55x3E34pcGaoUtezCTijuI7sSRaHzBmgO1pc8bPa+bY5r2nRDNrKUxK +9DyCpWkCgYAkXW9PcgfnV/7F7tGDJaCOC5QOQ8mDqYauoNKlUHGepTfaiV10jVSw +7m5PMn9lNwMjfeBoP5PLtn6CT9HPrMSZCh+O1HvTM3J8GcYRsET/mxZzhldJgN+g +7rfjCQDVAITIDCJ/yJnviU//ejYFvXsUf+R8BCmLOeU/cqkdHru6tw== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c2.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c2.crt index 219424ef4c86..ddeeb7588604 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c2.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c2.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAMevBYYMfrY3DQ3H2TBDSH5DKJYdMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW4zLmMyMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjMuYzIwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBALEnh4sMjt8W7vwIeW/PirZad6tqKbiCeGyhSWNL07VGDKUsAfF6RY6voyGj -Bow/fPxQ7OXjlw5BFEgO3DxlTbxLcNk06JV7d4RrfHhCGF3R3x1L/icTg+rB+QdA -aMdHAiU6Fme5qGUZg7k2C0JO9mTnJO4tIsL+frPGRFmlNZyzAgMBAAGjfzB9MB0G -A1UdDgQWBBT9XQWzCcEEuYHJXQVr447M/HqFtTAfBgNVHSMEGDAWgBT9XQWzCcEE -uYHJXQVr447M/HqFtTAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMy5jbHVzdGVy -Mi5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEABxKm -Id8s0A6ShwMzQT6C0QfgyYiI/dJ7ghAAg2nJ6P6Gl4WBQNSDkY/UGFXJALIud1Lo -Eb0x7omu7k8fYd9GpbWzWgNLwy9lRfvndzHbCLmCJjdH88jqvkN+lzINcEu2hjX6 -dEaR79lYBj3Kam9tLQCqRd66zN9/yt1N3oNCwo4= +MIIDTTCCAjWgAwIBAgIUD81S15BOvBDd3oEF6AYLabSHYhgwDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjMuYzIwHhcNMjIxMTI2MjEyMjU3WhcNNDIxMTIxMjEy +MjU3WjAQMQ4wDAYDVQQDEwVuMy5jMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALHIlrzwnyeSPpiJVTHprJZXBOaBEpz1wZ/xCWzY8/eHKMij2NTmYXr4 +i1rmdkCwP9WsomRN/E990K8yhjRCHvSIU8C3rUP+jfgnWIOJOPcGsJdcDFiWPyDD +nJ9dncGAYgUAI5iSESOlLx4VGEtdY0nnJZJMurK0VPo8DZ416R10t1WDv0zBXfqK +NsQy7TXlPYov9t/MWSCJc5NW0WNP9q6bbqtXIS0ieb86tcAqccgkpBlD6+9WvDtl +zVcAPQsRUd4C4hfBGUMOx3gcUsJ6Ue8Mno4txKwcyrxTBVNAozFnGjYi19ytUvO/ +btMkHNlN0JVXQB/8lygEDndn+APfPmkCAwEAAaOBnjCBmzAdBgNVHQ4EFgQUZsmn +44JkgHLJ9z4OIYuQf4zuMoowHwYDVR0jBBgwFoAUZsmn44JkgHLJ9z4OIYuQf4zu +MoowTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTMuY2x1c3RlcjIuZWxhc3RpY3Nl +YXJjaIIcbm9kZTMuY2x1c3RlcjIuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQCSJ5dLXlYQcET/luNsb0oW3Y83I/287zX/Ddu0bxLl +pUcIj9LqroIqnsZ69k3SVTYMxVE1O+UiirQfU95qzveybYZVls1H7ejvM0rgNa5A +FTtZfFIyy8yLAT00qqjQcOXTEaW7iu0RJgFMiExvJ3by/nO23MsFv/Cei8luvZlU +Lb8P8Hsfck0UdS0ANMHd+wmFTatyqbk8oqg6vllouaJ1bNvg3XqJvBfRfxwB1Vkd +EJNXbCfBprgEsj4cDmC2k2AlVa+hZCWsSu4AIN3+7Bnnt/1QCFEi+F5C5aNfAZp8 +aq8VPwBOt68KP7WdZN8RBg7jfGKhUj3rWU6tY49AhVcH -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c2.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c2.key new file mode 100644 index 000000000000..b0be430a7d61 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAsciWvPCfJ5I+mIlVMemsllcE5oESnPXBn/EJbNjz94coyKPY +1OZheviLWuZ2QLA/1ayiZE38T33QrzKGNEIe9IhTwLetQ/6N+CdYg4k49wawl1wM +WJY/IMOcn12dwYBiBQAjmJIRI6UvHhUYS11jSeclkky6srRU+jwNnjXpHXS3VYO/ +TMFd+oo2xDLtNeU9ii/238xZIIlzk1bRY0/2rptuq1chLSJ5vzq1wCpxyCSkGUPr +71a8O2XNVwA9CxFR3gLiF8EZQw7HeBxSwnpR7wyeji3ErBzKvFMFU0CjMWcaNiLX +3K1S879u0yQc2U3QlVdAH/yXKAQOd2f4A98+aQIDAQABAoIBAFZ79uA4yaONYAgC +HsCmJQAJmueq+GwEnUl6DxS1QZEzXeKk2hFPdTJGADybCgGT+6mtTl4vFcNxt/fS +QzjexYI9Y0jH0XDXPybttQBf26dHyNYIwDTD+jetEN7xwpFkK8q085v/ynilnZMr +veLDrk5ANKXDz7qpQQFt/03sedeKo9ZlZKWcLwSMR5YjAPjf7MteB+OhaWyyaeY5 +TlGaQs66PkPLKAhvbV4Q28eKE5IerXeb9RQE/XQSWOkWxSS3Sm9QSPFNfJ4PL/tm +zczg/zy5mXZdPBM6d8WhCZ5SFoiOruxDJEWRUiEXEniX52OHYp34ibJ1UYhbHzbF +8rUX22UCgYEA7tHwOg+0s7jBT2bME0LWmwZfk0t9ZuIBOS1sNT/uIvagjVg1EFxb +XMzSqlrYEfKD143GXtQWB2uDJ0qGm4YnfojttJmKvSdGqBnVzjayIUmLxd3mHsZn +llVjk13JTewadW1y/5CZXTdLtwYX/4HzQ7Z89wF3e0v0Kto9w/olMBMCgYEAvpKd +RLPUGxAHhQD1DzA6vtmVFh4z6Ma0LcUiBQZl0IfpsDSuMjkZGXto/DlkPWq7jL7O +TU80ld9cSPy3DritrDChIMsH09A0zF/6wSA3MaLrQ8JRP4uDFx/bRavZydO1DVdY +SoIbLFPvCTwGZ9CQ96w/S8DHe9XLJBIJxluYPxMCgYBK/LxzXq8cp2x7ZAm8GoaM +Eso7075CacB8lZied75sWAg9mW6//l/pmZ6kQvYJXJ6tNiUNbYalOcrqsWnmt477 +Gj+ZL4Tys7xp2ALcRyxTa2Zp8KzrCN1JxPIJOa+VugbszDsJp/uf21b41v7+CW26 +2zgNzvYXTtgSBIjUWl0G5QKBgH/OdAqPT8CWj3Rzn+X/wmYcJhAfUVDbchsACkC0 +ebPQcrjpm6mzAVJFobMGCmPmIMHTdSgD5pBcq9eHJH2K/W5tbqI/h5BWIhKui8zZ +fiqbyIw41aR4PAJWKwgeqIDiqpm36k6N0wt3fugCiYk2JwLzUb/MGfzC9OnL7yJI +a2JhAoGAMyETGPuKusdSFbiwTyhghPkXHBGj2GdBauKDQuiVzGdaay53SNxEMuzw +r/6BD2lVTYah6vSnikXspACFpJgs+OlFUcGLdPjNgdr7hCxBYiv5n7Ljjb4PcWGO +5QFOUPiCbL96rM3ni9mpuwOOApNIp104Ity/Rc4qcExA0QNoaU8= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c3.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c3.crt index 27727392f100..91527a277275 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c3.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c3.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUUzIsCAQr4iJUUIBY0ZWZuZfi8ZAwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjMuYzMwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0MTMy -MjE0WjAQMQ4wDAYDVQQDEwVuMy5jMzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAlMorfk1gUwZci3ta792qR6j50VUKQ5AqZrvOXk74eYzfPZiE8TJwnphGdfMm -8Ru4v04+4pf5tTzrl0L+dKebSLvVWYE+VPz/So9jyb2gHT7/I1ypEjum7iUpSiVz -M09exmSyMrcyRpWSRQcrMomC4EK5OwDctUv1EUqAlgbmuqECAwEAAaN/MH0wHQYD -VR0OBBYEFOkY81GYZIxcO6x/wKKrfPah0ThRMB8GA1UdIwQYMBaAFOkY81GYZIxc -O6x/wKKrfPah0ThRMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGUzLmNsdXN0ZXIz -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQAGGvtY -Byl4t9Jv1ykFwOnKZIBeUK31TxYQ3YP6w7Dodu7HJ9AZl+QF9di3FXuoAvwK8coD -djuyqvvh6fYOdXKZDG2ZEbGHrQlGV+VXQw2OwdY0r9TwCjLT6MPqJeqiONQ/M2py -JM+mshS35mZpsD1ZCtbw6M/CFJrMVQMP6y05xQ== +MIIDTTCCAjWgAwIBAgIUM2bQVJR8CQZ4l4oZ1n4h6UwYV0swDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjMuYzMwHhcNMjIxMTI2MjEyMjU3WhcNNDIxMTIxMjEy +MjU3WjAQMQ4wDAYDVQQDEwVuMy5jMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAJ6NE/DRhhGMmi3e/8iIuzZCcAi+IFKX2RZ6UtjVrX8Wq7p7QlQswpgX +C1g4PxCa5bbZMxP5086WSqd/1v6FX42tOV2F4Gny1mBrZEtjxFsxNiUTae/D4W38 +juiWq0IxkWmXTFPR3zdxuoOgA56XdRhAT2Zv+Gl3rBYztH9mft2GAwaIIWS6ol34 +cOyj3sgsAaeHMhUNRxf/EAdNvkjjFATRFmQZZTKIoXCXF8ld9C3d4zxVSmGiLCQ2 +Vhgdk5xQuzIqfmEJyzuzWT7n3lL5ZYNb6CQ5qqki74zyef0cButKcHFMslFnzEio +hcB/YC5fWT5PyuM6c2Z+mGVqKM14iBUCAwEAAaOBnjCBmzAdBgNVHQ4EFgQUa0ER +qF7M233CRElTl8TWZpD8p4owHwYDVR0jBBgwFoAUa0ERqF7M233CRElTl8TWZpD8 +p4owTgYDVR0RBEcwRYIcbm9kZTMuY2x1c3RlcjMuZWxhc3RpY3NlYXJjaKAlBgNV +BAOgHgwcbm9kZTMuY2x1c3RlcjMuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQB4vTz4ZViLlUVJIyVgJMxWvU7DKBB/ax0evMjAG5an +lW3Wmmpz2mP1WnFHBU0NTr2drv1JZhLmrsgZFNp0Iqf+khfx2ZpUdvt9gX44S6I9 +x+1ca8mpOoO+5FvXngB9Qb955CY5k1JvouD6W5LFvQ0ubvv09a4y427bxZI9xwex +C+mPcMEUnbVm4UBm0SU8zCl/4WEr03LLVi2zMLoKyzkXKz6h2vA/Yqno9bN35h65 +xAdtFybVYzyGE2p/9wNcuwmQoBQbWb9M09nGul+SddZcFJVju7pQ0k7kwdEhSX7c +PQQ142Hofw+D+Az4CpPLN/HOCOTHnaJckqdJLM8dx54M -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c3.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c3.key new file mode 100644 index 000000000000..301f4ed6313c --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c3.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAno0T8NGGEYyaLd7/yIi7NkJwCL4gUpfZFnpS2NWtfxaruntC +VCzCmBcLWDg/EJrlttkzE/nTzpZKp3/W/oVfja05XYXgafLWYGtkS2PEWzE2JRNp +78PhbfyO6JarQjGRaZdMU9HfN3G6g6ADnpd1GEBPZm/4aXesFjO0f2Z+3YYDBogh +ZLqiXfhw7KPeyCwBp4cyFQ1HF/8QB02+SOMUBNEWZBllMoihcJcXyV30Ld3jPFVK +YaIsJDZWGB2TnFC7Mip+YQnLO7NZPufeUvllg1voJDmqqSLvjPJ5/RwG60pwcUyy +UWfMSKiFwH9gLl9ZPk/K4zpzZn6YZWoozXiIFQIDAQABAoIBAA9fEpaXnaWgacc2 +NjqpJOtVBBsY4bXYMmwHNnZ+utgWKNaGwi66ezA3Fbq7E0QnHEu+oYDXiPUfpYO5 +NW0OHa4049mhE9l5uVZ2Ou5DMSN9MShlijD8XuPkpy8+AkMD3rrrt/LtZzRvInvF +3Ov4d5n/DCOcieshts2dPCvY0kZkI1/URrcciCEapcfPCXLnIsFXWDY5wwFp4sSc +KTPkwyEceFEFQoJ+/MW3QYn9xMn14xJfATllP2M6hzzgPy8eu+jXcPkYnlHYk8JU +0ijv+bQuE/3vrRKodZ8SSBA90EhMiPWHPWx/cfTHNpsUCfVeXesSRPGGW8mTCWwK +fefiUOkCgYEAxpA3+2cub8EImcyWxXXtTvkAQFgofBg3CRS2ZQa6IhvpBD200+D+ +O7/MLJ3qyP9AVS6ZChtWr4K+SHbWkbsuEmVgE5y7+o1RF4BmtNNpaqYZP/BJXX0X +viothVR58rxgVWfflgIvnqV0ZQi3XHLqv0XALmODlH6XwjvzYHom8i0CgYEAzGnq +3/BNUt7nCpa/CNMP++wEercFuMAQtICCdxoXf3pX88p8UW0zW4TanzX+Z1ZvqXYd +2kLpWzr2J+dYJhizQMubrTxT4eMPAD6WQT/lnPWgC4e1ErTH0yx7HeArXCYZGtzv +zpaZvDBRDSzNGZBPiDcQrvh5ldnBqwmG0g+PZokCgYAz3GUSv49MVvGFIf9bEYLq +tnRPWfcw+i4drBA7cLdaw0ln9ynskP414tFT8ebpmmlWFjaYgi1w58gFKyy9oVLW +MS4X0T7zoMdGSR4KKpGE7l736S1wQoTaATAke7ziHjtW8ytdhbKkT/iHjvA9MDKJ +xJD+TLWIKkw+yeO2FhFn/QKBgAtXRQSvnFqgq0jXHDotG5HqgkUcqmCZp1OcXaCl +/e/ApeiMr2EFIfhN4qM6p6asEP3h474cQD4jg0yrdSSH8koCAd0Wn8DEjCyDTGrK +VL7kkcCSAuXKydtZeGsr6LvwESyAXX2ktVrPg2+rsKVecEsqYu6SMoyxzCDlImm9 +G0OhAoGAHXnN0xmk8aUoHpIo6GCezBOYlnNMGW3P/GUN52QIiDExPXPnq7/N5rik +oY/rDX0KQ3JP5gJl1kKg87Nlfw9Ix2p7L6cL0xBRhlDlwDtB6GUFqZmSbdVRcwEv +gct7CVZ+baR5O52Ln+U0Rhkg08W4/PLNRzgbygun643oClXFIRU= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c4.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c4.crt index 911e756f2860..0c6b287b2832 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c4.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c4.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAMasz2/hWiW7UURuyjgybK8+NYpbMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW4zLmM0MB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjMuYzQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAIwFiuTI0adVuF91ltRwyCzA2Eb9fV8JUKmlVXfjElP+XGG2CUerk8H1Ps0n -PDFBCZB+0VM5BzjrtsEYmuEk7LJDXzoEj6L2MiYKIVia+qxzLvSfbcdJsGNXDrKn -47sKDrUJbBDVodI43xGCplWyyhf0MS6hMe+7ttwb+LIZzTFLAgMBAAGjfzB9MB0G -A1UdDgQWBBQJKDvefckZz3in61z5pdM6CpIwYTAfBgNVHSMEGDAWgBQJKDvefckZ -z3in61z5pdM6CpIwYTAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMy5jbHVzdGVy -NC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAS/6P -0hx5NljLIKafQ4/MfompNgk4/yTY2eJfBuw7qdpqdfe1+bd2jB8ptuQ7t+vR8j+F -42EmDGwZHwFVqr5PkXfhn9Drrx9muSXeXLheZ9FeknvR1n0upZ4KnKq9sqaN+JJh -BnxpDpp3wLmP6LwWxHrAxWtiCvjngDh/hXx/P4M= +MIIDTjCCAjagAwIBAgIVAOK+JDapeK8JH+vKe+rcKZSaox9XMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW4zLmM0MB4XDTIyMTEyNjIxMjI1N1oXDTQyMTEyMTIx +MjI1N1owEDEOMAwGA1UEAxMFbjMuYzQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDoA1LW6tTsYF3DsZuh9QRZUwyB6bflTIukLVOJP7bLfNktvRQg2Aup +z3LxZsiiqGc0ujtBFh3tV3dpOBTB94irZaKn5sOdiVG5WnlOUCkdxz4pOdnYZuec +pyfP2U+8aktje7QErbGHwRqDk9GljUiC+UbPkkkDBxLhnRmNwZTSRls4XdbIo+R3 +aOalyykDMd29Sk5WkEVhZsqaErp1emv4Yc35OvLhdRSAxpwuqnS6kStmsuQZBUQY +NuD3sn3n36l8e3hX993hoqFHsz4TIXnsTQqWWN2ybqOGtt31Ss2nudSwIsyqwSg0 +tQ6mp+scv5djZCaDAH3dvsPo+8hgMTITAgMBAAGjgZ4wgZswHQYDVR0OBBYEFLX4 +zI0P/tAGizJCmdIm3QvGVgLpMB8GA1UdIwQYMBaAFLX4zI0P/tAGizJCmdIm3QvG +VgLpME4GA1UdEQRHMEWCHG5vZGUzLmNsdXN0ZXI0LmVsYXN0aWNzZWFyY2igJQYD +VQQDoB4MHG5vZGUzLmNsdXN0ZXI0LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAvd7wKrRc42DfsRqCeJwgqe/CZkn2o54ZPi+PbZsz +Aw0XwVnUOkA4wpCxctdmO36XzxgoPFMZahZ/7Ik5M8UkU9/qWZIMR4eoMxcGL9vU +qSAmaT/TAhSysyrFE/WQjKhKb5HcY6P9G+t5Uq8dLqclq13d4II0HbGx0BN3H/D3 +rBFaqVmVgIvZ7foYHHKkD3rRdhEYzWqH/d2BTO2+LPuhbQ0wCTzy7rFMpnXq9/tJ +1g3FMtAWtaqgLMjULGrJpR70ikRjN0s3tTzW3xJ3zHrntcfmKgE6iNyENrePpWpb +I1KbR6SCTuWPTo7XJWhwg8s/z2YKzfjosPKQNlfa3t5tyw== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c4.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c4.key new file mode 100644 index 000000000000..3859cc57e80a --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c4.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA6ANS1urU7GBdw7GbofUEWVMMgem35UyLpC1TiT+2y3zZLb0U +INgLqc9y8WbIoqhnNLo7QRYd7Vd3aTgUwfeIq2Wip+bDnYlRuVp5TlApHcc+KTnZ +2GbnnKcnz9lPvGpLY3u0BK2xh8Eag5PRpY1IgvlGz5JJAwcS4Z0ZjcGU0kZbOF3W +yKPkd2jmpcspAzHdvUpOVpBFYWbKmhK6dXpr+GHN+Try4XUUgMacLqp0upErZrLk +GQVEGDbg97J959+pfHt4V/fd4aKhR7M+EyF57E0Klljdsm6jhrbd9UrNp7nUsCLM +qsEoNLUOpqfrHL+XY2QmgwB93b7D6PvIYDEyEwIDAQABAoIBADFQk4kO/RcJHKT9 +2WDBXZ1G47+xV98rN/s7jyf7HPV/ogHw0Jhyk3gSmhHpkPfR563zb6jT9KSAFOeO +fgsEfE5CGiKqECYqC+7j5ijZLQQhV3sXytAOf2hytymcvTAFP+TzZD4iVTvPtb0F +7GfN8f200nJIW7CDDmZaKBZTcTiISDwF+ia+vnYaS3dGrQ71XUebDEn7m4yvd2Om +WRaxLQUrbUBuQb8FikqKV1Y0+we87BIA/IWZCnUmnbTGeo1yquSQdvxefX/WNaWU +uYHHNtQKzHPaGk6JZeAvJyifWZAHE6xAPcsApjqQOyJSsJKc7MQzzZXwe4tofvoP +iKrKYfECgYEA8ffvhkh4XSw9hNiHD+K3X6/wGWX3gqWFrUsInnRjK/O7fON0SaAw +ajjbXMrVnT9h8QgUkD8oOO49trRWrzzbTGPtNFltOsrRVLWlo8zAFESn5F7ZyG3z +CQd5fciB4xhQZdppH3ARZUyoYct6+TQ/vTP7KANRWtEjedVKLtU02SUCgYEA9XeY +ugxf61Njg+G8bbUSbxyZibcTpFT7nRYJeJJORyRvSVUT2AVVZsKYn8nueV/pujlU +Svt3Ow1ueNH3JatAfSkbfMQJceeXrSrDoZR41fSndK1cMxBxYMMk+3hFSt7KlRwA +uUMYauRm/QJStnzjsNoEb5AAjyX3ibChC+tNRNcCgYEAnRhAjWk9pBhV9Irz3U9v +iJNcidDWyh6W1HSzqtSbkmySnDm8RRTYeg5h9TPHTmlZODjryN8ISTX8IUOiR/F1 +23nBBkzNCu+kyp+IY/lJ956/dhhSwrsHoCQXL5/fHEar5ATSXVPQJ3mGlL+cHnFa +tIcsF8VXq2O+6ZmdqRxJTHUCgYEAwUvkoN2NyypnezsPvvSQB59l6OSaOxYQ7aqk +2qWbrroqSH0Om1XX4ovq1CmX+UxmKZdroYBUa9Qv4kS1cpxgtC7izmUxatkMv/il +BWB9pz55f4/kwvF31tLVFJGpP0QiEzlx+HG1kLc7wpFJwRqn0mRcid3EEvW68Fp+ +93vb5RECgYATHqozTc9BwKYJpES7x5Cy06QNFHs3DH607qFj++aV6MMdpXdaXia0 +zZxnQ2udXYXcsAztCtq0nC77Z++O0f4s7EfppleLJB1/mJH8hE6Z/Zg/O+L4O5hn +CTeYio03rg/vavaWKHjvleuWzz1HQQOFqLFM/Xu5xASfUJ2Cat0PFw== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c5.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c5.crt index 1132dabfb9e6..c4b447e2fc8d 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c5.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c5.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUCiG69NDv2GoMmFE3qaYNb/8lYRwwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjMuYzUwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0MTMy -MjE0WjAQMQ4wDAYDVQQDEwVuMy5jNTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAm+gpSNeO4opovT+vEOBd34pKfiCaIqoe0D+vR60gB35glziDShITOGMs04qU -kM44nnv+d/Qib2BdkNlze+l/Sv92r+0DQp+MRzdNE9GkuV4B8yF99hpi71hJMED1 -lkrYRLUiEFUjmHccUoO6fF8BbdQwvD3rBTpAss1nDuO4BncCAwEAAaN/MH0wHQYD -VR0OBBYEFFT81Tir0l9q5Cr5n47pDeY9pr4EMB8GA1UdIwQYMBaAFFT81Tir0l9q -5Cr5n47pDeY9pr4EMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGUzLmNsdXN0ZXI1 -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQBeApiQ -M3HD7EzECcaSFMOhCpgmCQg/fV3gDaKSe8pbbzbSkKqoYsLwQM7HNUV+sEhrl+e0 -G9vR3cEeDMhuGuthVeqJ6jhgqn9vjXL2cLsDzgQYvj5AEOQe1/RfYWVOqYAuBtMk -M+k7Rwrddd5+Fko2LtNXGskYwv1ExHSOl3MYSA== +MIIDTjCCAjagAwIBAgIVAPVrbCMKMdUaDmrid+l9dmN7OS1lMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW4zLmM1MB4XDTIyMTEyNjIxMjI1N1oXDTQyMTEyMTIx +MjI1N1owEDEOMAwGA1UEAxMFbjMuYzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDscm/Dykq174Z2bmcnZAh6IChHnJ9GQnKXVv5YTLtNTKEjSVP+LbwN +Q6mY+vBW98nRLVy4N285q0DQHr/y4Fkn3LWJ6tWf+2j5JW8h9McKmb/6g4UUm/I3 +KsOWmMTl3ODmarWUwqh1s/eo/UDsjHEfe2NR0RxNFSwWxJfnupZUlE2fGA93mEJp +RCUgD0HQlX2gk28IvUUagS9JJFwGbz3RFMG9l26eXVVnabxyjkBvPutr22cDdFir +kNOfNMM3S53nPsLNEviXJ/JblfFEl23oDfWheO7eP3uAzdbT0ZnmPkATCBvwm7t2 +7KmqFhn9oEdycnD0CSKpkFKcSbRnSr0BAgMBAAGjgZ4wgZswHQYDVR0OBBYEFGME +6PtWgSt6pf0KfCyk9N4sne6UMB8GA1UdIwQYMBaAFGME6PtWgSt6pf0KfCyk9N4s +ne6UME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGUzLmNsdXN0ZXI1LmVsYXN0aWNz +ZWFyY2iCHG5vZGUzLmNsdXN0ZXI1LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEABSDAns/uTOizogdOmNEbi3M2byGrpAYAYfnMI2eD +kM3H3p7JWpbbEm7a3NZbFgc/mviff6JWFboSiyGv0lv7zc9US8c0iLE37/Nd0j/c +BYgP4avub7MlixNv/duffrTb6vwipQQlpfKpQ9mSUUIZLvDEcaIfQ8PlRTG9Y6Vb +zO97nOwkq0a6u/8mENY1M98EsJKLJc5ElDt6zTTUOJSlVJ7fWeWDrv5OYgXLB+d1 +d+PDXl9A0KkinkwhBajjykjHvFlb54sxXYtSjT9SLzS/8m/6HofwmaeLeiJcOnCx +tw1qqw5qELrKYCtvLUAxeNrN/Wp1kHZgkaMXyZ12TbQVuw== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c5.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c5.key new file mode 100644 index 000000000000..127a90999459 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c5.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEoQIBAAKCAQEA7HJvw8pKte+Gdm5nJ2QIeiAoR5yfRkJyl1b+WEy7TUyhI0lT +/i28DUOpmPrwVvfJ0S1cuDdvOatA0B6/8uBZJ9y1ierVn/to+SVvIfTHCpm/+oOF +FJvyNyrDlpjE5dzg5mq1lMKodbP3qP1A7IxxH3tjUdEcTRUsFsSX57qWVJRNnxgP +d5hCaUQlIA9B0JV9oJNvCL1FGoEvSSRcBm890RTBvZdunl1VZ2m8co5Abz7ra9tn +A3RYq5DTnzTDN0ud5z7CzRL4lyfyW5XxRJdt6A31oXju3j97gM3W09GZ5j5AEwgb +8Ju7duypqhYZ/aBHcnJw9AkiqZBSnEm0Z0q9AQIDAQABAoH/Lg3YIR2c0bUWzqiQ +ZQbB8++hiNlD2K6GnV2vasl/Tf4YAMR+nKqRz0+XkBmIiJf48kpQnf0GKAhoNmFW +59q04uPFZFieRWv1tVmKPOXtgu4Ri5zkZfA+DGrC7+lPqKoOEDNGrPQBVfO40GyL +ocd1HTmuTbhaN9Vg4gpmwBcCwvREBqQZXnklPZjMsuTDCnObETjWpldqKsrex/kv +tOCnTvyHFYblc7kQrRD4ywzC9j3imEz7Fs4WHEou+lEP2Z1E0m7CjjhBPcio5AIx +1WuAs4wS7cCcCc7ZYS6uiMBVJsOaF7j3Kp+Si5VzhNHKZyzMsHFQENVWfA0f4yFN +UWIBAoGBAO1U7s1nHsLNOtIrTtXzlQ9EKitNrTrbmSg07Ep34So1wctXqwSYGNMy +vZQWWxzHD2bqooOCk2E3NShaRophQshPxDZjIqQ9HypOTmh/j5FBJi0PUdUu95rR +rBAzgF6a7lT29vEoNxZXOAhtMOkyT732aRXvlqSLzRzwegLcBoYBAoGBAP8LsBYn +IFICjlJhcItTNkSXF/Hc/oB8Ig4KiH6xqe3UpOKVNpPObLDFPB01kCIh+1FxAaE5 ++z16fSzfpx/QarF1eV2Q7d+hu3oBThNGrbFkh0UijowhGB+TUIyouRPYKYlLsjWu +SXei54bmg0xpD9fGFDcOXcUFuYEcwaBIejcBAoGAVL1GpsBzE0sD/ZbJB0t5pYHJ +LL0HZUSTYu/CjrRPCSs4jso2neVQt9geKIny7kuFDFIqxJoS1VtI96eEGFSx66PP +gzvSWqIYRpq/FEC3mj2Qy/X0WhHzBqxmq0I4S482GdA0S9b4FFn//8I8/IaiQkmB +K/vn5cik6/hRGERHcAECgYAFxnH6TnCA6uXmH6a4wO2dp/42z9ZDeowa3Uox9Dvw +0rvZFZ/iwO3dIto1nxzgaPyT34c162YxAjELbxUNt15zHkHdSIbaZYMlRBpMfXE5 +bxlRC3NG4Qj2zhMo3Dk0pdRq83fZl1S6jMnJWDbo6AgfzrBufrjI2VRWBByuUHXU +AQKBgQDbjFt4jwddHTkSzgkZq/gTJn4PaK9ZY7UOpukvNYI4HdOIcze6KtvVc710 +elYVTEz4+wIZTJlFVJv6XEAOoEWm0+z3DELy6Sbu5zydgJ1dY27U9UuFJc1iK0oP +nyvqQ/EflTMlu2rKIvMWVCXHgtj1+hzQnhjdsVaP8yBk28i8Eg== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c6.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c6.crt index ab338792607e..a6fb40f71fc8 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c6.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c6.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVANXn8r0H2YaPh9YLfjlI7c/QtXNVMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW4zLmM2MB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjMuYzYwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAI9iaWrpHwmXPBC3YhfzkVe10A7AKjRzdo5uv4ra8xQmVzsdk3lQ+oQDYMDF -oP7rfxtRLzB2Z5l5rChFbkLav+3G8DJ09Vdzs28J9Kxz7N/H9tEiTmgvzdPVBWRy -A7D2WPPiaHBeIIrtgzfZbHenyxuDxfNtvgvSgUZMEjM7Vg83AgMBAAGjfzB9MB0G -A1UdDgQWBBSADI6qBveWvxd9C8xucsFtogIDRTAfBgNVHSMEGDAWgBSADI6qBveW -vxd9C8xucsFtogIDRTAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMy5jbHVzdGVy -Ni5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEABgqe -FBbsrBz4276uUtr0YkmUlVUoOYewotLq267YGFH/kQkLJbN/Yvdow++7xn0+W2Wo -LqkwlMfzZicpnm5TgqKO9SQCXxMonhCyvNIuJgShkADIgwKnLNQq4PpV7gyN+ePV -RUCAQJdcLMPt5SH+q9E528K28WJ1aVWarDDkxEo= +MIIDTTCCAjWgAwIBAgIUB8643/7eGO7b2AKA6fGzsqpWsp8wDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjMuYzYwHhcNMjIxMTI2MjEyMjU3WhcNNDIxMTIxMjEy +MjU3WjAQMQ4wDAYDVQQDEwVuMy5jNjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALyp8UaakM8Z5UlHPlPP458akRIOzuYUZrsnKHFZDBpX0y00B9VEkbwS +XH857PsIOuhJAzEQ+OPn8MMookyFmyeWB5gokSYud4PyOy0HPbPcdw0eVaYi8EQ0 +PfkQ2qvGeFT2HNBIDwX8Tj8BogAOQZ4JK2YSgsRftpOPYuvn9rfrIWmjYvs1eFbw +zYyETsY0Tk3auVP2ecHkznfliSRgyy7FpKf10f1Sd61MidSPo8A0tBv/xe3gOCz/ +aUU5HY8Io94ToJljF7q381bB3nO67k8IaU7ApAunt7bToHMQtKfnPR8/qFLW964S +jtoZEtJzFdt+VoxwB0p/4TWfhe1wIfMCAwEAAaOBnjCBmzAdBgNVHQ4EFgQUkYUe +kyCWC/5aEzYV+yx4s18doHEwHwYDVR0jBBgwFoAUkYUekyCWC/5aEzYV+yx4s18d +oHEwTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTMuY2x1c3RlcjYuZWxhc3RpY3Nl +YXJjaIIcbm9kZTMuY2x1c3RlcjYuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQAiJbE3qFFQ8M22RgzLoyQnV8gcQ2xKTBi9S+I0nZEV +xXL0VY+W72LiJvaG4buXNjpKCo+bHnL03xHNOlSFjyjlsZCDvm+LvPT6e+67y/L4 +mlMpyIe3k6txSG5RUV7lHi2tpvpC5Wfu6nJLn5+70JdgxmOLXRzvqYFKmEZTrh8C +dfqOSQvnHKoz9TWdoyElO66e/+7vUFIBn+y+LQjSyK12HN85PEZqvyroHRfq5gni +l+ntElEbun164qaou1m4eBfR8O76vl5CPongnw1VgJ0J0xn9T+gBey7o6sUINWNR +/79I/gpYVd+6Yx4c/CxfsnrZIZQHY5qwDBonb9AYQRm5 -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c6.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c6.key new file mode 100644 index 000000000000..dc4e1c7700c1 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c6.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAvKnxRpqQzxnlSUc+U8/jnxqREg7O5hRmuycocVkMGlfTLTQH +1USRvBJcfzns+wg66EkDMRD44+fwwyiiTIWbJ5YHmCiRJi53g/I7LQc9s9x3DR5V +piLwRDQ9+RDaq8Z4VPYc0EgPBfxOPwGiAA5BngkrZhKCxF+2k49i6+f2t+shaaNi ++zV4VvDNjIROxjROTdq5U/Z5weTOd+WJJGDLLsWkp/XR/VJ3rUyJ1I+jwDS0G//F +7eA4LP9pRTkdjwij3hOgmWMXurfzVsHec7ruTwhpTsCkC6e3ttOgcxC0p+c9Hz+o +Utb3rhKO2hkS0nMV235WjHAHSn/hNZ+F7XAh8wIDAQABAoIBAFg2kcT0cm8TaZz4 +vdDMbF6xOlGaMO1tbpdleMh+3WTX2brSEDOMyyOH5k3qlL7Pm1OPB/oCOI2/nlhy +AGkNM32v7b8cgLm8kk9UjIzCTPxzQgsVOvDidzs8vhld4xDcTNlU9iYA+11OGnvl +VIz4HYA6Q1uHnjMZmbGnIpaxMC/P555xIqTGc+7LMLfzmA9hqpgOBItvYg0ocW+r +1rDSavJu/CGkuVocCBqzwVeGjV8Hd5F/ThLBDaxFYBz08MdLy+S2H/hpCzobcSok +OurEFWIcStSD79Wgg7FzaymmMdJMs5742fh3lerYUnLxM/4ccVqMKrU13Lo1chBI +XL9+MXECgYEAxzT9uiFZYFdOmzYweqy+VBkoqnsFHs3Ir/am0f2ibf/IWoU0fRu1 +DSELlWK9L/iasLQY7NabRPBoKBK/UNfFztKB8NcS4AYnIZ3yYKDec/ocprEz4htY +62ThIjIDr+qqlvcd4Ft6Mwp4KojaiaQQhI8Nl5D4M0Kxrp8byxthxAkCgYEA8nN3 +BnplB/TQUxwe4Qg2KMZfS6mqpAHU74+tynPaRw3oNQCnfekY3ZJco/t1qYa3H3n9 +ZDSs4dC4VZqYOmfOn2gRRG+Bc84B2auZXz0cYwWMuPWhgbTSjPpxksFCyoF7lz9C +0HwmHp6OG870NVwyO/WztjUIWW5RveGKbfzMDRsCgYBoHBa6zzERu69WSOTfSCaU +dy2co3ySk8Pdys92kdLxICvKuzEqbW+a8/lE0nzDdG8MCKO6K5KsXeRhhSK9kndL +1xKYFx9DO5P56r1AqPJ5tG6C47uU4OwA4hETOQZaSw9qpK3Vkplz0XXp7Ooh4z/K +t6TH4LkGuUV5WKhC5wWaWQKBgCsZP3VVWJRbfpTnkM2r6gTBjrsihAT3lmSng9tT +UOAEptyVKzEEuLjahcFYuAfUvMTJqLOhcYIK2lnNS3ZGccKAe2FQ+RAe9NE+4dq+ +QEeR+vv+HiGyhSMuefX+iOL6tX+jbTsmdphG0zg6R8scgomKGdIwBzXumHVKnJME +tzk1AoGAbyNDAz+wTMijCoP5bdFeqHtiFzDlrGkGppPrBh1xTkxLTEmEz+uNsgsx +MiUrpmnr4erxtRRPihL0gbtMkLlBI5rynGChbV6IHXcqFzxUXLiuO/jNeKq3bmqc +0P+qag+WFLjpxYMY3aLVxFqylv86npsaNuHvVm1ZGNFX1vINJTQ= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c7.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c7.crt index 5aced504e995..d04fea93e805 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c7.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c7.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUXRJ0mqzIAtG7P2+PpLY08BWDIigwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjMuYzcwHhcNMTgwNDE5MTMyMjE1WhcNNDUwOTA0MTMy -MjE1WjAQMQ4wDAYDVQQDEwVuMy5jNzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAxbb/mneg3oKdKk7tMpcpC+KDBsWWvgx9tKPqHFBWIrklLEeHVA87wuWoXVoE -moPRmaJkHCuNSUOmNUsw4ZAigdCHLI+Fxgu3RXE5YcOlR/VcfIdmeTOkZTmfkAVl -6sRIPMARTeQkqaaG93WtXpSFQ+hO3RBIkJ3t3vTL7m7dUH0CAwEAAaN/MH0wHQYD -VR0OBBYEFL/uRR/fiAlWZMNJZ8ZcDCDLLkCaMB8GA1UdIwQYMBaAFL/uRR/fiAlW -ZMNJZ8ZcDCDLLkCaMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGUzLmNsdXN0ZXI3 -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQA01y/O -PSsN1yMbI8uj8G8HH+ZTXqvf70DdL4Ujy3t0l+bMUW61ZtPKVE1eoQ8e2Zc7c52n -uMuLF+ndIAaAg07aAA8ay6vCivSymgzskMfy4qludDAnenbfrK+7+elrTh+Bq/9X -h6iBhL+cb6ooOJaoynlZyHKP1HY8xq6GYY7K8A== +MIIDTjCCAjagAwIBAgIVAN6pghHjL+hceM5qySvpjDokFK61MA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW4zLmM3MB4XDTIyMTEyNjIxMjI1N1oXDTQyMTEyMTIx +MjI1N1owEDEOMAwGA1UEAxMFbjMuYzcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQD6fM4sO1MvlU/wpFm6TJ2lDtzWrfpj95cvFiALz+y8x/cMM4qObLOu +/d5Y4M81JiLtf0bWU6eouqp05wMMo47ZAlKOrrQvqKGVY+GcMuD9UFhpr69GBUon +j68jPVVztDjqAjuU9ge1n5fJZGnSx6lQ+2kyw2CN23r2X7hnGKEC7iapIBaTdJqf +aycmap/cmKy1C/3zLf5f1pf/tEztlnRSJUV3MReFCZDk/yjjAY3gjf0uoxwSwL1R +XhLUXJmIASgHJxTesXYLd52EEXJXnYBx/zQUkYYaqHNBgkLPnFeCtcNaWKNIszrb +1g4qdM9xq2Qv+IApkK6nJYrCQgcmN2FvAgMBAAGjgZ4wgZswHQYDVR0OBBYEFOjo +/YNHG2YQwcYhT5U/k9g/PWQ6MB8GA1UdIwQYMBaAFOjo/YNHG2YQwcYhT5U/k9g/ +PWQ6ME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGUzLmNsdXN0ZXI3LmVsYXN0aWNz +ZWFyY2iCHG5vZGUzLmNsdXN0ZXI3LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAp84lG6fryTC+fSgnYaT0wddnwi6MI0FHI3N1i5ic +mT/iP/iG5YbkksIzRMb7z5uzId5Ak465++VI1ogCI9FKyV2U7Vao5aJ0K0zIV927 +miANMYOWABfneFc1yP5Vr0944BE8GNdrqWozsmw2EaP/CmVK33i4bwVafvgLVrrK +hXeKUZgJL0WmMA5UXwuU8ppSxUom+/RW4lGYSxJtHccEt1ir7PKUOGWHkCOCGBju +FDSoJT6X4YyqQLCwnbDDUwpWg2//xycZ6v5L8RhN8ENIWdKtsIbuInl5oCeoQd02 +QVzsXUGXeho5C/ZaIG9AoPzZm5IgujW3sjYHQvjB2/24kw== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c7.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c7.key new file mode 100644 index 000000000000..814bbd213bdb --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c7.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA+nzOLDtTL5VP8KRZukydpQ7c1q36Y/eXLxYgC8/svMf3DDOK +jmyzrv3eWODPNSYi7X9G1lOnqLqqdOcDDKOO2QJSjq60L6ihlWPhnDLg/VBYaa+v +RgVKJ4+vIz1Vc7Q46gI7lPYHtZ+XyWRp0sepUPtpMsNgjdt69l+4ZxihAu4mqSAW +k3San2snJmqf3JistQv98y3+X9aX/7RM7ZZ0UiVFdzEXhQmQ5P8o4wGN4I39LqMc +EsC9UV4S1FyZiAEoBycU3rF2C3edhBFyV52Acf80FJGGGqhzQYJCz5xXgrXDWlij +SLM629YOKnTPcatkL/iAKZCupyWKwkIHJjdhbwIDAQABAoIBABC2xTcUWk+hge0H +umkA9b/uxvOYNk33VmIw8sB3Bk/uc+Cfi85HGJJDpFN/5LOiMNByHrLscBKFKhyU +0ZJFU6HNUt7sb1+CvA1w1bwLdRHSC6Q5sdNVRQZwwvnx/MK0stuCfKFK7WbEGGe6 +yIjlA7pAnFtjyAyPKZxI9NAER7IfOi/MyTCNB3hXWKCpkv28STSyIBD3J9gWhVeI +wnUD41evcQJRLunJhYv8QZFbDLrn/aTpwC1tbYMbZPi2iS/1SuEZJCmRFHxka3Px +pPN6j1+vKSzdem4sx4PIpPUQWvE8Kffo7hbZO7KL6m265BhKvz6E0dAp6mw/G89R +ymkXyeECgYEA/FpiIV32plbKS7LVGgTYwNTwZBqw599VOHwbwlhunysgh3RtYvZI +6Fi4fB4eaF8VrirCTknx3ohMFw7rxTtMkGePAkGwds4pD4j4KGthyS37ut36lO8x +/Y7T/hK+NEGtNB8Pidr44TotHkHE8x/VPrmHol2uGosv6BZ1bNcA/NECgYEA/huF +LHO1VTS8Mp5doutnPDHeym3uUUNPAKygj91RBksXqzu2shnDGPnCZfUHycZFo2/c +/k30GxwlPSfCjvVGi2yLh5hsarF1M2WFTZqJ4HbavLWk24enESZoNcSxpWoue3C9 +TakC2btwzDg5vCuAWjFM2B5xnlgqyZNpN0PJCj8CgYEA7A+4oDH/2tZxYqKlijNi +d/A598qoFh/q3m3qocCLqsVzJoMZGGv/A6qK2UBrfYMFCung3c96PIsPcrp4fwXb +t3HIa3n7kweDjY65ZUYeMU2cn8+q6ste3ixPzurFlqihV5J+vJxkGHyNXDJ2BGCQ +4J45bP9pMFdntxnbOrddqyECgYEA+HdMZsTVYHVqOEyl3nIzxaY15NZNG2SnUU96 +yHFtkqvGfwaqg8ew/zDopu1LKj8XRNJS0s7Ci+efegbwtPyceCQw4utlx4WxxgnU +qKcdfkB5beBnTMl90MUvTLQByG7k+Iqqb899HA2uI+IughI/NPHT49cQ3fX0EJp1 +XTaI090CgYEA+SpKH72fBMP6VyjjILmmSaVEZTlSVTUynd/61MdojgYOVHE5CvGm +e6XgX15k9bOZUgruZ9keANXVNT1npMIOntKk9z04T3t9Q93KoO9JDF5jnUJvafYk +7ctrTGwhGpGaJto57kjPy1GlgKz9jfBjlhLHVw2SookBWxjARg+HogA= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c8.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c8.crt index 87817130c15f..df0d2c8e1a5d 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c8.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c8.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAPLfYPUyngardZMqXbQSc9jFW/ZNMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW4zLmM4MB4XDTE4MDQxOTEzMjIxNVoXDTQ1MDkwNDEz -MjIxNVowEDEOMAwGA1UEAxMFbjMuYzgwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAIr6JyOARsRn4zJ//TSQ3eMigJDnDTz4qloEcGRhdsEmEkiVxmawSx5XYZeq -vQIz69axBViHQZEa1emNrgktmzR4swV8wUip8tJhcic7iegSYk40VG8w5YSDfVhC -a4JEjTF+H/0B61j+Gq81Xzqw6g4m8kJWqWCvICNDo5SKkBSzAgMBAAGjfzB9MB0G -A1UdDgQWBBRYXtwMkHRZ58jOv4Naa7AkihBRijAfBgNVHSMEGDAWgBRYXtwMkHRZ -58jOv4Naa7AkihBRijAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlMy5jbHVzdGVy -OC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAiP9A -lTjp4exnE8d71b2a2bP5+4zF/yT6Jq6eVKKUi1ChFnGpUQLHaCtEeNMrMusQfDvz -NYi9E+3TG/5/CDgkimAWxJc/gzTge2Y2HhCWV4Sm3qV2TY5gnqbN4Ib5YQaHcL/x -1fKEKHxHf+6F8CdyGlMBei1LwIx9iHqWMjuagg4= +MIIDTTCCAjWgAwIBAgIUWkcRSOeqE9XdcFAqtBIinaP0XQowDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjMuYzgwHhcNMjIxMTI2MjEyMjU3WhcNNDIxMTIxMjEy +MjU3WjAQMQ4wDAYDVQQDEwVuMy5jODCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALNIneMayIua6przMCfviWOO9Fsr9b0Nm0AFy7Y5BGa3IwPJGt755NKy +mxr3GRRdXrHxgxuPr2qxva10UdQ0+YE8+hN+ndg1jtCV2h2nD2NScebwLu4f310w +Swix+tcjecMXv7E7xC0JIfJYhsI0DK9ar2WZ3CNh85ejNlPiVSIKTQZSoDN1gZY7 +uksEabGvQocdp3thk72HFnWlncdjjGVBiyC30wbTLTSxvbzsxEVVCqTt7QtOAhrA +U5f/coRHLT4iMtMTL0Zg6p+/Kef8t04Cadto0EzdFXzznLKhDxuOSRCjJNx50f/8 +vRAuaGYwS6+E4hOBLf0DbX4p0wPojK0CAwEAAaOBnjCBmzAdBgNVHQ4EFgQUUL6D +UvnCMaHFwlqp8/V74qi1h+owHwYDVR0jBBgwFoAUUL6DUvnCMaHFwlqp8/V74qi1 +h+owTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTMuY2x1c3RlcjguZWxhc3RpY3Nl +YXJjaIIcbm9kZTMuY2x1c3RlcjguZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQB/DKPXEp6C3Alj/glG+cylPxzTTMUQxynJ+38hiqRM +n0mQ4E4szHXxRE4ctz8TLcyd41Ga/hi3sMHkeWqXjixnoyI7PIULfTw2+pYyoOt5 +tg575Tjnb3Fs4bm63PNaW3hYYDxRLSinRkj5Vt2xR0D+KpQZm7Rcaq3AcInEPWsb +kMZ3nXUvSXAtNmw7we8ZBLp7I3bKnThXNlAIP5CBcbw3Y++CmH6FuVrYb6l8HyzG +EbgDB9K4WSgR2OP8S496dzdvWPBz56RQs7nE5lKYrejx+hdcR9sliEHq9yPqPBYl +4Sku/7Fes7UhZKhuM3adUtG3knF0cnRXDk3YsmeqgsdL -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c8.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c8.key new file mode 100644 index 000000000000..699c9773a3b0 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n3.c8.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAs0id4xrIi5rqmvMwJ++JY470Wyv1vQ2bQAXLtjkEZrcjA8ka +3vnk0rKbGvcZFF1esfGDG4+varG9rXRR1DT5gTz6E36d2DWO0JXaHacPY1Jx5vAu +7h/fXTBLCLH61yN5wxe/sTvELQkh8liGwjQMr1qvZZncI2Hzl6M2U+JVIgpNBlKg +M3WBlju6SwRpsa9Chx2ne2GTvYcWdaWdx2OMZUGLILfTBtMtNLG9vOzERVUKpO3t +C04CGsBTl/9yhEctPiIy0xMvRmDqn78p5/y3TgJp22jQTN0VfPOcsqEPG45JEKMk +3HnR//y9EC5oZjBLr4TiE4Et/QNtfinTA+iMrQIDAQABAoIBACuGYSJeAbr2yKcG +jDXdzu53KbaqpPWZRIwV/B9Il1tycZM9MwNYCqfmSYaR36pUgqVTmz9JMk6GSSS/ +nRAWLk6kZZfvANYZJotYHgRd5vv3a4Sj4djYrwr6KvU49EtfpvNopBacimNemLmz +qT+jkELQEs3pPQrwAEnbi1hxFbb1/QzLjp7X9NbT3zw/rvRqRclQNFvt+HZ7LJlo +4P1SO13pASONVDYtPu1DApUudmMkxjUNcac/ntKXIpbwtikwqI5+14BpLgO2DE6F +M3PEB/AECAmD3u/Qn/MBQ3oCauloTWG1Aqw9wIoxVVX5ncTz14K3kwELI+HlLjX7 +pzBNa0ECgYEA+OGtEdwjQdnmgnSDUSnJMddol06njpSeaW730w0UDvtvcQOl7Dcv +z7hIyvspjBSGxsdC9CRaX7hE9vnSb+LAxj1hHh5QzJmQ3LzpgFBzfc/DH6e07zZs +ZGI4AbohUUktAWF1zx0R3zWFCzXJ+DUHmUWtRx5D8c4qMq9iwiuNyM0CgYEAuGlX +XCZh3pJVpsrpqVbcWgPh3O8WrRmY2BLTwgUV0f8Ijs4uYG1ingn4h6ijJqisdFDQ +Q/25Al3AibFbudly34fbBqvqObpNnL+EVgNQsGfTiMRmyvX+ZGjKQW74rNuSbinT +KyMmMSw3Y/0VSZ+I3ANhNBWqkV729lu0PutpU2ECgYEA+GRD1w9eINrYtFqUtOx/ +YL8cI+bcU/EAKToXHQxweIN5NdHuR1LM5QOHow4I6UWTPdChggjGKcI/Ej9DR2eH +/lJEG0smWQzrB2ODqAyjN22j0HQQoK4wf/+G5jJeDkJ6KGacrZjVvXAs2INWj+Jd +MxvXVKNgLHUWqH9ikWfmmgECgYEAh3BuxfI2rSYinAapiEKQQdhaGU7MA8Qp+oAR +YI0lkn7RxThu3UlZRaMjEldrxCfbyO3VmCoPqEo876QAL0YiVUPNPGAk/dLzz6zl +P9rdd0I6HVfJ+0SiWxKPgXFEWIhHSnuTaTo+gZzs7ZXh/ZmbBaPBz2QeSvhJP3To +s9FoIOECgYBJqfEwXcpoyGRAPrfZ68g42eUbyJ89AgKbsx/EHa53JaNmcAKvPxXq +lsYCAU9aIGeE2fWQq8jwi8vL1Y3xm1sCLuyrAWpXYQoevROfW6LJ3KFtrVWizVuI +DxD122FwR0OLaNStTyrTNeNovnA3bjN0dTstTicjRYC5NdbrKqPAYQ== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c1.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c1.crt index 3083fbc5ee2c..dd3982013a03 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c1.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c1.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUP6Apm4lhwWUMZcX1Sgop10fSuPEwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjQuYzEwHhcNMTgwNDE5MTMyMjEzWhcNNDUwOTA0MTMy -MjEzWjAQMQ4wDAYDVQQDEwVuNC5jMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAlusHSJz+gYQ2zUkOFFPijbePcDBt5QT8IQpmrAdQhq+O8gdwPxVrL/ROkAms -boSiuKqWo036b4Gbro0Q2fK4uyPhfergvJfdbAWKZC3gtCSyx+/36rWS9pxSArLK -2hne6aYcjhuGlhzcGrhKs5ByEt30RkEK9REwp0M+PGxoAmsCAwEAAaN/MH0wHQYD -VR0OBBYEFMt0AUJ/1BWNa8n8VS+BmXy/GSYaMB8GA1UdIwQYMBaAFMt0AUJ/1BWN -a8n8VS+BmXy/GSYaMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU0LmNsdXN0ZXIx -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQBhVNAp -rAoxevBNuG6HP/1CougyBIXz4/hxdEfMr/D/u6kEBXTTkulFbI7IHUvwATz4oKUd -CNzBQYRp+dcWpvqSL1rgkdxnPkNbdBHr9BEwiyCpk4nQAF12DpPPMxgeeOE2EiLQ -kTkE00nh6i2cJlYQsriPlQ1S9KUcY70ch+rjaQ== +MIIDTTCCAjWgAwIBAgIUZYBYKhCsZvpfySUytHi3Pr7smTAwDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjQuYzEwHhcNMjIxMTI2MjEyMjU3WhcNNDIxMTIxMjEy +MjU3WjAQMQ4wDAYDVQQDEwVuNC5jMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALpTlDyrKSPySpN1XZiWWvfVeu9ix8CWtKJZ5fQTAi7kkkXA4Zv+0FL3 +iioCpSFg7JYH3tejh1WxCncOZunQDtSjkMHnGCybHzFQLk8+/A+VIwilV0SR/WVs +45ro97LaG+CQcD5QbMci+/7xCBwX+hPmgtQzW34++ENBX21RwdYEnYoyx1zaWrWM +frV3jxLue0CECa022dwwlvyNRRJ4YyrpzyZR1tDdTA7qs5tB5JqqNFG/RdHU8h1x +aK7BLmabBOKEB6uLkCdW60HZn2SGWdz6xV8iq1gndbq44/d1RFY+3YiHPbK09+Sq +uODoC1uUAQotReXKIPgCcKCYZ0kC2fECAwEAAaOBnjCBmzAdBgNVHQ4EFgQUL5GO +2RzM9WZcOfuTAO9loD1/0lYwHwYDVR0jBBgwFoAUL5GO2RzM9WZcOfuTAO9loD1/ +0lYwTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTQuY2x1c3RlcjEuZWxhc3RpY3Nl +YXJjaIIcbm9kZTQuY2x1c3RlcjEuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQCfBGwRW2y3aAddyauaJnmnYb1Jpv8NhrGFVhCJYruE +M3FOmUJGSErX0nTLOYs8n6h5u4mPYYfnQp2K6ow4x/UDWr7m6VnxCbN1BqjDxk3k +PcqiLb+VgtsnWARTHejsaw8ByQ/BFl5Uy0OXM+M/vJLVuHTEhskI9nCQs32LA9Ld +ngnaknOzBs1hkQyGeaG7ZZ6l1MFeOhQBZU3LmXvzPlXHoIw0FB+H3/Vh7K8d4N9E +RpOOWJjBj1YVt012DFhApMq7XSLvtNrccj/4lp8nrozRZrp3haanj9I2fhd6PwQ0 +17D1AFMScje0n37uPKcS3xvYLdqejF7A7im7LYFRcOAp -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c1.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c1.key new file mode 100644 index 000000000000..2b7db0bcffd6 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAulOUPKspI/JKk3VdmJZa99V672LHwJa0olnl9BMCLuSSRcDh +m/7QUveKKgKlIWDslgfe16OHVbEKdw5m6dAO1KOQwecYLJsfMVAuTz78D5UjCKVX +RJH9ZWzjmuj3stob4JBwPlBsxyL7/vEIHBf6E+aC1DNbfj74Q0FfbVHB1gSdijLH +XNpatYx+tXePEu57QIQJrTbZ3DCW/I1FEnhjKunPJlHW0N1MDuqzm0Hkmqo0Ub9F +0dTyHXForsEuZpsE4oQHq4uQJ1brQdmfZIZZ3PrFXyKrWCd1urjj93VEVj7diIc9 +srT35Kq44OgLW5QBCi1F5cog+AJwoJhnSQLZ8QIDAQABAoIBABd0kgKZIVKvG4g4 +jBG7S7RLIXClJLQvxIlze6kgA3RXvboeL8htaVgoZTIopxTumJnCX+ERauX0pXab +T4U7dcJ63KwsJTToSiLxDwNW0HA9u+1IsxWksxtje7tyfEv9fOjmBdsyUJ843jQX +r4ep1QdQS0PfOzNHRgVhY1vloNsJ/jUfM8ccAupzeTRZwzCr0CRdPWhCnLB9V1+W +46MWu9ZGRPvkrlsaHnaWzDxKfjoXnxXdZ/CEbPEcjShn0ztlLPDttWZCZ38KTe0W +CBm7YAwlfxEWEKRYsd8iujwV+7v64fAJc+VD1eWRvpvryNpmfR0dDWnRd7t4BZxC +AGQiwGECgYEA+g6u+b8qbOBrgslX1TcyMpns0QoXsrMdfOwcmhgi3y6k/5rr40tR +mDjKRPwNKweUTCfUUTKQfDZtf5Ci0a0w1l8eYI6EC6wSnmxNeJKUNInM92UM8p7I +J86vEZ8BWu7KE2EiRUSQfKlra3qWrGiQ3Sbi1byK4lja4NclLWIdnMUCgYEAvsEq +SKwnZRlCYiRCPvcC0kuw6+I8mi/GtZ8MF3XinNcnOxcrS9cz5nS250vGHeiMOEzH +1derT8/CHjY6zBXPdEQjHRdzM0LsBRxsB4yELmaOVVAgtvlA02tuIZJZOzN+Cgw7 +FOTPi+HUPgTe2FWF2trYapxXlhkcezx13+jocz0CgYEApfDGl0AQRG5tij5ZRko5 +yrCxIkvjTBhnMjZyCKSJx7fpkSJa9dxOyoitWjg1+N8vv5YS5Mt8bKGl6qiuFB0t +sqUuwH7zC9fmBzHx+1iyd2cXGPn7LKrIQvp63WO6f4DFozSrjHIivEel3I5enQ1L +TSd4EsTrGahDoD/GESbyZc0CgYBysxXo0tNxYAnhl2naEI86wlakz8me8OFsLjGI +HdNgeaLWTi0zkjhb94xPcemz7DP8WnbC58bT29oebn9WXjr670mp4YLcrP3caOWw +eFa2y+YD+IVjYGcReYSkXSqYhXLhOc+A1cDSnL0Reb5BmNM+8N625EFqOLk+hPFf +7N1xBQKBgARJ89KPeuIKJ3ree7LtI7+FBlQAjZI0SyKC9jsAcxxU6U3PwVvVmgS0 +jmAVp1t1m2YmXvDv0SkhsU+KTSiqIMSvrcfXuUTqdd/THHMEAAUBSfIV3u6BksXM +ZStOlMSErFxe7mIXVghlgCNZ2Zu+q8+FFtftvBu6fIYOedgpsYCb +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c2.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c2.crt index d0bb18e84d3e..2983bc1cb140 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c2.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c2.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVALfLHibW9Z409nWoaPHhXA+o+2ksMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW40LmMyMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjQuYzIwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAKj0cnL86BiN2VBhvrH3XDplcwd8lFAGxAzAfP+Sx1Swqd6y4hZoX2VkJ43A -uYvJY7aoMK4xIQJ4lWbrvCa+5epbOY1GGg82Z+/Od7mfZppfajCKTAmAIXfaSW48 -V/fRZyYl3KDfP4h5AqJV02wAJ4g5r7Olm74dwAMclLF6FqwfAgMBAAGjfzB9MB0G -A1UdDgQWBBQ9SpGlYooWMcJYNuTND5SQk6EPfjAfBgNVHSMEGDAWgBQ9SpGlYooW -McJYNuTND5SQk6EPfjAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNC5jbHVzdGVy -Mi5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAO/iR -mu5N94XQp0J3K5v0ZZzP2jyhpkki746RtHO2NGIOaq96wdivsvqYpDMCI1DXUktQ -Pbt1Wti4TkmyOXmDQdHtf8pWFHeAPdZgi/g0ThPI8Fz6crrZeJZL9JCj29O8MxQT -LNf0T/FXeo7EChM4VVekMcv7eJXwKTWXOng6Axw= +MIIDTTCCAjWgAwIBAgIUGkJg/U5N2vJRe6qhupaa/Aq8tDwwDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjQuYzIwHhcNMjIxMTI2MjEyMjU3WhcNNDIxMTIxMjEy +MjU3WjAQMQ4wDAYDVQQDEwVuNC5jMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAIVaLTgKx51KbttfwBSJRgmFiGU+RGwUjOXm2g0sm6ZnLGmQZH/sv0Pw +RYtufLYtcqLvTzBRbU6XPdOcnT8el2tu4gixiPiTe67WQi59hXirS6HLqkZ9CfrD +r1VegU4LkWWOkBU8w6PSIiu1NhX1Xwtnw4cCHQJLTLi4pBD3VPV3Xo30wmPndO2L +XNtgbUgh4036+oAc31zc3qEYsB9fEjS0QItE5h/RjTPKmnB4XzDBNOkENdfL6cMq +n62og8HeLQYNqeTgw198WkPQiKZX1GqjTUz1GXFgUsofddVAs8dCbATOBh6jc3r2 +v9tuYmz0BNYAppkz3nI3wHO2Zhp+x2ECAwEAAaOBnjCBmzAdBgNVHQ4EFgQUtmtn +oGqsCEDnKwVDkohALut3QrUwHwYDVR0jBBgwFoAUtmtnoGqsCEDnKwVDkohALut3 +QrUwTgYDVR0RBEcwRYIcbm9kZTQuY2x1c3RlcjIuZWxhc3RpY3NlYXJjaKAlBgNV +BAOgHgwcbm9kZTQuY2x1c3RlcjIuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQADFtnXrG+jl9AO8Z/EPLMFRqbpMx+QqWn3G1fLtxc1 +zldw9tS+I3SM6lO0XtbIEjUtFRTj1Jx8TPKebHU1uYRcR4XWZaFpXBWUwsKwD15R +YF7sOh91CAGHtlC3dHOqr/El3VFXxmcYJSYg6VRHYUrBf4le6N2wmlqqD3HMWvHl +JGOxxg2PC121Ie1Iz28fx2zffa65izZInCluE0S6/Z+FCI84An9QRVnCgwLI/4jq +lcLIBb6I/3NUHVbkxsiwUXQxhRZJuBk1AELyltwKqtIIYhm/XY4lgqTV6qPK5wig +JgJZDh0LDx/DofhP1yRFO3pCrq8kzZSvqh+mWwTV6ndn -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c2.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c2.key new file mode 100644 index 000000000000..2ccf72c9bf5b --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAhVotOArHnUpu21/AFIlGCYWIZT5EbBSM5ebaDSybpmcsaZBk +f+y/Q/BFi258ti1you9PMFFtTpc905ydPx6Xa27iCLGI+JN7rtZCLn2FeKtLocuq +Rn0J+sOvVV6BTguRZY6QFTzDo9IiK7U2FfVfC2fDhwIdAktMuLikEPdU9XdejfTC +Y+d07Ytc22BtSCHjTfr6gBzfXNzeoRiwH18SNLRAi0TmH9GNM8qacHhfMME06QQ1 +18vpwyqfraiDwd4tBg2p5ODDX3xaQ9CIplfUaqNNTPUZcWBSyh911UCzx0JsBM4G +HqNzeva/225ibPQE1gCmmTPecjfAc7ZmGn7HYQIDAQABAoIBAAgPiuMz15wc5zWQ +hKJZJ+gkb7m6+VTVKy0sdqrMMv5shyU8aq7G50rah8GlZJl4htDtiUpg6awR/VVA +xsqNdVazpasj3CU2eQf5AJgh00MXi6NUlc7b+RH06TDOXR+UGG5cnz4BZcwNxC4D +LqlGI0AjvhB3orxEQ/d4hcZWxQZvYetFaBHENe9f1+TG6NizRL0vwbNkmhjFpMSO +fQoXyLjrON9UTQMHkDhhi6gmNibSuwfeBML5b5VuBka7fSEnfwjjzl/YQZB24vMh +12w7Q/rY+BLbQ3jjfjXS9cVDxavH+oZDZC1+ssET4mts3ZpXos84L7ul7VBVTUUK +Ohw1ykkCgYEAteRqIPGInIlvGCMXprBSVKV2U3C+xrruGxeltpmJdtmi3UQQ6LKv +H/txcJIQ96MJ0yJzSSy27cBG0y+3dfL5jT4EYjDuxSQpPm+xpGXuC2yH3nnIHfFb +62F7iWoXSrMysVFPuna8woYH5NTDAftbkMsqYlJaPRWDtzuWSRTBpz0CgYEAu677 +JdiVmXGZMp4wkRx45zF8GPJVLdYyQkeuACxw+FVqxgRnsqMnUl+Oga1t0KBumD7j +mZjgzGTAumGWdnyOeIfowhZS5k6unnXylvWBEVRhfP0HanI4//X1JbED7xGQVvEE +16JqkZCheI3v6BJkU2rl1/sil0vtJ7slkHD8QvUCgYEAqZjfx/XN7j9LIdNesyLG +sXxSNvzyNTSI3SQQdJD2lqXY8L0NkxtApO57WH4pae1QNNhzbDBQJKDaqQFd65Fx +VFwWFEQR7imId1oH5gTQj0BT84HTsO+iSHftizQroZAy3Ri24WLjH/F/HMOxTIDF +YN2+mEy3LSuJU+kweNXstvUCgYAW5tI0mwoeMDNubskkW+IQ7APlk2R7dGCrn82G +reWXF+0wfocc0DXTIl/kSQJWa80y/LGPI42Cw2SVGaVfjfFgmcz4KfsxqTY7lc7l +Rs3aDH7c3o9SGOAFs8CXOdNuD6maQkzB90qjuo9Jg6BbpCrj1u52/WkKJzSoHXkG ++BKAJQKBgQCWan8uDctb5gJ9BJHf0HmYpyVw6YTWBFieG7MjMqHpT1V/LV1X2L1s +wGUCASP0/1HD1RwRomTySKib3x7ippJojl8CoK4Fv54qUdTRQQ/hKao1tK9wDU4K +GN0QZpdBwQynGi1KyqaOWgfgLD0iGkjJrxcMhL9+EHnbair3dNVsyQ== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c3.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c3.crt index e68c6097b31d..ba49233bbdc4 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c3.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c3.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIULVXMkTsJtk6I/NjAm5UONplDpOowDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjQuYzMwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0MTMy -MjE0WjAQMQ4wDAYDVQQDEwVuNC5jMzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAlLG3qMUCPi2usnc7jsxRFHVGjbf5+HWY9nWaboIQ1vHfn0Z3m/hOOCvxwaNY -4RF75V1ZSeRsAKzGeDD4NC1KC2Dxa81elMfbXq6nLDIfh0klkK8lSw6snzpe8NDM -SJ8agNdr6zLUgL9e3qh2pU1Fc9ApR6+HRXA9rcnqp/xm4XMCAwEAAaN/MH0wHQYD -VR0OBBYEFCz38GxHKudMx4dyBlrAcIB+8XC0MB8GA1UdIwQYMBaAFCz38GxHKudM -x4dyBlrAcIB+8XC0MDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU0LmNsdXN0ZXIz -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQCNoVNn -+rZHQclIPn3eB5sFMypQemZSz+k6VCm1o85lCBJnMn7NrR90SFY6PDN2U7WjKLXB -6Q2+xqneaTLi2zDU5LvbBmRakXHn6kFWU8xVnTWTanQqvh5lqwptQjb33yN+iSBT -/D7jEcYAcntrwtcel1EjfTbmfpB3UsyG09s/sg== +MIIDTjCCAjagAwIBAgIVAI5MDCx4+sc2UxgCqPOc6VYX727ZMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW40LmMzMB4XDTIyMTEyNjIxMjI1N1oXDTQyMTEyMTIx +MjI1N1owEDEOMAwGA1UEAxMFbjQuYzMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCY/ApQs6XT/wALVRAlXbDFDci1gEM7HVX5MmHQr9rBnnXKY+JbFAB7 +nVqLG+AXse82I5JTci1ESz13wxnl2L5Dh1Fi5dkatEycnQ37Zo+dyjqHJz2s1M5X +WnMpEWK71vJea4o8cugqYo7SlTp8rhNr4PTHsyQ7yRl7t4gmdbUoot3v+n/3ndcN +D/Vk/EIqXeBWbW1AlzunUR2+O1OnsvhRa5uVhSa9w9dz5IzIKXsvQ5emax6cO+yg +ImRGXtpske9cYkg1IaKlINcf/WAT9ecR8gAX/8k5IQg38ctR0H8uVfD94x+BoaUn +w8uBH8/xI+L2TjI08vOp9Q3TZNTKsmgXAgMBAAGjgZ4wgZswHQYDVR0OBBYEFEuk +04CXzEzB3+hXzJTq+ak4WEkvMB8GA1UdIwQYMBaAFEuk04CXzEzB3+hXzJTq+ak4 +WEkvME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGU0LmNsdXN0ZXIzLmVsYXN0aWNz +ZWFyY2iCHG5vZGU0LmNsdXN0ZXIzLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAHq66M2T/xkruS/uPs8iqtv+UbEwMAfscd7AkhPXu +y4ir/U/J+1p+aK9w2LY+HHi8gvUM4VOFER/XL8ATvImP/CfoFQ/m8CqAS5xF5KCh +iUuXSG0MVC4jia94f9gI9JTvchIeWn53OPkztISPTmA0AAqz9G3fA2HUtC44BLpT +1cPBxcJu2cUe+WQsMv6+7xq2Bf7QfSAP4OgjRuhVD1RYksRSQdEHhpmPw450vaJl +87uG1FsX5T6yxZ7+cu/kY3c4KGmFqsDhrs3MXQLz5qjp462poP23NPx/ZuCnWePT +dXEkrjL0XqTTc5nRRz0CSiyEdTrdaPRxMkWQ26J7+67prQ== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c3.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c3.key new file mode 100644 index 000000000000..8ab1a5a75dc0 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c3.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAmPwKULOl0/8AC1UQJV2wxQ3ItYBDOx1V+TJh0K/awZ51ymPi +WxQAe51aixvgF7HvNiOSU3ItREs9d8MZ5di+Q4dRYuXZGrRMnJ0N+2aPnco6hyc9 +rNTOV1pzKRFiu9byXmuKPHLoKmKO0pU6fK4Ta+D0x7MkO8kZe7eIJnW1KKLd7/p/ +953XDQ/1ZPxCKl3gVm1tQJc7p1EdvjtTp7L4UWublYUmvcPXc+SMyCl7L0OXpmse +nDvsoCJkRl7abJHvXGJINSGipSDXH/1gE/XnEfIAF//JOSEIN/HLUdB/LlXw/eMf +gaGlJ8PLgR/P8SPi9k4yNPLzqfUN02TUyrJoFwIDAQABAoIBAANWWYyXJAKdk80A +0bWu9zXlQ2qfiK1CdOdXGlvTakHtCK7Il7MwRYPCKVZ9iNmbemzDlo6WyrasWYPs ++qSmoh1BAh0r3HJBBVF+pO5wt1NUNQtREimyCWpNwSi1MLfeTDKTxVPk7HxNjXC5 +6/5focbUWnf8yB6RwtTjFRJEYRZTe+rzFUQwZJPVh/AQJ3i9fQ3LGb97gLtwYme4 +akZ+FIWfBS200lzDasVMRwNZEycNPToXVYRDfW+QmCMmQB+QEvkezRKgMjf12F1m +mBk3HlS07U75fs+kT0P0JLaVt7HpNMyeZrvDthif8kX9c2w0S1jI2UscU1n+VkWV +/VDV4KECgYEAvcHMwGusxnTyWwQoDXUtQFTRaQdklSq3oJI85O+62Kxk3ZdXqufg +kiNl5zdC6pKTMKivOtBM3wlPb7SC5gJF8FZ7FT7MV5B8H0Zuj5UjEOLYWYWDFdiK +DJTfACtUiq4VG8g7b2xidtcDvxorXgl+2jhmHkR2LWfFOde5OuideucCgYEAzmP1 +AaG6nKPqEuaDO8US7/8xvEFUg7MDhaD0OgfyVNHvBboDsKxxxMxlwtIBCUcWLe4s +pnM8pByE0WZlmurUG3WPL10+/3z3RH1JyTAd02jzcqlh8HkKQZp3AkpmDzArk4Zg +W5hrjcFfff++pvgcAx338Nmx/iDv2uuv20W5s1ECgYBTEovj1X+/JiWPqQMr2Rel +aTyAQYdMMnZPapYUF8tx3g8OX5o2rOrIr4yGK9A0x1R+4epM0jJFFltbaOhBb89U +h1e9Fk/5KbzdKEVwwHaohtb2YhfgMXtGRMpX1aBeUhXVotQ6VHXK1xjByeAgfAG3 +4Sb7qzcda6eRShrCamG/EwKBgG57H4pSEt0K+aBIePOj2TFTwEHwDte5GieMM3Bl +FJnHD1ZXbuZCSVx5tfeGBzv6K6tGlew8P7fx9D9LuxQkpIpWceF+3txQ/uRQ1RC3 +ix/CTJ+SgHUDtjPMBezZ+qwisxDzm90sSFbHruB56IrkwcoBXto1DozlAMCF7ctJ +FdEhAoGAaDMkD3P0+EfZV3KgnarvfJlk7ALI1oA4bS/tWAY8vGJk2akS5AJOsCDy +krepJ9suoKVeGwEoEsChlxs9uoMskwt5kZ5oy20X9VYrsf/gj8qmmGUKdN9QPSvv +uNKkdOccrYt4XQyha6s0sS/MkeeNgIbYK82s4FCrN3cT02tFTY4= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c4.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c4.crt index 29fc38bbe7eb..9aefe5b2e80a 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c4.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c4.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAKdwxZ0iNx1vHQXPhBF0+HxwMwMFMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW40LmM0MB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjQuYzQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAJDwOCTGpe52Ph9EXMq+Spr7uLi+hxtJFsNVy16vzkSfjaEKhifl95ieBRzx -1m/Zesn7prrSux00lMer6Gc4BVRtTnGenVGpH3wpG43qBlXvuBtqI5cH740K6yYa -AemlyS1kUw2OFazToIo5Qc2MMmRxReCBywFg//LtEQhi3YmbAgMBAAGjfzB9MB0G -A1UdDgQWBBQDRkoU6EfsM98Japt0aiRlTZAV7zAfBgNVHSMEGDAWgBQDRkoU6Efs -M98Japt0aiRlTZAV7zAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNC5jbHVzdGVy -NC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAKfU5 -VHQXHnNTvfvzPUD0PDswSvqV3Sp+dujKXX9YcSEtp3kIJzFKDNb5Pjw50+/H5vl1 -Pu+n2nGaFxSHnqbsGGMylzAO7qngaJMUXuFyYHpyxf4dXqu3hjNfQFQaAJ0ggMnp -WZfyNLA2TO20ptEDa1A5wbLqRu2vLnLeGtyBfqE= +MIIDTTCCAjWgAwIBAgIUMGFzK+eHNmepSg75gEXfiCojpKEwDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjQuYzQwHhcNMjIxMTI2MjEyMjU4WhcNNDIxMTIxMjEy +MjU4WjAQMQ4wDAYDVQQDEwVuNC5jNDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALeJc6AJEFfUDid3pYAIMHtjdGGw0OKTLeIKBej/YYME2rzi0e7F5dDn +gF2AobXfS/vnYm00HeIb5h+oFdPfxZpxYKJ2jqot6MlnXkzn5n9g7sOxw2qzO8FB +4uNIhwxrcX+j5oKXS8828m9+nrs8WGt+lKezurXG0NbNeDhyXdkps+JloHvW/Ix6 +BBMOTRy6c/hsL1Ro3QqTpsFEAs4+VwxIUYVH2TPQYua7VUYaxFXQQKxCr+lESEWY +t3D/+62fX66QpmMvOP+e4W9zrNKthmVGKeiLt7WF5iCYTz262CKR+xTvdJK9tTk9 +UTyKVSOTpZU2tIfdgA4eV8wSaVdnYZcCAwEAAaOBnjCBmzAdBgNVHQ4EFgQUm4AC +GOQjiaPZBvc9/DdeF4qGUbcwHwYDVR0jBBgwFoAUm4ACGOQjiaPZBvc9/DdeF4qG +UbcwTgYDVR0RBEcwRYIcbm9kZTQuY2x1c3RlcjQuZWxhc3RpY3NlYXJjaKAlBgNV +BAOgHgwcbm9kZTQuY2x1c3RlcjQuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQB0Io+OWf734N5XdxSwI1Qv6DLODKedfngEHWlXcv8a +oUwWf6IW0RX9/DJb8jN7rugiy+1J9IXtDfpC4CfHOHnNcv0FzsKmfMTUP0zWLFzZ +V58fgNQz24zSs1uyJsS1nu7RCb4qYVpAegyMcD3UctmK+STw3An4JNeNssx3sS6V +37NjYE3+62KXKMtFQhICETfMjrUnz1ZNfxXeREWMN2qgrVTl2QwLOhT4rgDvI3nH +LzLB3EU/5M8Z5gUiZEc4S62B+SsxaMxh1u/j6aWZV/dSuyVZmqqc+MpE8LuyLEDj +Ps59DYN6nb9YzQ53jRpNqXqObXEkJJaVftwWJO1k63ll -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c4.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c4.key new file mode 100644 index 000000000000..f835559dab42 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c4.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAt4lzoAkQV9QOJ3elgAgwe2N0YbDQ4pMt4goF6P9hgwTavOLR +7sXl0OeAXYChtd9L++dibTQd4hvmH6gV09/FmnFgonaOqi3oyWdeTOfmf2Duw7HD +arM7wUHi40iHDGtxf6PmgpdLzzbyb36euzxYa36Up7O6tcbQ1s14OHJd2Smz4mWg +e9b8jHoEEw5NHLpz+GwvVGjdCpOmwUQCzj5XDEhRhUfZM9Bi5rtVRhrEVdBArEKv +6URIRZi3cP/7rZ9frpCmYy84/57hb3Os0q2GZUYp6Iu3tYXmIJhPPbrYIpH7FO90 +kr21OT1RPIpVI5OllTa0h92ADh5XzBJpV2dhlwIDAQABAoIBABWzfeVtTO5ll2rZ +PcterITaZJdsdbZkmqdAciRzlc8NEuQEmbf21E6YINhtbjF3/p8BY+TvpZznmZUi +pboQmmfBgiCTjhjXJtAxlbfYL6veBT1EVovWN3mJVc9z8uvoBFqIQagtRXxxzcab +HR+cagQDyoXtvWTi5iyPzOg3tZsInP1YJvJLt26HxBb4hs1+LRQX3kD5LgUTAvcW ++QB3CVxHwV65nSeoh8aVVvchwPQZ/0i+wGR/teolw5SDSa6nNyn1wz/9BXyuoAtV +DsqWdrTAETMzkNGA0qbxpiYclXPqJJycoP9kB5shdvUXw9MNtDVIBW/cGWLsPjBv +hjWuIvECgYEA0Keii2jkx8M6/YZkBU100lsg1Nl3vBwfwIxabhRKxNpGoSSrA4Z1 +8yOgXXM2YrpNQ7XCEavkv3ricTOQjoR1BqTNjFeARNzpgVYV1GgEl9DLU/wydbHN +96syW5fBpuaHF662n2nCVVEKglrloS7qJS9tfkOnJfqgLdu7M0rrWukCgYEA4S7D +3ZdEvYyVqO7DZU+FPaMT1zLGnGFrNZcQ5DKh8Y+xwIymnhFVCYsayOV3yZQDlilp +F3FFKFRzrobyJT4DzNu+oocZUMefg6XVobhJi7pH9gae1QZXGKdn1O6pnyniw7my +hOF0+3AK1L5WJRQpWCBm1qmmVmiKhApAMkaeCH8CgYEAigAlWFEC50Avdth4tK0a +87nDyCMgtvBe68moUZ/oJ7Q5/IA0/zriLfJFMSlvMfMIAR1ogqmOvDn5JGe3Vxsy +DzfWntiMhH8bjSgzfe1iBEra/+wQL5luoTzALKX9NXbIqJgsPtkC58yWx8p4j5do +0zlAEL+MAGl3oXzvnigR62kCgYAd/FM6s8UElK7tMvY0DHXKsmeW9Xz7IAHdvgMS +0Cc+eosJ//DEBbzUK4DilaPw5NPhcBw9cejqqOxKpk+iY/Kvmed1zxWw6xQzvKaJ +BXhNsf2ZJmKKLXD1qH0sHqfZuQMoWxcJmNgyD0Zx8/KWt2uV/6KSHT84LG2djZvk +1gB/UwKBgCzCSqYubsoshymflu1KkH/UmKvVE6YqA+N+BEDmyF0fWL4OkYkDwJ04 +xQE6ZUnyd7ze0Qv4/B1wqHXnWRbNOYLx0Eira5IpXxWP+JV/Izo8tnMNBTTlC/jX +PCoZKWMfCNcRDlqvj28ubsSUI99M2zUZyBABeiqFyM7W1Y70Dj1Z +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c5.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c5.crt index 2ff79753fdad..dab6ebbbf5e7 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c5.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c5.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAON+/VKvMR2xMgnemknulon7vQlmMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW40LmM1MB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjQuYzUwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAKCP9rT1CKEdFPbGE2ywJgZiX1qf5H36DwokH8diz/XVk/t2QOU7Hqui5HJk -Q6Du6ywPXOqAIg/Iv3Rj1Vo+B9jcPs/DY0Sj3mKdZHhySaoTHPWjv7mVa/PLfMEl -bmB4sYkgfotHVxUir3Xw5byGkJ+SjG+xYrMOSktBNph4Zh4jAgMBAAGjfzB9MB0G -A1UdDgQWBBRrw5xcRVozWKOs6ZHd4Q524MCFyzAfBgNVHSMEGDAWgBRrw5xcRVoz -WKOs6ZHd4Q524MCFyzAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNC5jbHVzdGVy -NS5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEADiK8 -asbnbUu8Aj6OO+lizquvCx+p1jTHN9+teu2WNtQzcSqDvE7AzvtFQoU7381BCIoA -IrtCwnqK8TIliZfLYK4kNWAW2Z1d6fn7KlL6HWwtHBy+ht/7jr7M+wcLd78UbxZL -mdCX4+s0FJ2mfldNe8xq0yRjM0laiF+P3F2WREU= +MIIDTjCCAjagAwIBAgIVAIMscC0h1NnPF1npd3hcWxu/jSAlMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW40LmM1MB4XDTIyMTEyNjIxMjI1OFoXDTQyMTEyMTIx +MjI1OFowEDEOMAwGA1UEAxMFbjQuYzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDss810Ct63IBqOAbs2IB3zvJ6HJuz0qsevoilua+yV2BYcocbCahkc +ETDmKb1jwt4uSvjQBayBgu21fwdm5C75GH70PoFsQ4xQDE/Yf6+lQP/db0DUvbyF +NmaaTZ3g6yxel67Auixx2PFFXjkYxaIbmvRjHWpzKmGij2lzDArse6tkxpa6+x3j +3ZrZeBntbZWq2AO14FUdei5buOqNhgv2ZWGpG9h4RKY57KoPM6TvdPFtzAa8aV6W +Ja4G+U9esXUX8+g7NlmjFF94UCkJubZq/S6fTM/J9ieJz3NO4z8tPfYAQNVnbXWO +r+/R0CcqT9hUyytoLrSw3jC1R3KN5xAtAgMBAAGjgZ4wgZswHQYDVR0OBBYEFBjX +1TgKnV5XM8N3vikUG/+KOduAMB8GA1UdIwQYMBaAFBjX1TgKnV5XM8N3vikUG/+K +OduAME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGU0LmNsdXN0ZXI1LmVsYXN0aWNz +ZWFyY2iCHG5vZGU0LmNsdXN0ZXI1LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAZulsFv45x9YamlzsBrG+pHSmWO0uOogfurGFr+/m +gfRtQ4ozZkL9tghMqxv8QEtTe/4c5KO4JV65neSanBfdD4W8mhQkfRuTSIHwIIOB +lVeKNwiAoHsgS/Pq6y9pEeZBrd/0mX18cNCCEnDfkcg9cgTfDB4VkPqhChfbm7n+ +0vJWNFFihjjX7dFo5clC7zjRLTOiNgNtW9jhATR+KAwey+5T+JmWQ3utArsXow9O +lt+zoRD8s35Wna0+UxTR9NKt15hx2OYJ30MvQA7ZsCu+HaubaadtMeNx0aOrdz63 +j6RiglJmrA9Da8ApXq1/Yf5kEuzbRM6ap68IjIsQx32BRw== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c5.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c5.key new file mode 100644 index 000000000000..5490b20eacef --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c5.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA7LPNdAretyAajgG7NiAd87yehybs9KrHr6IpbmvsldgWHKHG +wmoZHBEw5im9Y8LeLkr40AWsgYLttX8HZuQu+Rh+9D6BbEOMUAxP2H+vpUD/3W9A +1L28hTZmmk2d4OssXpeuwLoscdjxRV45GMWiG5r0Yx1qcyphoo9pcwwK7HurZMaW +uvsd492a2XgZ7W2VqtgDteBVHXouW7jqjYYL9mVhqRvYeESmOeyqDzOk73TxbcwG +vGleliWuBvlPXrF1F/PoOzZZoxRfeFApCbm2av0un0zPyfYnic9zTuM/LT32AEDV +Z211jq/v0dAnKk/YVMsraC60sN4wtUdyjecQLQIDAQABAoIBAAl2seDxG1D2iWFC +m/XQyGnJkYwFSCWLG3M/+EiYK4wdKNsps6zruYfH/yzAn/gYXzI6IKMOiwRdk/v0 +8XqRZnhinJbO1fVaaQZ9S4b0KfVhOSmlDFJFKL+pumXycvy+YlaA2YWOm/7MRe84 +Um+VXUMdyaYZ07r5h5BMjBKDuX8VagSz3JAb/47kIt+yuOu2lYSm/UBv+01V2xYb +K6fahrkIKyA2d3YfPOWnQxB76759fsprcJUVWUH5qCO2o7RpWWP/waF1nmF/5mkN +mCsxOXV35Xyler1RTQu09bTQNVOpDx8xPhS3b6BqTWP0kEWCn/0aQzuteibba0wD +IEs0/XkCgYEA/+dDdfk375wJcFv7du/fKc+U/UYuR7sihjdMJOvTCbeyxe2tf7XM +mt2AC7H0PzJG175LBZUS5+6k0B70+LWKTwbUtJROC3Ltlm6tCUu7RwxwlCl9J90Q +QubyZqLf+RStFiZAW04mCoflXXPOAdI+WIb5BYhnVyPVoRbua8woYlkCgYEA7Mqu +2PMk47Js8Z7E2akFOiDS7E796vZqqs7vqkwrRU+JiMPyRjyZgUWYfUTAKz/bDULv +oDU+Dco4SdqOTL05oilWV1DoE6dkXrHzUIP+nZyabq+nOBP5bBKoJzIEezvGEbpD +LgS2Go9oeZMvubg3QrgIlU64wF3pD9t3jWeQWfUCgYEA3NPKiUF4nZJ/KrCVfXtd +jo0NOsqXIVtBXaTWVn4SbS5SXS7PK4fMek9uMals5iQSshYKU1TjLbXoD5jHOOAf +7DYu022vM34HvGrhc0Yg5By+AX0T/ZRsde8pepxOvnizLIvxuVjkxu2iZlSKj1PI +bUh8F+QvmSjZLtvE/sz8hHECgYAdBE1e2pTXrkK5bSpUPekdEW6ndJqbE21I1yj5 +ZnIbAT8qWlCzGLP66mtgVm9yHVYbjutMZ5neGLzDGBw9/SY13/jrpEd8ORQMoqPd +Zv9pkwo9rBrCvXfgLX0bqH5yF4YTIA8hcYRbRt2XoFjF2nQzIdgza70qHZf00IEa +++jtjQKBgCSvTCcQR7aRE/kRi85TV8GEqDVoBTbeBXq8R4nN9IniIABfkgj3JPmE +FOM+/MiRDCUbbqpP3MLwt3DLsccrMBdLwXl6tsTgwYt7C5yZ6gy7/sTwTyoTZIMs +o4mXUVDB3BPyEd2NrItkUqxSPkBQS6mEJ6ak/hkg977vdMaF4Gb/ +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c6.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c6.crt index e22f9161b0d7..87402f0f2c8e 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c6.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c6.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAJ+HlcBtFkTPyQrCCvbdHlBLtP4pMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW40LmM2MB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjQuYzYwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAKiyEVPmc9H8quSkeyFE0GpA1yuRJfDd1OJ3I+ogifTkmZcKu3u1wCbxv675 -xRdpRzCbB8+hUr6ZC18qWUDxXp1ml4iRr8n8bVAmENgJzzp3RLBZROFuwVe2dttu -n1SMiBlnlcUnmSCX7OCTJWrr3tI0/F8RIdaB+63gf2Eby2eLAgMBAAGjfzB9MB0G -A1UdDgQWBBSg537AtIe7R0hQVRHdpCNBBw4I2zAfBgNVHSMEGDAWgBSg537AtIe7 -R0hQVRHdpCNBBw4I2zAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNC5jbHVzdGVy -Ni5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAUdzF -snAZflUna0f2B2WW03U3sBl5aybZaajWNxH/u/CCnSmZcdSytawJ+iuyuEaLMPci -zZqgxRVfOwgKlKou6u2hu2A6zOWuHI83LfrQXHNipy7X0pEVX31tgb0yRu4azsRr -3tm4FOLGjYqkU3LviuukIBSzigcOp3s3DkLI60Q= +MIIDTTCCAjWgAwIBAgIUQ6I6ZODwCiYVpokq22Uvm7drWJ4wDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjQuYzYwHhcNMjIxMTI2MjEyMjU4WhcNNDIxMTIxMjEy +MjU4WjAQMQ4wDAYDVQQDEwVuNC5jNjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAI4L6UeuVhkmz3m6v556y/yu+L7fLZa5LyruWVOfQiqcoWtgz+GN+MgH +GyDeCT5vDlSFuIxeW2HmEu9fzlIirvmarnlLjeLfkZ03bGHT4UxlA/D/Cnfeo/UD +Q0OoeNWKU4nf9usYiPg/QuF/dcyBoQn1TbbaywrZHEebt3KmftPzyLBuo9E8IQWD +yLsDUpssMqjcwIz4YjsSes9gPdhIQtQsOVoW2Z4BXYZ6+KHAzS17knS7KTrZnIqS +M4n38PFWyysZqy6hEab5Gx1pVuw6jzCUAMgzCh5TbciwTie33E3bNvAgza24+dLx +QVitADXThri4zD7jBVUofQkmCvaksS8CAwEAAaOBnjCBmzAdBgNVHQ4EFgQUA0A9 +/zFtZkQEa024Tm32dS7pPnMwHwYDVR0jBBgwFoAUA0A9/zFtZkQEa024Tm32dS7p +PnMwTgYDVR0RBEcwRYIcbm9kZTQuY2x1c3RlcjYuZWxhc3RpY3NlYXJjaKAlBgNV +BAOgHgwcbm9kZTQuY2x1c3RlcjYuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQCMGviMfMk8DZfI1DSJFSQiNgYkHYxd/96kGpSehP5C +kwxRLos1hJO1p7LGd44VYizwJQoNNPU4zzatyFPLhZd1Q29deP4QFquV56MwV3qh +BO4fcTPkcnAUU0/W6DU6VZLT78kYzLLhchyZ7Hid/pmvQ4f+sxqyK8xNI7h//V53 +Pn1RCcq6saqb+Ebj20eX1xdSX+ehCD4FXEPY8Iz2Mjq89EbOPjrZth2al4B1xqRB +UNsA3ElwbXUJ9wSBbi6BKvtCg64jysNDj5dNhK4RJLyLaIwwlQfgZ5edzxjkKRMe +9vi25UXCPAIB0FWEQ+TKPwN7yfuWyy+5s4bGkwnScrnq -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c6.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c6.key new file mode 100644 index 000000000000..326fbcb0aea3 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c6.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAjgvpR65WGSbPebq/nnrL/K74vt8tlrkvKu5ZU59CKpyha2DP +4Y34yAcbIN4JPm8OVIW4jF5bYeYS71/OUiKu+ZqueUuN4t+RnTdsYdPhTGUD8P8K +d96j9QNDQ6h41YpTid/26xiI+D9C4X91zIGhCfVNttrLCtkcR5u3cqZ+0/PIsG6j +0TwhBYPIuwNSmywyqNzAjPhiOxJ6z2A92EhC1Cw5WhbZngFdhnr4ocDNLXuSdLsp +OtmcipIziffw8VbLKxmrLqERpvkbHWlW7DqPMJQAyDMKHlNtyLBOJ7fcTds28CDN +rbj50vFBWK0ANdOGuLjMPuMFVSh9CSYK9qSxLwIDAQABAoIBAAKepEjBewOvvDfU +gorbsj4QxAmmeA1wO4x2ejND8VEeiQ4/6irmFrvUVbhqH+WE4CYdk+US1cCdIPrv +22WXD38O+T9yGJsh4GpJMWMvKIKOLQpuybSi6Icw7jal5BnxbnpRJRlPidx6M4jm +dDe3VE7+CoGK0Mp8bTxs2hnv3tBDMbGpM3ByZXqboN1kYcLV7nir2X7CdSrg2ucx +zTa3Ei5w6jEOWyqzWRHxLcg1mbRAPL3ncU4V0snSslsEB1yiMYrHoFux30YYFv0m +YyWuQWNv8uAfD+7LgO7SiU3LuYDMapCHXjJ/jm/okQ4Unv0vyIeB3devZfA0FOne +UcKe47ECgYEAv0xuz4JUssn45qs+o3+ihkgBlzQ/9xCAYCiGihvjB4v3A+c4pHVd +c8AgyRRVyHt91GtMNwuFkqGcvDKQN3SJdzz3zHBa2j+2nOTSOnolry4fHKBhoVFR +lafbRODsg9uj/wJlJEJUBIuzZy8Z8ZwAK05LKnqX+lbCGgcPlOH693MCgYEAvhb+ +4+pC8gtMlVBQ76lewN3AlIgIfu8epZWdmsZcSYFx7tqJKBXkfcOd4VCMpJFixjJX +0K2/cy5B0aF0fnc/hf37XPsVcAhdhAe5pIzlNF9d8z/V6piZaX9otQyu+Jmg1OME +FghSi31kVmy21k1FgkFlTCbfPnnOf67MsOd7WFUCgYEAmrWr4HKdIUMq4xKXRA9T +V5ogfzKokJ1t1VaxCDzpbQ8LJML5EjhA7f3yBe1GOy90YjismM/epOnDIokaDV9Y +VXCfT12GfH+OpnCAfP6QP47jZnlmu9pSSuBH+vqVDUay6Of8+EgUPuDkpIx5ROdP +U7asH5yqELHdpMMVHOlt2HUCgYBOaXn3VjkLUzHiHeFlGz6k3FSGcJ7gjY7cyxza +LZ0ADU+MMcWGLZG1dy33NZzufbJ7A2VKBYIF5zzrVmiRims/wIch0ckED89TmUQw +5pXdxsEgiGKbhRszc4UjufmrPuQjLQoQdJpoQ+9R30KEoBdeJc+9XAOW5/QQo3B4 +QOSWiQKBgQCIAjRqbgwX9KrBBnBcpWMeLqDQarRe4NP70r8HcHnbY6rpt1v3CsAQ +1HKKHzq+seclgdcqZuGIoaLw2T2/mvWWCzRCxqDTsSl4o2iLvclPpHVZLQQbLS0w +XQLgu5BMjeLVL5ykjUGTtYIeMt/61j86PkR6bKcRzgmKQeaMQEteNA== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c7.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c7.crt index b0ec3d41aa8a..8f2439ac1dd3 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c7.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c7.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUb4zqML7sOuozKkgpS93QUD40XSIwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjQuYzcwHhcNMTgwNDE5MTMyMjE1WhcNNDUwOTA0MTMy -MjE1WjAQMQ4wDAYDVQQDEwVuNC5jNzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAjvRiPU8N+9dppeWCZf3gRL7r2afS6iS7YZn3yqYeikXec14GkDotYxR+ZPeG -3iTwhE73I+iXIScqZmtqk6sL3Iv5Z700w8CBIhLFZP0L6MPDpT0WwBweRzC1BA1u -Xam3Cwp0hux5xAH49h0EZvFT6bxSVdhrunsWlW1N5vZIOlUCAwEAAaN/MH0wHQYD -VR0OBBYEFEMbNKUNe9yyOtm8R/QonVKFl7C9MB8GA1UdIwQYMBaAFEMbNKUNe9yy -Otm8R/QonVKFl7C9MDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU0LmNsdXN0ZXI3 -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQAKn5SX -5Cwk9cXIijfveeJ+vzzqs3K3FBukA1rT+0PrFRT7kZ4Zg5IeUwCGnBVDKf4XoNyO -Aq3ads+iVOOVn8GvlMacvh76YqDmlgF3TqX3KJRm+NEVHwonwdaxXLMi+iK7sbtA -af0IE1zsgddYjz6yQ4nbFGKjji7U6MxwcEKRRQ== +MIIDTjCCAjagAwIBAgIVAMIJinHuSd71CpBcnBwTrF5CS67XMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW40LmM3MB4XDTIyMTEyNjIxMjI1OFoXDTQyMTEyMTIx +MjI1OFowEDEOMAwGA1UEAxMFbjQuYzcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCjgq/KdqWYn8l9dXrr9d2usP9ov4zL5jlRMswF8zSIe0+W4H/eWihi +laPQCCF9h7Lt7Z10dnrw7xrLJoNrGYcZz+qiOFeuuMjkGb9nN7C2rOOoXe3u1/95 +70q1MqqfJY0oUZEU2f8sEu9wVWJXeG49e2dRjMC6snnDaZPXYliSh3J+azU6o4y0 +XkxzieeE7z/dbrihNAe0WqJkPLy+JGsRxeqFP8fo7k4244v5NHcCZ1Kz32ScbslV +HtgtTSXWqATNMSZxuzvGzKjfUTQMcz2mxwMijxvB9+tY5l2DQ27+kuTmNJyibxRk +FM2c+JX2aR+7PvbGCYjsVQCtmu+UdpVPAgMBAAGjgZ4wgZswHQYDVR0OBBYEFKk2 +LFGc2F7aV757ATsMm8DeJEG1MB8GA1UdIwQYMBaAFKk2LFGc2F7aV757ATsMm8De +JEG1ME4GA1UdEQRHMEWCHG5vZGU0LmNsdXN0ZXI3LmVsYXN0aWNzZWFyY2igJQYD +VQQDoB4MHG5vZGU0LmNsdXN0ZXI3LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAmoJsZszCCBZpi0Tu/Bx3Tdkyn5gL5AXJ5B1zjMCB +M/jjxqrpdg1FrXpbfPRU7AtnYsE49M6jCiJH+SLCso5vZDhueDnZzhPlL6WQYAjz +pykjWW3+xojS3A8OScvGUnCU7UzZSpuLQtMYJhchgdIExpU1rn/ZjlkKECWjX8Qw +L5JESdvqINOmB6Tx7JkQ7bpjxVymMAzG2pNjkRTPQlD21JdR01hCcr2Qe/6ParRE +WkIMnv+I+Vti/2pIcb5YwFnZJaa6SpE3yOVcvvL5PQWG0HPHPAlxOUe1Ht4RCgfc +i+RQM0H81JRvtFjBAwihhbBmpdAR6+mdT5quvhdmj8Sy6w== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c7.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c7.key new file mode 100644 index 000000000000..1d83dfa11bcc --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c7.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAo4KvynalmJ/JfXV66/XdrrD/aL+My+Y5UTLMBfM0iHtPluB/ +3looYpWj0AghfYey7e2ddHZ68O8ayyaDaxmHGc/qojhXrrjI5Bm/ZzewtqzjqF3t +7tf/ee9KtTKqnyWNKFGRFNn/LBLvcFViV3huPXtnUYzAurJ5w2mT12JYkodyfms1 +OqOMtF5Mc4nnhO8/3W64oTQHtFqiZDy8viRrEcXqhT/H6O5ONuOL+TR3AmdSs99k +nG7JVR7YLU0l1qgEzTEmcbs7xsyo31E0DHM9pscDIo8bwffrWOZdg0Nu/pLk5jSc +om8UZBTNnPiV9mkfuz72xgmI7FUArZrvlHaVTwIDAQABAoIBABPEceIlM+Het2o+ +cKzkHXfIGCiet7PS0m0ZmgbCH0C8v6N64pqMBcPdWV+GDB5z8FLJXja9OCG2lmOm +9U5WWLnrI++WpisOVeMVpckht3BuOFubd29fl5gzMLH1PSFeVdFhAwwlSq8WXvIS +sH/opFM0fwoh3Q71ErloIyJv1ktoeMNpEA+UE7V31zK9PtdSZa2wovR15qoDwGqv +Oh48+5n/1KXhcb749gXy9pl54HS9Bf9qMhjHr6fbfg1TxbHxEbMNk2anFQpSgGxP +7DdEbIG/IcGjE+iXNfv/hIMf2WdjrcFds5naMLPLgws0drgGr0fmHXNFG4fyzEYZ ++jrZMOECgYEA5kOCDj/0RE//38KGKwrP6ln1++T/2FJCR//EUdKl6dcrdKBJq8Yx +lJ7amjHWLOUpeMA+PvDPEHnsDSKSnQ1i/rHKA31XhOncii+Eos3BPW3c9le2xz/a +WPTUsCPPHH1IX7TyPnqVOZfIPMN5nfsegG/dTzUm5nigkuwTLpWRH38CgYEAtcku +gjyxDPRcduIMYHgky2nfA1TgwQs60KL4qpb/ZgYNPZXs+ltz1N4FX10MvR8Zo6ex +QPlWv/J1IWo+4SI6um5FjP3wHZEN4vMfwEiYGEGV1M4Ml3eklLXlhiBnacLhNs3z +6vL28u9ROUvDeG8tkJL4RvGS9kwUtVP85yDXcjECgYEA1Wftqk1FkuZd68C6Xjvo +FHeJzgofwBoEyfvWtEq5cC72XGGNxcCrBOXxDgVwTS2aMQpi5iyCi++jgUzwetQE +Uq7t3iwVp96iggOmNsjg9pDSwBDtGl8oe1d1o0Wc1veZ0vG9rYAouzbb4sz78xng +3AJRJu7I/cMAdwqKgJsn4/sCgYEAn6BFYr+AiOuathnQ3HUjeitBVV+W7Gy7/LDe +wo1NP31Y6xdXvI/JqsY62X6PzRIeJYL/Nxq7flmdWPY+uNqhz0s9b40c6BmTpBHx +c5tLUyFkPDWjAO5iauM+5QRXSpuBv+ohodIv3ysem71Xq82UqTslIMbIUdaE6KUT +krFX/kECgYEA4x+Uucmn3tMrOqoywd98429sNY7vaNgjLgSfdp1F4gBGOdGvavNC +cxJMFnKfda+C09J5B5Bn1hHmne1nksc3GI/GGCdlwD26gXLLCM2UVxi9Vmp07GNf +2HvnR5Pra4lwv0K3Njklaf91Z/uIEanQtc1ZBJo4jtmEG3Pz+B3Kgko= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c8.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c8.crt index cbd83c42b525..12d8d54e4679 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c8.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c8.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAP9uvL1aQzoHJdqBJ277FRrW7RTYMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW40LmM4MB4XDTE4MDQxOTEzMjIxNVoXDTQ1MDkwNDEz -MjIxNVowEDEOMAwGA1UEAxMFbjQuYzgwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAJ1FfrHkc7tQtArp5NrVzVl1SiHNyGtT42dYoceXYjfCrKoF8yT7VZdgSePg -yCFvMlFanq2tT/QHG3kKNW7cz7wK+Y1BSBQMWSPze98dCgRStqx6eu0T1IGQZ3/H -5Da4ejolu+g8o/G3dUez8hqxuoc41v4JBAOVDGNlIdYSlGpNAgMBAAGjfzB9MB0G -A1UdDgQWBBSfXMfH8Xf7zowDOoCEMP76iAtYBjAfBgNVHSMEGDAWgBSfXMfH8Xf7 -zowDOoCEMP76iAtYBjAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNC5jbHVzdGVy -OC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEARVu0 -SuTODyHIHTuHOByZ1h4UefWpYn64DwsrZC1wElUimOjcxxGhRKlgfM6aElDNLAOO -hcEO4KX9GROFzzQ+4ZvifXWU6Wdsrmu8Y1RXxqQUhbzlQok3rEJE15TSvTspMs4X -+7iDYZ8wvWLnZ8mf8IoPEb1r5uoJOkf00PxYLv4= +MIIDTjCCAjagAwIBAgIVALs/XE/mtZilJ7UcwM7smjr+NPryMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW40LmM4MB4XDTIyMTEyNjIxMjI1OFoXDTQyMTEyMTIx +MjI1OFowEDEOMAwGA1UEAxMFbjQuYzgwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCcg8THAP0R8k9QBd5RCDndp922O1vuuc+gf/vgAbYdu/yL6Sir/eyC +vODll7vK0nk4ya7D2eRTPIvLKQBPRJGXjbj+xeMMlru7ttQi0unZMjh/3/PN1Zd/ +UGinXY2Esgzf2JnUd3/6naH9ai1kWNGDdvVN7mpWCiyOhvZwfkXQhDAYtcYSWcET +wCzg/Q31PIuht0hqf7i6PRIjtoop5oMlsQm5hPzcEAhXIhCKCNOl3KitQ62bw/Qr +BTZu6pBd3O+hWv+s6Cc7xQD1irydIs5xO3sfbL2J/NqoYO0ruTXLrEFCiYwBjA3X +AJ9I5gZTXDTcDxglL8SHq3QTfCeLRQcFAgMBAAGjgZ4wgZswHQYDVR0OBBYEFHIm +GGPn/dSb2dsMCkiBKQcHfpnNMB8GA1UdIwQYMBaAFHImGGPn/dSb2dsMCkiBKQcH +fpnNME4GA1UdEQRHMEWCHG5vZGU0LmNsdXN0ZXI4LmVsYXN0aWNzZWFyY2igJQYD +VQQDoB4MHG5vZGU0LmNsdXN0ZXI4LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAZ65V6X3biHSSt4DeN2A70GUmTpP4fVykMs63YE+6 +DmYh96ZF/bXfCeLgF1TXOLNi1KDoAn+Anvm+ZuHfPsxnGbVTA28NNVDUFKI2XKoc +wzjgxUvC5KIDagUACBnMhhDzt8R6d6vjENf98CmCHgPosLGw/ixalo+8vi9EukBb +af+pTWpT8IOQNhw5Adntv56WktZtQz1bxJkZva0XFE+3UkB0iHbCtVq79qo4V/cJ +7qmYGhIycl7HbNB6+ZlSqU0bWi+UBtziQm99OP+bpyoSZCizLl4dYKUcGbPqeN9m +X3/aGHwbJeg16AHN12pviRhjVBo+eqgPNk5WHjW980vpow== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c8.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c8.key new file mode 100644 index 000000000000..6154d76e0aa8 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n4.c8.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAnIPExwD9EfJPUAXeUQg53afdtjtb7rnPoH/74AG2Hbv8i+ko +q/3sgrzg5Ze7ytJ5OMmuw9nkUzyLyykAT0SRl424/sXjDJa7u7bUItLp2TI4f9/z +zdWXf1Bop12NhLIM39iZ1Hd/+p2h/WotZFjRg3b1Te5qVgosjob2cH5F0IQwGLXG +ElnBE8As4P0N9TyLobdIan+4uj0SI7aKKeaDJbEJuYT83BAIVyIQigjTpdyorUOt +m8P0KwU2buqQXdzvoVr/rOgnO8UA9Yq8nSLOcTt7H2y9ifzaqGDtK7k1y6xBQomM +AYwN1wCfSOYGU1w03A8YJS/Eh6t0E3wni0UHBQIDAQABAoIBAAKDeTsK8IWDwzNR +TM83VxqQshZVoLXWRRfaRlTEIkUa+kTS80TD2LMGzoPRaoIo3CqFzqk1jB2sGMCk +AW3Ed6Qb8g8hZXKTzyMGHiULBxdmX5LqKVOybPLt+yhI79dkNqG3rEzFuIfxVSMp +O95jMMYwOGNOg2WrVyjY0Qdgd8WP+3mXVasju8eQnSTvwxn75bsrdXCgS/dNiMZf +Ap1hNrR/Pz0u5HMuSJGE/ICPbLpMUBXnX/0aV/H53iU96mPGskf8VgNK5ea98BIx +kTutbfpdUrFVf9GTxaDb5vtyZCA8nxaVX9pVBzkb6SljGK+goqpiTn4MvWWyEu6u +7oxdWZcCgYEAxMG0QX3cD4pE/1RU84J4xZtR8w4rddZA98ed2PxLrMl3E0SYKZhT +Btmfq4757/w5QQXs8zKpImra3qFo//xqPtlA/idhfGOkfYYTGyXW/wnbax7ltuv5 +5HBHhrM4AKlfmW3XQTICiKBrpdp1+vdK8mh/HsRmju50EQ/EBg53tdsCgYEAy6Qo +Abp1cX6u72RNnMKLadMHLTT9Z9ZdIgtlb0oawYNGy+iMmGKSYT/WTb2pBv+cQgWZ +pIZ0cCM0ppUu9IuBg38anUeGIjx/qXgRfeUTOKap3ndroBO2iZW8SBqG5+CRKuHX +CRaW4nVsp2PqfjtGVYpPq3UpLTkGloRMsDEPfJ8CgYEAxJFIxo05BwV6fSQBYu4c +Pj84Ff04Ches/F6fIiKcb9kubz9+TT/y4ssUK89BgDKZ+I409ZrNQqdCVl0ni2bf +tj5SIhex2xWpRgZmnB6ntlBgt5o8oQ0LsN9Ddh0ie/a6nYwRGv8dbOXn5eBlMf9R +DvC630ATvgQYTNtVWT+m9TUCgYAbhv4tdaIIjIJjM+DV7upfjLosnzwd/Dksr/Ov +PootXGyGhdHzi2eUI8f5TEj7/DOILS+esNouimPEnOfzAKSqPNVBK3jzSdBL/UII +DZ4yizztKKFwyxlisc34xTU1RQL6k/q4bMhKpmrgmb4KAEM6ru6kE7tUNKAyWoHv +U0JvqwKBgCyNzCz6AoqyabT+K5IYPgTvethTKjFKli0ijlizx4U4muIfk5umuYLw +1CJXFJlFc+myk4b0+gvDgMm4Akv8QNBR07L6v/GpuSPNjHNX9DiZIl3phf9AJKmP +sFw4h/BOZ+TKjre6POipP7e6/LTUTpE1cQiqsPM7IU8xUypSZ6D6 +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c1.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c1.crt index dd3475e13b3e..a4e2eecc3844 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c1.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c1.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAPJ+UHse1NzYUAWE4qVvMbBC3/WNMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW41LmMxMB4XDTE4MDQxOTEzMjIxM1oXDTQ1MDkwNDEz -MjIxM1owEDEOMAwGA1UEAxMFbjUuYzEwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAJNmny/UimTv9fGqahCbXJ1Z+o+ShVYIsC0Z6g3tDtMGigLgGrVJEbtehE1E -Is/YFCdy5RQP+/gPuqQcIFY+a4d4oXkOf559+Dkj+GlY8DDHIxSYFMT1b0cUdaY7 -KcSgbDHcE7ehFwYO8iU1iOC5PdiNFsur29CQH+fLMkGHeeuBAgMBAAGjfzB9MB0G -A1UdDgQWBBR7PXi1qWwEhdtmH36E3IaLtwT33jAfBgNVHSMEGDAWgBR7PXi1qWwE -hdtmH36E3IaLtwT33jAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNS5jbHVzdGVy -MS5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAi6Um -1XXvrakSEQLnFHYwVoTGEvQOX48J/W/sB5CYFHunn4S3I1h+uY1U3onR8vkHGrj8 -ClfYuDeEXv3UjmOulAb/YT5pcSjXroYRSQZgkFTlbMmoaIyNVOPw2jXRm5l27CjN -lWKnJAu9OQEPPkjb9SSkqcazgPi5wmAKtnRmEaM= +MIIDTTCCAjWgAwIBAgIUR3i95m/a7j/DtAaMXZdVWTq5fw0wDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjUuYzEwHhcNMjIxMTI2MjEyMjU4WhcNNDIxMTIxMjEy +MjU4WjAQMQ4wDAYDVQQDEwVuNS5jMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBANQHW7eBKa6mNFCBJwtqazIJ2jf24DgSVMAZgTonthGfvd7mX1U31gEp +CrgsPuMqklve/5rNjlTKsRT3Ohrt90LpzuxOezRM+ZpYbAejD1NUY3lYwx9zKKk4 +8RYz7KSl2wEarCJY93bGcJnyqP4MEkNlzltRuTImJavHQ2Ff5mYtibSgdY5Yxm4u +9TDvTlYdhv+27pZa+jJtQXhQP9cMOTXTlZVESbAbnnPErnnCwj5XmVuQX0QMWU+b +i3g32ruyHnONZQ4gJY2whccbskDyVnFjDHAh51xWAJjtZK/3pPuublaWhfND2AOc +uHuiRQMXGoJhyhRVGC2nU40qYxe9BGsCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU7vbc +7x6/7U1EAIy3rwQb+bduvNEwHwYDVR0jBBgwFoAU7vbc7x6/7U1EAIy3rwQb+bdu +vNEwTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTUuY2x1c3RlcjEuZWxhc3RpY3Nl +YXJjaIIcbm9kZTUuY2x1c3RlcjEuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQBdmOtcMoKaP/ilMHfhR8ZWaMg2Er0Dx/omM97ovYBm +JeW1dKxayvN0DyDGa+tKEIgY5f9C/XCu04MgrqCFBDdZ9URK+5t5V26HeC5qRkA/ +rCuogwf8cIJsaBAYRJjufbbTX26bnehOq5iF08oVtmIIsZZC91FEnsnfjWazZ5JL +BPEPwR47YSsmS1sjzi6zsXK6cimIC2+riQe9wZXRDFYsoCZmn8TUxbvCjTiL0Xyu +D10ECj+EIoIl8QZ8ryo1Q9nd4FIpQaoTqMxkt6bvAnfzgwIn0up1IhwuUK6kARSF +1KUFUTYZ3F34Qcl6UNz8MGRpV+8VsDzK/cn24D6Ahvqw -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c1.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c1.key new file mode 100644 index 000000000000..0505508998e7 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA1Adbt4EprqY0UIEnC2prMgnaN/bgOBJUwBmBOie2EZ+93uZf +VTfWASkKuCw+4yqSW97/ms2OVMqxFPc6Gu33QunO7E57NEz5mlhsB6MPU1RjeVjD +H3MoqTjxFjPspKXbARqsIlj3dsZwmfKo/gwSQ2XOW1G5MiYlq8dDYV/mZi2JtKB1 +jljGbi71MO9OVh2G/7bullr6Mm1BeFA/1ww5NdOVlURJsBuec8SuecLCPleZW5Bf +RAxZT5uLeDfau7Iec41lDiAljbCFxxuyQPJWcWMMcCHnXFYAmO1kr/ek+65uVpaF +80PYA5y4e6JFAxcagmHKFFUYLadTjSpjF70EawIDAQABAoIBAAyZ71qFo6uMlzld +o6trBLnxmfWPh53tgPn1yhdhutFrVAaGXI4dBmwFO2zcvFQV+KLbKwfRRa9GaKXd +V6e4EuT4LmMe+vWNyZvhu3HswAFs14ijvN0GpBkCmFmAdoUGknCLXEWiUfrOKm0L +3f1TTFaTG2OUBNS1LbH64sdLws65tGqrNLhaUS9/mE4cyzHuXeM/FfZSfMbtwjtS +6hwQ7QxGISfkRAL7shukci5qvAp6Eq/76fwWdz9xB2D7U1VarItKSlBBtg13QCA8 +rNn6KCzMnPHVZulYF8RImxEUp9yM8eXMXQWA8e/afCBlDk76TDauQDT8AXjSGPMb +YlSvUqECgYEA8T5q4FnNJmHbg2Q1J7tRljU4+K9WlE/XvdjhvDugp5nitNkELzSd +mijb2i8cotNRG9pkfOETDPqfTc0pWEKba9bvWjEj/jZn+auj2/mg97LwK4ZIXgWR +CgGKZE2UHH8kaNjfBwjjw2d1rCh9kkS7h79p5mCfo0W2lGRTHtp30fECgYEA4P93 +8JeoHVUp/BreNrm3YWdhSmX4CPznmDB6UO8m4NxXDW1aM+bXRAYf2ljqtbfI9ejY +rZvtk7c59/+qIF2/F7MbNZ1LqTPWaMfg7QrZm848mzcesjycRaJWn0NRqOAgLJ1Q +QNBZrRsbJzbelr4OEhKpnjcDkvQxkSNt8iTX4BsCgYBA4cKo2D4epo79RYQuTWjh +9yp9Z9rnRJoHiz8a+wZ4TSD19q/zajlA7uBCTA1tlVvpG9GO7JHNGdKigB0wial3 +VXVGUpUVl9hbA9isHvU16UBC1Hi+IM0jXcq0s1C0YVWPhVRmdfOb6EVzqegr4KR3 +G/oFZdNJfERgoKLiV2uGYQKBgQCeK/otcPAGbvBtApZk8nowR3Tl0HlmGCTXoHzc +mMNEIKIyFkXrZsS+VW/fF9MonGLyPRbWNV/3ED/4UAtRZZYJGyhtsH6YHJEGaD1/ +ZrlEBuAkXcN+mY1ihpVOsQyfI7B6rLzWU6I3R273SmOApWtO2T/UsegZTwHpfna0 +W6JHMQKBgQCdLxJLH8qLp6rtmyZrXghhzLRpQB1k58FecyamxmQGcF3lj4uF2/w3 +ixTmLNB6V2gIh+TMoVDUVG11Qz00cAbdQLouDcXR0kAd2xg0hZr1QGYnLrs2IuoL +V50fUOi705ehD7p3eef6eKItO7SLCvtEXsdNkKQuP71ooeAIaXDy3w== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c2.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c2.crt index 82d7e9c42a1b..248155d6b8f7 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c2.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c2.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUAjNAqngaZ7ks/XGNv6f5elfJU/QwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjUuYzIwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0MTMy -MjE0WjAQMQ4wDAYDVQQDEwVuNS5jMjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAwIe2m8DEVRgwTgfukT218TRvgaJ88GO5v12NUIg/uzgq2ADAIS4lynhz5mzs -dhcArzWFCz7MxP/fMxfwzRP7dNm2pjz57+DdUUSpPrBCDL/3+JwHWevd6LgMmgLO -SohsJOZmWe5PUVMiP8/sluBb1+63v0e3iDNUXreVal0xxF8CAwEAAaN/MH0wHQYD -VR0OBBYEFOOs6r9usaTHZ735J+HCRsZb8mDmMB8GA1UdIwQYMBaAFOOs6r9usaTH -Z735J+HCRsZb8mDmMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU1LmNsdXN0ZXIy -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQA0lOcK -U1O9pvw+GRqtodI1fNWObmMewUc482JWTCA+WVam4kwnXF0zsCYePdAnlu3eCBV7 -TAZeSIVRez6HGomSFz1OqtQOSOB/2mg4P+hNy+VIi+RoB7BPtQ6H7JBrTZ22wMcp -7CzrUiOStlsuqx2K/euX109WmRLGy2Brj3MxFg== +MIIDTjCCAjagAwIBAgIVAPAeByZhvQ2JsDNknuBtrbIlsVzbMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW41LmMyMB4XDTIyMTEyNjIxMjI1OFoXDTQyMTEyMTIx +MjI1OFowEDEOMAwGA1UEAxMFbjUuYzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCQhsQ9/Xmz6+79dUjeEE+yPh01xxpGkrDI5QW72l3B60FC+i00HxHP +vdjPlcndeBJBnOjz15m2vXCJ/19evk8f8XJ6rHJo7sFtkimaGFUA5bI9mHCUcvIb +B43+nOJrYxRIkPYh8YwaWoqPFeKMtGztc2XdArcbOR1eSDXXZ679+GptH78Z7STa +PrkvHFEOnYwo3qWP8FJhx6jO0GOVn0UPoZXKTt5IZsvMZHbZEybt12PUkZ3W6MVZ +m2Yaqa09i1jX70iflxcCAbaMoR0CpObSqy83Xu36Erjp3qwb2VR9VkIJ0dBmrbSJ ++twXt5DDGFXJXTND4hNwFKts15g8jqWpAgMBAAGjgZ4wgZswHQYDVR0OBBYEFOPc +MuJlv+W9jJOSYRRGTIDkHdExMB8GA1UdIwQYMBaAFOPcMuJlv+W9jJOSYRRGTIDk +HdExME4GA1UdEQRHMEWCHG5vZGU1LmNsdXN0ZXIyLmVsYXN0aWNzZWFyY2igJQYD +VQQDoB4MHG5vZGU1LmNsdXN0ZXIyLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAP+/MnGk8S/ViffzUikr73810VMfXOdUHnr4An+C4 +WbhMDOmjBeW6MpkEXdDkYnzq5sI8aYh0gpGfiipY+nc37RFm5YtAHUF+2+s1tWH+ +R+FVMhS5dpWFGzLfrNyXyYwswCO9kx19ZKSSXoz9RoDBP4KlBlRjaqPCJO05SH8+ +NjE9XCDhtP5h4GWBhhnRmyfeP2j/xCwl68BO/HUMLReW0oiURf6OA7Dlt/3Jp1c5 +fTCePCSjZe9EJB60DNw1JI96NJ0Z1sOaxO06IgaFoPX7vrMAttgZhdEuPn5qmNYo +zF0fR5zt1SwNQjNfiJL45VwI+zAB8qI4sRYpeeSuaYH62Q== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c2.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c2.key new file mode 100644 index 000000000000..f62314131820 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAkIbEPf15s+vu/XVI3hBPsj4dNccaRpKwyOUFu9pdwetBQvot +NB8Rz73Yz5XJ3XgSQZzo89eZtr1wif9fXr5PH/FyeqxyaO7BbZIpmhhVAOWyPZhw +lHLyGweN/pzia2MUSJD2IfGMGlqKjxXijLRs7XNl3QK3GzkdXkg112eu/fhqbR+/ +Ge0k2j65LxxRDp2MKN6lj/BSYceoztBjlZ9FD6GVyk7eSGbLzGR22RMm7ddj1JGd +1ujFWZtmGqmtPYtY1+9In5cXAgG2jKEdAqTm0qsvN17t+hK46d6sG9lUfVZCCdHQ +Zq20ifrcF7eQwxhVyV0zQ+ITcBSrbNeYPI6lqQIDAQABAoIBAC72U6svA85mklxy +sXBTDYqGK3ivj0ayYNaOsMvHW6LpHmY0o96lX+TItDxT2GnqG8iNQ5cEjlK2gOye +ei9vmIdxLflVssGNH3ZKwdDsKibl/IKOV7qtFSWaUVYURXCOm+cuj4QdGKDxncd7 +3YdKE4Ee99EWkkg6yWO7BB/P9alZg6Y+dUg6WuYVoxleqooTS49oGot7C3MHPL2x +QrrU9z3pE5J7jM1Ad8oV7O7HWN8lRQn2iErRfmewfDUWKKyzNNTSu4xLaXsA1l1M +Vhfx4qeHIZTHFQzjMwp1hpjDR6gyzZrT3hTPsXBuSS8N1KfuoKfQUGRZ8DH4/CIf +DYIOmt8CgYEAx+kqeN+7nkiT7pP1SEzWXPd9cRzCHtuIzKx1GqMHsTeroOhXcIii +Axf7Vt04XyhLwwMi0DsyYQ+Q+M4k62EeDILmol608PmIsenj3QIWCx59MVn483mr +8YmM0eq6fyPjnXg/G0zmnuoFQNJbmeKK1RsYC0Y0LqayPpHuZ3y+8sMCgYEAuRON +LOb0yVL5vT6Je+BO5bv0GA33RUwKxjheGtnLuBbgY/ocIFoq0gnkQ91v83s0MeaN +rUXPiaStQcfmp99ly+bseqC0r8fIPX0of9/DnlwuH2Ghgpnuh8meo0fxxDUPTRBb +Mzhf6JLOLNtnNTFI1fkDV9DV9c9WwFFfbK+tZyMCgYBIgPdneXWZdac5ey4Vt9Bz +TUuQr6Zz/JlD6jRaodBTZOZ5aGr2JIfl6Ve97GPal0nLA6MntKCT9UpWAqX/PGzi +isNNFl43m7kJFdQPvwbxp3AuavxQictjwchyBdj37bNK1GbG5wApmKSpGuIgELrb +iYUR6bdZSBoTeWfm3qZ+cwKBgQCrZZzniZgG3k4kYUcHrsrCXRcIM97ckTilK8yS +x0Kv4Je8XwMvPvHLBJgd9XPZJC9JSz/HyRATY6uEcPF+ZBgtBlXYlv3ekIEz2LXp +/rt1695W956QiafqnuDITDdtQspBzzT2sW24wEQqLKJfE55lcrAS4ndTpsmrX3Cl +AejvywKBgHN4OFMEYja4iEmq0OKCHSeDapDA2hrTWxmV0J6kI+xtBwTHhvTmW8Y9 +xuR/vTzgrBcm8fiVoycVtCzY8/1p+6CcnHPy8koLQQtFLCQxcIPVE000G0Hm2x8H +HtkeUP2NNQXjbbMaamfE45of1NmIB7KA5RBYs5grFRkbaSekfaiv +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c3.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c3.crt index 40166d0c2717..b834c8352251 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c3.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c3.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAIyTRMpfs9Nfo0QL/yDgL0L8eeN9MA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW41LmMzMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjUuYzMwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAJOBFDvbKzWo6MzAQUVb9t8ChihkEGokN8P/MdUkg07CEHIiTSLMj0FvfH/J -V7E4YtiELUY0grTcWRln7Q0bt7fuGtC97TxL627dx/jiuTHCG0tko4bF/KyKiRFG -O1CbXQ5tQzvDom59FNQzk07HT5aFt85Oxhqw6Om8R6YraLclAgMBAAGjfzB9MB0G -A1UdDgQWBBTlHIVNp/hPa3SPVI3U9cusa0lKKTAfBgNVHSMEGDAWgBTlHIVNp/hP -a3SPVI3U9cusa0lKKTAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNS5jbHVzdGVy -My5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAcAdT -R+l3uo8EKz1p9PxiCB26VI+1ZWVmyD4bKAjAgzYCz2HvNAbkkVMW5tGfmzLDuZm1 -pxpIrxi0VFeFhZyzlgF/umcPjr88MXWox0QNYLMiYFIxQQMJljglDxwyO9keVLoD -VZdIp8pZoaGs6tKQDyzmBh//6deecyNDNubfJb0= +MIIDTTCCAjWgAwIBAgIUd9cnNGI4x0z4dz8DLl0C+a8habkwDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjUuYzMwHhcNMjIxMTI2MjEyMjU4WhcNNDIxMTIxMjEy +MjU4WjAQMQ4wDAYDVQQDEwVuNS5jMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALN1NeNFFTeJmH2NkEUG5oK1OutyqBeCezfgdPfLeaa8igBksqDrKwSF +a3eUc8DuMK2xfCQGf6/20YJ57i1pc/A0Bd0vnhSquWQtyqTzPiI9JDLxjSlVzvUs +Es0fBYc54LjH0yWcmX9yIguFj1EBhB3m7wCp0Z0qBKsXpA9hHV9U1gk90d+mZ4r9 +w8upEdVtOjulSrdfdLD0mGSYhPJ05lJpUpMpbNuCf3WncWAzfuDUZN9VBEfu9Cas +jhswDkezjqZG4VD9JvyPv1XW3UZ38SxonleRKGrq6gSyJq+FRFzsYFE9DFqJ+Im8 +wXHA7wHyLRuMFUUCsVs54SU32+dDWmUCAwEAAaOBnjCBmzAdBgNVHQ4EFgQUlE1q +MDtKj+TFW/tEJoKc4ucVTMUwHwYDVR0jBBgwFoAUlE1qMDtKj+TFW/tEJoKc4ucV +TMUwTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTUuY2x1c3RlcjMuZWxhc3RpY3Nl +YXJjaIIcbm9kZTUuY2x1c3RlcjMuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQCoc4hSkEuhKvFNNnmO/oL2Dfh/K+n8iJ40kFYdjepd +/CKlfQR52q+fQY8PESeu541izzhm5kj6fZJZ8J0avcugaLMMiNLX877YsqaOn9D2 +ZEI3KbS4BbOqBakNwTrz9W2zhM+lvhY8fu9l5gAbIiMIkfvysn2qdoK8e37oC5EL +7T2NrvrGcOwRFrOSBCx2uFK3T3ubP/RriP5lw+ODMghUi4s+3gd1/nP60iXC7vz6 +vsSW+kTz51gdFwqt7POFvDURamOmkJ/s3yL3o+8d3TCedQP6AGDNGu9NXtFCyl/4 +22nVZm/e+9JcU3m/Fo561tycBp7jJJXc8a8olb5rVN4J -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c3.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c3.key new file mode 100644 index 000000000000..60dc9c8c6246 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c3.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAs3U140UVN4mYfY2QRQbmgrU663KoF4J7N+B098t5pryKAGSy +oOsrBIVrd5RzwO4wrbF8JAZ/r/bRgnnuLWlz8DQF3S+eFKq5ZC3KpPM+Ij0kMvGN +KVXO9SwSzR8FhznguMfTJZyZf3IiC4WPUQGEHebvAKnRnSoEqxekD2EdX1TWCT3R +36Zniv3Dy6kR1W06O6VKt190sPSYZJiE8nTmUmlSkyls24J/dadxYDN+4NRk31UE +R+70JqyOGzAOR7OOpkbhUP0m/I+/VdbdRnfxLGieV5EoaurqBLImr4VEXOxgUT0M +Won4ibzBccDvAfItG4wVRQKxWznhJTfb50NaZQIDAQABAoIBAD8lrmmjhHy/cSY0 +6yAu9mvmA66X8Ogc5Ojbla0gL1VpoJHU3Tpcpg14OUGhvj2UUxK7knhGxSWRD1X/ +O71FDyPl2m5jdBfHn2bN6jGy2hlINHWso8R4ylNhR9hf4x0bU6URw43HlgfPjIVL +YQvbIbc37L/qAbVw9KWTswoqXSCO7MDJwtf4cCaiD6GSg61ReinB+y+Yxo1SYfTp +M/HcfhJwhOdyXmi55PRNp9ZWo03B1IHmlDjzd7OIvV4eB7+6BAhKzGiuVfqQVyYy +dKJR/RQduU+oBNW3lhDg13oGn6sp78FCBvffWtJVs1PHKhqIaPb5CS2pCAHBQAdF +6ut+Wf0CgYEAvn4hMdrwkKn+cIAnFn9DrOC8KTjba97HDzsX8kaZPydRcWiMKVuE +lvYU3xNefX531PC/5rlbu74kosSqLso/d21bHOATDoVdZOfeHIhNYsmLFGgdoggF +scDcnyj6dtlc1pKufdfWLjFP3TN2BU9chtqv+fFXnYiA3XVssqG+3gsCgYEA8Suj +N7SRsXTnlqjrxX+lH4IYd1IXCxLMJ6AjA+8dNIHGPTGuv9R9PXlnm+y8kKNeFIOc +wz6VsyE5b7Uy6rGTQ9V1O/ijVJyl3Lz4grOuE4qb8DRfqOnP61z9bvuUL7+7go9T +Fg4G/BH5AD7uV8/L8zCp1vs8RWFeHFu/ouDcn08CgYEAp8zvbL1R3/z+9xbMV9Lv +SGAHH8DH+HObod0KMQ7je3AlG+NeZCgL2fcXV5H2UFGROXCBerqZBf5uwzq+Ns1N +ruQTuJHh946fQH3HeBbRJE4pR2aXg93RNk2PilxMlVjPoOxA7PWt467ojRABzJem +B9yn22IvLcPK0EnZ4Fj+ixUCgYEAkSOy8aZg/AwgB+KXKd+87y5rLPwhVA+2/633 +BLoVuHDVbiSFR3tI+AMlYIhjnyRCTVQ7tqDmlMxM9bZ18xp1RyCikjn7icNQanzr +jjU2cRo86J2MaGp2L+5hszYCo43e+h7pYN+GQpNKaGR3Ki+rTSsfO3stdY+4hJaO +ySZMgC8CgYBIzDSvGKpVpRz7oSNNz2tJE9Ujd5qOSofOgCu2nmZFtWm+SYpFjv03 +DMUKM2bW0BDS1AW5u8p7rGDYvY8bG+sCirilpoZ4GVOwBYNqLcYVfpJNIdtsZ4Lz +qTrTuoQkkhh426F5/zKLJ/vqPPR/J953iXLCIF37jwSF91DAKGiJaw== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c4.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c4.crt index 5f9167b441ce..6ad4a1da686c 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c4.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c4.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUVxoYfxLb1GFz1joNjsIRlzhQ58EwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjUuYzQwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0MTMy -MjE0WjAQMQ4wDAYDVQQDEwVuNS5jNDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAiGKK1GCMeyJzrzzi4pNaCWqd2qiFPISAANTh+laKkKQDLbrLo6BNfv9Ef2Zn -pFO2JXWbDzNd2UDA5wKjlNPacFvoWbQM+pydH5KOmQ0tG5AkrWgnMSVyZ4UY7tUG -AnkMe7ZwtWWETVTOzW478mqV3TAM9FjIPmUFuIyR40hF7EkCAwEAAaN/MH0wHQYD -VR0OBBYEFI+J3qFQpd1bJy1q/oww35Dt/lB+MB8GA1UdIwQYMBaAFI+J3qFQpd1b -Jy1q/oww35Dt/lB+MDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU1LmNsdXN0ZXI0 -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQAqxVEf -8W8V3ZHi5DkIxGqZoRN2ukHG1UVOvxItbhlbvdYN0mrFeGl8xzWtP4wYHgiGBs6D -3lzwD0cj1hbUi5Wg5kt8KuGqpAiELHKLbdp7AFRrAPTkLC1XNWEMYjZcbR4XHVP8 -rMVVYK0QV8ycLsotazvpHbmT+DfXRDyqFkxAwg== +MIIDTjCCAjagAwIBAgIVANt0GDxh1IVmxmAE9bhsPGp8A83MMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW41LmM0MB4XDTIyMTEyNjIxMjI1OFoXDTQyMTEyMTIx +MjI1OFowEDEOMAwGA1UEAxMFbjUuYzQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQC4yOwUlBFVyCLf8RCgYi308AO2kl1ao/WtIiMMtDVTknK5vaWUZcrI +dFPSmiuJV4xWUOnMed62x+RD7l2gWcd+QCK8L3PymqIlGeeo0ogDeQWJhdc3LTRx +k/nIGkbdCykFGe3DumO7JgkZYmAXHjU42sqRor6KjHRB3Dmy2j/T4ze5U8O2rWPz +r8mH2Uj1k4XGnkSFn6dejihk6TZS4430yuNbD4Ca73GyefvuSbQ5UrWVTgMs3uBf +Eb1RXTFN+k41L5Jj7a2HwhDqefVXEVq/5G0LpHVQ9PQVBit3IulubS240228pF9j +sYxMjZt26mzAZFdqxzYu9aZ3oad0zut3AgMBAAGjgZ4wgZswHQYDVR0OBBYEFOWS +Q9VXQJ+HQlOa/1D9jWspp2HYMB8GA1UdIwQYMBaAFOWSQ9VXQJ+HQlOa/1D9jWsp +p2HYME4GA1UdEQRHMEWCHG5vZGU1LmNsdXN0ZXI0LmVsYXN0aWNzZWFyY2igJQYD +VQQDoB4MHG5vZGU1LmNsdXN0ZXI0LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEATav2pGjvOkAkZWvjKDiYySE+s4BLIFV2XI3sunO7 +eznvU77WPtMJMXGn8el4XRDUHYsfdRTsrzfpXQFPBIJXzUsFqP3rPF1djIWg3Z1n +Sm+plHHRVUTO1h+8pCc2pZ1LwMcnpHFlV4RLw+fB+JeiVycQ8fz7Ane/HleAH2Gk +DTGzcOh+CQJIMxyHMRSOuOKjLm7ejPToRgIY2GXw9ZsWipJZN5qpHuit3f/kdRWb +P4M9d7HBbPQUQKfzC9JvEmHgPEIwLsEVOSNwwoQylGMwrOQvhA17jVTcnR8CfRm/ +q3Vrn9FKd9fmcq1nuk5QQ+BYinLfoJLQftwWtpe0noTecg== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c4.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c4.key new file mode 100644 index 000000000000..c39099defddd --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c4.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEoQIBAAKCAQEAuMjsFJQRVcgi3/EQoGIt9PADtpJdWqP1rSIjDLQ1U5Jyub2l +lGXKyHRT0poriVeMVlDpzHnetsfkQ+5doFnHfkAivC9z8pqiJRnnqNKIA3kFiYXX +Ny00cZP5yBpG3QspBRntw7pjuyYJGWJgFx41ONrKkaK+iox0Qdw5sto/0+M3uVPD +tq1j86/Jh9lI9ZOFxp5EhZ+nXo4oZOk2UuON9MrjWw+Amu9xsnn77km0OVK1lU4D +LN7gXxG9UV0xTfpONS+SY+2th8IQ6nn1VxFav+RtC6R1UPT0FQYrdyLpbm0tuNNt +vKRfY7GMTI2bdupswGRXasc2LvWmd6GndM7rdwIDAQABAoH/c5vsGEk15KAn3El2 +SDck3u9LY26EpFubLuGstsYIlxYzsQHLa+JD0iZK9fh+70QBs/9A8i1NVLi7Qm+1 +JB2yMnWYmyRqkTPYk7MhZ6j7GoZycG13DjkPa//+HAj/TPcuiC0wv28IoTkAIww+ +vOKpab4j/qSo5wX5C+Y83dQDxbcmuvUQxlytZLSpVKPIpyvrfW568utssgFc1XbW +O7wY2BtSfg82owZdQAm/a1V1Oo6GIsoMjf9rDN9UQgslUyaYFgYZ41PhUhCRt291 +1oGLX0gID5lqrlh6t9CsQBiTa+ds/n314VfwNRGGdXmLfGvexfHhHu2cweEstUyp +V0Y9AoGBAPD3scFP9WG9Q+gBbqu6776eKhJRJrvCtX7HtOFj7lU4qGnoOvmTQB29 +T3oaDOoWJYGvXuRy6e8Frj1itBvLzJk6VDdiDvW0c2PB2wwoTRKBtgsrFUCt3MnR +kgdgY97fBx+vOjTk6TityagEgEFdyP3fbHy3Y2w1mg+VW/aGq24rAoGBAMRP+j6A +JD01ivX6nQuUN8YZxgZxGkm6D3IyIFidD92A79xjCTW76nbyqIM6qrz7/nJ1SJj7 +ae5Xl6NriL0C5z5THA4paM/UdgME6tDFVJY5Ib/l8c4pd88OgRogtKnVOYurbZtY +LXSub6Uaf+Su5TFMWiykx5QiINN69GJjL53lAoGAN72UaNtOL+XQJ4LtTtxXSQjd +Qo9zHkvUIBeiR+EN6LjKfHUz7zeYxUhdcilEg4HL+0vN75qFcr0lqR2D/EoQcgGE +KauNOlkbuB/Hw/Auo/FNYxf8OpH9WNIZrYsmjw+YXkPyevnY2TuaQjuIT5aaP9/H +ALKLVSSxFpm+RDCyxzECgYBeg+cGqmlIMvwpe7Sau4yyyvvlZBT1BCiloW6jW1Ml +IQvavq/TeBrlk+T/8YH8pRKXhtIiHYGd5/wxCPf3kGtHPXpXcyZsUW1T/M0VILyd +6vnNy2bFDIALZ39O1gpFbkxba3jE/wo7kDReeEi1EGN2fb04m0kfj+LgDuWUbQ6i +5QKBgQDmSL8X2kkeUsOuvT8T3TOmCxa8FIWHOrpYD2vv2tdHNaJq4Eqs78zjyZex +uSVrPJxFDpeDPDtKU3lI1Rpx64fttrSYTXc+/cTa7ikf+mK2valG8cGK/q13mQsQ +QY6iK4VTgEn2TMv22ip8tJKQntv+RyFqEOW/uGJet8DefOqheQ== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c5.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c5.crt index 0c6f69a17e88..ad7f47cb838a 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c5.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c5.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUNJzE2lRQpXQl/GqdwDQi0JvuPqUwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjUuYzUwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0MTMy -MjE0WjAQMQ4wDAYDVQQDEwVuNS5jNTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAu23ACgbulVvEbb1O1WLxhi9/KvDXyqPekjC9Mfe4PQ2yqUaoR2XxKimbChuE -Rzbv1Ggt/msDngoRxRqgSM8mjR4I98YhDFY61IIeL8OmnbyjfbAXOPS5jy+qA6WF -jlqSBFDIld2AcFJWdrFhsdq0+A7Q/2x4YuSk48SFUr+aEacCAwEAAaN/MH0wHQYD -VR0OBBYEFB3l604mCVN5uOPCBWbsOdmi9aEHMB8GA1UdIwQYMBaAFB3l604mCVN5 -uOPCBWbsOdmi9aEHMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU1LmNsdXN0ZXI1 -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQAt5yP6 -J/JgBzJpYmX7YQp5gkaCZZDK07zGl0/53HDb/yiSxeiZcDuqwqlpmIeGdCbf6J8+ -2aEdC47+82TQn9ugEudvLUfTY7RDVsKW6KAeD8UJhQH3cjARxlJlwTELkS5QqFyC -mM35puJoLctM3pxK0iqHKSh8wSkckARDYP/2Hw== +MIIDTjCCAjagAwIBAgIVALLrYKwnxwvD+geAEyldHa7i61nfMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW41LmM1MB4XDTIyMTEyNjIxMjI1OFoXDTQyMTEyMTIx +MjI1OFowEDEOMAwGA1UEAxMFbjUuYzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCf1BHcvQiwsNS/HPFvAp7fyzvrE11S4HvL2JKXbFjF6HLDYt0ux0vZ +J5olFKpuBgZ+fT9TzZhj+a0nB2KYrFcZdE2FbTGaDB9SgLOuI7T3OvxDzuSkNlDB +hdv6p/N4+JXUJUzmxUeoEivRJSm01c0Ncb46mqOliIUVmFnb7/+nSG7k6fj1BNs2 +4oEuoxT/5VxGkzNvXEIOvFg9EbiMWg1KidntqS+fVBDH2yB/ITpkBgPwketmdpqX +BlI50iV5bq0NxJGEO8AEw+GCeXPlhLGq3QN3QpUL/7jzgTc6VnlU9ZJoS+R3D8Do +7jmO4y5nB7SNOCs4hUjMIh1SwCQi/rKHAgMBAAGjgZ4wgZswHQYDVR0OBBYEFJU8 +XRWMR90LxftEfCy0cUNEvGblMB8GA1UdIwQYMBaAFJU8XRWMR90LxftEfCy0cUNE +vGblME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGU1LmNsdXN0ZXI1LmVsYXN0aWNz +ZWFyY2iCHG5vZGU1LmNsdXN0ZXI1LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAeziF15irMtCGrFQmHtDCjlwhTEKcR9usL0Pp6piQ +x0zlrOfoFB60MJM+/nZ0Us6r5iPid1SRakMcqzqEq+7CE3EIUjUp78fA8kQDFB8V +TibegrUQU01hsdGDae06L9iytR8O9ViXOkemEQI2X/VT+3eZ0fTSQaPHeomKGNxY +kFFpxWYH/OHKwLe2/FHkPbY1KkdwFjIMkc1y5mQYvt0kihqboacBPpzU9uvOC7r4 +dmykTRdWPp0320uYAdG//UaIGkEVJI/IZq/QhyWa0ByxlQ5kgngHOituQKOQbIwn +f+GEPajEMAJUh6FwSQ2Qey3rMGDhl7oGvnrf8KG7IezyuQ== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c5.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c5.key new file mode 100644 index 000000000000..81a2e58bdcf0 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c5.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAn9QR3L0IsLDUvxzxbwKe38s76xNdUuB7y9iSl2xYxehyw2Ld +LsdL2SeaJRSqbgYGfn0/U82YY/mtJwdimKxXGXRNhW0xmgwfUoCzriO09zr8Q87k +pDZQwYXb+qfzePiV1CVM5sVHqBIr0SUptNXNDXG+OpqjpYiFFZhZ2+//p0hu5On4 +9QTbNuKBLqMU/+VcRpMzb1xCDrxYPRG4jFoNSonZ7akvn1QQx9sgfyE6ZAYD8JHr +ZnaalwZSOdIleW6tDcSRhDvABMPhgnlz5YSxqt0Dd0KVC/+484E3OlZ5VPWSaEvk +dw/A6O45juMuZwe0jTgrOIVIzCIdUsAkIv6yhwIDAQABAoIBABrkEO9NRNGJpvnS +slJlo/puVNH765c+msWU1jEw8wy1S93Q2/r7YCkUrywu+m/RhrUweH+ul0W24+mt +peqfEHaBaWuaEB7ljD2S1uhlLJTuMTRtcm2oj9THo1fceyvu0zKi5fnr1l8SU5n4 +QTiiQkJRUVrF2uPGgPG9tNy62A46bweBuI7oVuEd5lBYIexUCJEdO3G1xteFVA15 +Tvf9lPKygxLUaIguL1Kg4oxO9AFtkU2NMpnnW0s6v1T3Bjtal7uFpySvkoiRvJoO +eXPU7xDHwo8t1xtAYqbhjx8XfJhI9SBHq3hTtfoJgRf/4dK+F46LbOoWWuWvq2xI +1INf3BUCgYEA0QyKwjwK7avyPOnVB2VvjYql25nsnFeh3uYqE2Kjh0BJ7P088uuX +Hc/QO9xPCVN0RDtScCAUI7lsJAoNgzPvY748WcardJZ2XH4XRvXWAbWnrYCLATmg +qGvWR/XncEeLDC9O3vT3o/8UhKD4iphOnt4l2wxaUaXec+0Y80COwDUCgYEAw7mL +uv9NZbDPbZnBozabEqyzmgDA56fYlUAq0AyNMADzypf0o5XUbszO3KsNJ7AaEpDj +9RdTPbojPUXH7RQtXSiKynxpjWXqT20LBPwSOFn/GubQ4Ioe8ShqeSAJ9OrJe6pB +mR7idqb5kZrb6bXcGayKk76l4CEJ+h95dWR9N0sCgYEAufOmGlC+h9vDhluZmxKn +wAfssQKibg+ldVjXZJcBT3XoTF6WsJ7PtTz9xcGarHkHmwoJsqNWRtDRWN14V5UD +BH5bwJ/wM60DbBLdMcnLq4mitbr56wUP28DQwwIB9OYu0Y/jYIAXKFHmNnPZoa8J +gFeq5MaAMNCSqBvnbbmnQWECgYB5yd8UwQAcIuUvwdkoAjaEbEplrcpbW6n9VSxO +wUCpei45Bk0H7Zd+Spr93MIWzBGtM9NlEtqNMmHzIpTBBSklKrESRlA32A7w1V1i +AKglwHpo9u9KiSev0AJQ1Irdh6tP9CVVbk5knon0/HOS/cKzhxXD0YHN8UEQDr25 +Oopf+QKBgQC8PK1uPIIFdUAcvpv7KCoA+YTHFCLrDLeZ3H2I07eVKmwzhEivvYGs +dNSQC6KxhFSTeVB2D02AiiOcdAWwY/jZSlUOYy34m9dd3rzuQ+A33dTcEIb7BMCg +ezoMD4v+gLfbQ3gve+zm9GR8xvkgGrKZAjFr9/42XTGDJ9XSCNVHSQ== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c6.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c6.crt index 5cf212017fa8..820afacfb918 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c6.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c6.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUDJQ6adYh3ugguVhpwXAztpir0/MwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjUuYzYwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0MTMy -MjE0WjAQMQ4wDAYDVQQDEwVuNS5jNjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAxUpbKRNQ/WxzJj7oYFBzeP3/JwtVTaTqlJexfKpEXAGlTXrUYRVyZP804p+2 -Dc5sAFEFXwUcmYgVlLCj+mc2OGg9Uv6EyaT1Bz0r4oEbIB7ePBiefx41i2Rhd6zy -3Jg164hGnrHzmOdqhz0hjKxUAgBFzEtfx0URtIpscMefimkCAwEAAaN/MH0wHQYD -VR0OBBYEFEyrP19EhCIvYsJFVyZPZ/2Uis/gMB8GA1UdIwQYMBaAFEyrP19EhCIv -YsJFVyZPZ/2Uis/gMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU1LmNsdXN0ZXI2 -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQB5b061 -fk5o+h+m/mOR7pi9TKwKnseiK5P/N4Gv9WuAyN+edee74+/MfWmLtGtTyeZQ3XXb -Twxfr+UvHzHDN+0QHuQIj77DBDTxfULKF6AXKoVX4NGABDdhKCkxRXjdqmq19G+q -upeZXyel+qvteAH16mzrVMGu59gG2IGz5+RMsg== +MIIDTjCCAjagAwIBAgIVAPIQAY+4wdoyfeEuTqz3wwylFojmMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW41LmM2MB4XDTIyMTEyNjIxMjI1OFoXDTQyMTEyMTIx +MjI1OFowEDEOMAwGA1UEAxMFbjUuYzYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDTnSgWiqKP8NtGJt9Ip0cw7FxNRHbysm/qnlDTpvAMf3GSkvdG4IO2 +B9I/9lc9Ch/Ai+lSzZh7HVElJsReqsRB3BeXzkwW8vVvgtPq3Kvb0bg/Q3gCKWx3 +TeMnZy1tX5R9IcJP/Cp8eCby+AI/3Xhg8nv/7A4iP02QoPxiGw6VcRjYl5HHFlzt +ECQ1ppRta4/xIiAuG3883j02DIbpKAfWfC5zPtugkJxM2r0jKPDvuFNg28D4GPPP +Wykc0+AH/38tWotQ0aOiCY1m1FNO7KxhbP7vXfpY2knbYphvPo5SpalQECq37oAx +yRGGP756k16M8wlyzOhL3moOAFLx3zCXAgMBAAGjgZ4wgZswHQYDVR0OBBYEFC5Y +lfZOKmazs3GfLZ5slVeXX/whMB8GA1UdIwQYMBaAFC5YlfZOKmazs3GfLZ5slVeX +X/whME4GA1UdEQRHMEWCHG5vZGU1LmNsdXN0ZXI2LmVsYXN0aWNzZWFyY2igJQYD +VQQDoB4MHG5vZGU1LmNsdXN0ZXI2LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAA2wsusK//Vz00qcuekznVPnJElKunw1a6a8ca3pa +mnjx39D5FKDoI0hwfpPkiPKADc0uY0CZgORsPUlhNF7K/fMirwEfQfBGlEveqCaX +RLlB9QVqSXAwMYQuAPnsqsGNqkI41Uv0TIIhF+xqzt8BQ0YnJHOHTnHEFbQVXfhH +g6Itzy48SlL5tkDTh0XeXieSom6wCh6Ql9Xk8NSU1mM2cOShJQF99XHkii8KmXGr +Ac3yck8FtlCZ090+X/xuM2TzqFMdFeUvGNbneRagY/CiqoPZHifEgXkQT6H9P98P +oxu8gvqtDWCQlxSIxeDZpE2wniJu+mpSv90XSJyfg7ANqA== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c6.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c6.key new file mode 100644 index 000000000000..6ba4955d917b --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c6.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA050oFoqij/DbRibfSKdHMOxcTUR28rJv6p5Q06bwDH9xkpL3 +RuCDtgfSP/ZXPQofwIvpUs2Yex1RJSbEXqrEQdwXl85MFvL1b4LT6tyr29G4P0N4 +Ailsd03jJ2ctbV+UfSHCT/wqfHgm8vgCP914YPJ7/+wOIj9NkKD8YhsOlXEY2JeR +xxZc7RAkNaaUbWuP8SIgLht/PN49NgyG6SgH1nwucz7boJCcTNq9Iyjw77hTYNvA ++Bjzz1spHNPgB/9/LVqLUNGjogmNZtRTTuysYWz+7136WNpJ22KYbz6OUqWpUBAq +t+6AMckRhj++epNejPMJcszoS95qDgBS8d8wlwIDAQABAoIBABnyCjjpT+aHc3sg +clP9c98YZnYaz4gQ6g6W0UuCC6tHR5eJvsPJQfI5oe+xtnSHdJPZE7HRVMFo5iAV +/BGsrQTI2s5ZaZQ5acT9rMSpFcaNP3fnlSPpYaEnGD4mimKp9hIkPNItwCUkCN0q +x1Z+mFnaoUAkTdSpH6DR/DUoeBtJBNaL1FPyJxBZ5LsMlAscE4mYy7vHafAPkjBX +gFF5ncODUQzHx5CU7bpA/BbQRZu6RlZYxJok6l7xwacOArVH5275vTbfA6nGdYEL +FX8UKXTKabNoO6on1qW0Q3B2h6QQcHdaLW/RnW7rqBDxvOsPJV3rYEKuVWX51jM+ +AKEqifECgYEA8B7/5P+rxcINlkNbd3dWf+HuMW3qrq0p8KhvfUoMpHPqkD/g02n/ +/l5gw/bant8GUETSEtH5L4FdPuffSqIhE6apVId/9WpYqzwz4VxgPhjcqz4xL68/ +wy0SwUuE0WtgnSDSkYlBJhewJxovk7xs5yk5wtcGWeRAkf2LfB2i8RsCgYEA4ZuP +TMiRuVj7+m3doYxejVcsSVcaetAY0VhwhQ5G2OjquJkpxZgv8iwcsxw8w2Y7DCi0 +LqPmEFi8KNZt04vN6lcvPkknCSlJYo3WzRTBTNfka3cAg/x5jtn4+g4wjGp2tb8N +3oeNY5ml/+sqraNHvDFiNZKUyh7U9Hg57MDkMjUCgYBCOoqiAtLB7NAKjfGAde3p +v8sO3x5Hp4NV2V9G01HBWidsOfIs3aO56RYPdtq9zw3SveijQD7Wl3u8if/Qef9d +iC5UHjtw2TAzqps0oWH6Pqk+ohy25kNT9e6iWc38ZP102US613ycoRP49QQCX4o6 +B+jikKQzzvzwR1pdlV8H2QKBgQCAD/5S1pMpg+JcsBqbhgNx9aa05tWoGxHLW4lb +bdQ3clcye/ajLBUjQXj1DrVLMW+ZIk3NH8oLQ2NjRvzGlVhIij7N+1ZGNAJtCBiD +ByIGjZIikkVfpmQV+5MB1iYmvpQt7Iph484VWHVLBOGP+NxSbjHSOSduZ/v33Xr7 +8i5WdQKBgHqQM48U7idLfyRliXo/xlIaakpsSHcouVHUKQHzXwHj//WkN9+bXsDG +fqFhOVNWI0TtCpnLoOjVSXOdr/NFHbDvZ1bzfngScn9q0Tv7l/UmB4r7ZhiqHjUX +H31Ig+97+yY3hlUjtqP1DUvUry7Jx0zydidfGXDWtJ6OUog70jqg +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c7.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c7.crt index e928f5fdb5de..e666e854dc14 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c7.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c7.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVALd4gLmbZw/Ym+O+RgzBT3rore8kMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW41LmM3MB4XDTE4MDQxOTEzMjIxNVoXDTQ1MDkwNDEz -MjIxNVowEDEOMAwGA1UEAxMFbjUuYzcwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAIOhA7sGBQNF8E/f+5jB7Bw3oya3w6vHWbGJtPeAkA3HXQWtw0cR7h75TxyP -tsjYa3Pi7CF2tsV7raZhX7tVJtwVq5753TpCtBVOVXcjXhXR/Hlo0+EFVa+l2iOf -OqiGn03CwILdpNNab6HGIQGHe3xsk2GLG1uMI8clz9uo7KRvAgMBAAGjfzB9MB0G -A1UdDgQWBBQkRUJkntKPJPIaIRVydRbQeAPUCDAfBgNVHSMEGDAWgBQkRUJkntKP -JPIaIRVydRbQeAPUCDAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNS5jbHVzdGVy -Ny5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAQJlL -BeTrtZr94sm0hYdpl54dPJciBtyWi8XMw8SmngyuDaQaLe9kpWAbrsDX46m4vXAd -FkWyRZexDS4R1mBuMHjcj6hSTAFOiy6ypdQvfCuzvLxLEq9NXq/4y+2pkp8G7c43 -Kcnk1o3P/BqFoHcwKV2oozDgIYawQxvE1fIXzHc= +MIIDTTCCAjWgAwIBAgIUb9QulilVWkQvUt5SwBauRXnkkHowDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjUuYzcwHhcNMjIxMTI2MjEyMjU4WhcNNDIxMTIxMjEy +MjU4WjAQMQ4wDAYDVQQDEwVuNS5jNzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAKHj9oMTSE8PaKWuudm4HBlJ9AyedCQbIO3KHO/G9CoMQWMa1Oih7Sfp +cXfBwkyR7gJ82ogyS5sB5rUTl7WmgJ2HxdPv0O2vs6dwCL4OwpKxciay0283OE0R +w9xkLjMKCW6eZ7btU7qD0bRJKq6fG3d4DUc4EL53nyGZnNgTNPRwzwV1YD2vomum +SYxmLD9I3wBWphnTSQyDQGqt1tMqDlPtVnNXqyuwmGh8tPsX/uHCwOll/1aGVeSy +NrECLuc22FzuMbXf9FP8pQ7sYgqUXK5jkyZA6uduLOg+yi4zsFA+hT0G/+T8cam3 +SnUanLFQ80Esvho/RoxLz/31wJnv3tkCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU7+Hw +R8ErGM/gjrllcPIGK+6xKRwwHwYDVR0jBBgwFoAU7+HwR8ErGM/gjrllcPIGK+6x +KRwwTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTUuY2x1c3RlcjcuZWxhc3RpY3Nl +YXJjaIIcbm9kZTUuY2x1c3RlcjcuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQCHXwx17N7nu4ssT7Os0WJo1NTG0vZY2hZbWuvS7YfJ +Zv8e1CUg8Namrzi6bWBVJLnAwtIdar5CtI5E9x/3Tz/sAdDEC/X5ORYlt8W/smIY +kgm1GWDmB5sTme1Nr5kHpEv6TPdn2DVW6reTlA9KX0LYFOrG3BHrhbwAMkCTk5iE +v2VyxZUdJF6PJ99DlaFHGZE8klHLn7M+Far5jvBs6hze0DcVwZsH/SWX6qVFRApd +WNPIyDT/SaYN/OpsfmgmTrko/ZkUujDZ0DIQGfvSHy3PG8WpKiBQVJJMIkKyzGQm +J8b1oiYSEQngs3OofkRJHfHX9vIqMrn2mdd3earap5fk -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c7.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c7.key new file mode 100644 index 000000000000..b222d127c8f6 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c7.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAoeP2gxNITw9opa652bgcGUn0DJ50JBsg7coc78b0KgxBYxrU +6KHtJ+lxd8HCTJHuAnzaiDJLmwHmtROXtaaAnYfF0+/Q7a+zp3AIvg7CkrFyJrLT +bzc4TRHD3GQuMwoJbp5ntu1TuoPRtEkqrp8bd3gNRzgQvnefIZmc2BM09HDPBXVg +Pa+ia6ZJjGYsP0jfAFamGdNJDINAaq3W0yoOU+1Wc1erK7CYaHy0+xf+4cLA6WX/ +VoZV5LI2sQIu5zbYXO4xtd/0U/ylDuxiCpRcrmOTJkDq524s6D7KLjOwUD6FPQb/ +5PxxqbdKdRqcsVDzQSy+Gj9GjEvP/fXAme/e2QIDAQABAoIBAB+TBItAAYRZ7wER +ePH7tI9CGzUy2lgIan22Q7qIRSrGRSJ9IzVUD5sPLrA6vBiPBaq6iyyxEoni7wo2 +DgrBtKgEyRKy3T7eW922Ph5DZrORpD/j9NTmIjb11gtwBoqx1HEcuAG3e/MOrqHI ++2gfZA4C/iP7zVg4oHK/MRX7eQKLhkjWLVmxzzlWTL7k76M0PyFi+rooG2wMnRYA +kpsnju7M+tGAcYT05r03aE/B73FfWFT74EERCruJY9Amva/sVszJgiFUHrUiVDRm +HsUqQhr5468VQeYSkhYv7KKPkTsmDY/zmvsRZxbwnHIwnBctHdr1q8fmfd+Qb1gN +IDeqYuECgYEAu7VqYOasRp32CGIVwC2NgfitlyFYthOyf0WYUm6WY/DZgXsx9Ixf +f5GS/gls5ZYhMfHn7qUZE9iZdbLGdD4x81ki5ATyJyPARJZtIRxL3VIPJlb39rCR +jztfDsKg4Pp5cHxXAD2bIAT4g+CA814pIRfhytGrU0zc5afrZ0SfkRUCgYEA3Mns +1M15/K/Q5hflBT3qlUa/NRTcrjUE9xaha/bHdOeV5wZsEmdFAZB9sTFQG1on342z +EzKfX1tdn6zoaks/KX0UG4YaizDGn92DPGDxoUN+thfjZDmoBM4e2CvCeymP3zzC +NKKGkXJHvZeA0iWB2M8irctbPCYxDNrbmQwJ37UCgYBHaA0OzdGtj3u/1qhVkj6p +udM3kdzwbu/RUMVQXndhj4b+pcHsONY/bj6P20AM2p7IJDFJ7I5sqVI/nwG1yM8x +tq/6NBN2ZjhHHz5mgSAvEn9m42Cy/Jpy9XAE6m9PBpnZ+8MQ+V6gwKNVb7s+WYyl +5TLk9/wH1U6XItj/UPCuvQKBgD2r4Y1nf8WHTrCWyA/4xYfA5TPsid/XS3XBSoGf +wwLylTqc3p8dXXccZx+RiLC7NBwLtpOMNlwqZzJs19apAhGSsIUnpk5u52xMMZYx +9gx3j/NJJOkf+cCt2ovDXWWNrHRMwmp1Y2VJKKTaps8MTwLCiQtGnEtm15ihoWlU +pYVpAoGADCkaxfyiGQAvZlHXViu+ks5/fOl4isdVYXQS9SLo1vxOK8L8kGEZlW+I +o7idusBYoWEfTmBPPSmEtYiMYgEr/XYfGwU7gd3uepRjw2ykmBOVnDrvol9v4vHi +Mr7o2Kd2AbRVRTN53Sc7awhXgiHKKwAGnOg29YzSwENL5FIWqkQ= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c8.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c8.crt index 5cb3781514f7..b2fe0e6acd0c 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c8.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c8.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAIQ/OUvFomO/a5eOYDI6WJ3HM5VuMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW41LmM4MB4XDTE4MDQxOTEzMjIxNVoXDTQ1MDkwNDEz -MjIxNVowEDEOMAwGA1UEAxMFbjUuYzgwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAIJQgqB0qJj7Db0GiPnvl4AsKtFa5DV52/DLfqri1FwhOcIVhtnLXFA2wsUT -/g7aJ/b3tZszfTwG0mKSmNS9voTlpaLAb6Gn+vq6bBpgvgW3kvcP+r5lxMIGU0zs -8yC3hqXwUOX/FbWZzd87eEiM6fDfhwg4JbP7nSk/JX8MUEqLAgMBAAGjfzB9MB0G -A1UdDgQWBBR9W6ltaRBtK+Q3dWeXPFp5cwdSFjAfBgNVHSMEGDAWgBR9W6ltaRBt -K+Q3dWeXPFp5cwdSFjAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNS5jbHVzdGVy -OC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAW0cU -1UW5gZip0ShYRi8iQ4CiXreN0BcR+Snts9b5BCf/P4mjg8x+55Ewz/+sVigyI8cV -h1zgMKb84g3bnxG9vahb1sA4N7q4o+DDgDLCoMNUUgyuLWa+TSdXVfqDONikIosr -fkJ03rnOg57QvOto+SR2hiIDC2zyHif6wUn6SOI= +MIIDTjCCAjagAwIBAgIVAJ3az2MqXVBVL/PtjC6PanAQkDfLMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW41LmM4MB4XDTIyMTEyNjIxMjI1OVoXDTQyMTEyMTIx +MjI1OVowEDEOMAwGA1UEAxMFbjUuYzgwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCyda/Ce1pnhuky/aB0eyiMiNixT02IKKe44PBTJ/utWTzTHLMrb4xC +ojL70dCZGgWtCbiSTdr0y++ZcSyvdrMOlu72DajFJy9XDd8EOoHaAhwDwt0Foj4T +feJ750f+nxOrE/TyDWYIYSBhnNTlnnjIzw6/nWQUIARDidJBD8wT6TUTZO2K+Kdn +oiUoSJ0kiQvhIA7BI6MdAheHhzB1vxA1vAt4JfBCuUJ4AOm9x0kfXs6zFEtdJzWQ +FflLxoycWnB3FN9NXwJ4QuXFHEgnJ7ryy0DwTooPKaevSchImpjqs5FWwOcQLD6f +jdVn1ByJDRX1yl22X0aAWJh4sNe0JJmlAgMBAAGjgZ4wgZswHQYDVR0OBBYEFEyl +MlqF6EtNYPt4hiOgMcAazTp1MB8GA1UdIwQYMBaAFEylMlqF6EtNYPt4hiOgMcAa +zTp1ME4GA1UdEQRHMEWCHG5vZGU1LmNsdXN0ZXI4LmVsYXN0aWNzZWFyY2igJQYD +VQQDoB4MHG5vZGU1LmNsdXN0ZXI4LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAGMcdCezwP1qmr07WtnyCxU8qBi1IwP7O4KxAxXaP +ElcddnxBBAQXH6S+VNcwLG1+WcDy9vgY0FwjjI+pdsND6LpVU7v4gBVuuaeF9M6Q +Il1IAyUybspQzxLjI5CaqP6jxau98z3uW5dl3TG1dMHbpgpM0PsATAlS/1dE64v2 +DvgUD3iRzvFZRhe6oqxczATalBWl1P8R+JiRWROfzCuxSvvlAoAtDLukPxqJ4e0/ +TDcGVEmon1FIl5P2LP4E8Kox1vRAGN1Yw86UQEs2ERmu99dUaCBURDUpQeE1nz4O +g6GYIoqJI9Hu/aRgXtci9r5V291ND7mDuyHSrvUzzrKJOQ== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c8.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c8.key new file mode 100644 index 000000000000..7585be097ebe --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n5.c8.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAsnWvwntaZ4bpMv2gdHsojIjYsU9NiCinuODwUyf7rVk80xyz +K2+MQqIy+9HQmRoFrQm4kk3a9MvvmXEsr3azDpbu9g2oxScvVw3fBDqB2gIcA8Ld +BaI+E33ie+dH/p8TqxP08g1mCGEgYZzU5Z54yM8Ov51kFCAEQ4nSQQ/ME+k1E2Tt +ivinZ6IlKEidJIkL4SAOwSOjHQIXh4cwdb8QNbwLeCXwQrlCeADpvcdJH17OsxRL +XSc1kBX5S8aMnFpwdxTfTV8CeELlxRxIJye68stA8E6KDymnr0nISJqY6rORVsDn +ECw+n43VZ9QciQ0V9cpdtl9GgFiYeLDXtCSZpQIDAQABAoIBACxrwOEfBUTJvxhT +K3JzCYfZtF6GR91BiWYqj4eTZ0Eh+lLuTObd8ELRzvEBq/PAF3OX/NpmCL9wV+aQ +1jeiyzUxeq/CfKT+ePkpWb7atWHHrqN9CLkTRnJhnlOLIU+O1z15TTpG/YPOcVn6 +yL+TL6uEZr5pdfTQd188WhqZzQmTU0PG+kM3C/28qYo5QLpj6KAdRunRRMez05Lr +GhyWbFsL+3+RpowhNdwDyama4oUJAL1Z9RT0zEMnjS1xJCQcvShEeqrd4FZAw79+ +68GKgNrM7H8snBLcXKYLgI9fYE0zrF4V6rdzz32+FjVDPQbJETOUZNuxZey8bgRI +2KlNhi0CgYEAxUZAquV7WQAfP/UY5JQQnugBcBNXIlCvNVku/i7N0u/CbIsegaGl +zI8Ml9olYJKZpkD9/F5qgSTLgPaFM94K5YjHoIV1o8NVjM7wWDrFMedNKs4YcKHF +a0uWul7Wsd7xK+R6svMluSs/j0Po7gmXDSHdZhf8q2U7QYbqBUm3v0sCgYEA55Wd +sTNddkCVGEhP/I1SYrWkJrRTA6GVCngPRUE8xUr91WtG5JDKXNxDpzCxNQXtehM6 +SSygF828bW0unaG4ThLDJdbbxs/m3cR2HTTFzZZQ+uecqjgjanEpYsRmtPXiOnuT +YocKBZA27G8AsTReWpwQnWn3bbw+rOpLEVejRM8CgYB0T6dxGzxOjTPfkbLLkkD6 +b+/pFH4bTVcALUT3uMDBzXrkZGHoLFaE9RKHtaaz+bt9unQFh+Vjnz4dGzwOCKyE +CNEqY4WouFKIYrpySrRZZ/andjwSrE6ppDeKSsBqvveS2XM+bv2e31DqNy/uqNjW +dVEEEMB4jYcSa2kvFPexYQKBgG/E5/yz+7Gs7chwbhpH4rE9VWJQuZgRDiulUkls +ePYHgYbVZbu+ncw/im9QstYSbFD0orbaUNHsO7rcbRkrCha0O3cxwZSslpIF1bO2 +2pikqaTOz+/fdEhVIVJH1AIWgf8T0UhGQHH67mciE8Vyru8wT1yJ3h8aMW2nDl/q +WiRFAoGABUZtvfO/A5TQEsATIt+RqRTuVM7a62E6a4A78zrrAZsqASbcLAUDzXuw +DVOxvRd1lmvYJSTj/Kl/OpGX97HywtRZiP++IusnEPpTu2K5FhC2670QT2OyJs7/ +D4GWM97PdR+6TxO9BCZYyjtHJLq4/EBv0uOvMAOuhA+33Iadcq8= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c1.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c1.crt index 5c822ab56b20..93c946a51491 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c1.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c1.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAPfDYqSl581XnG/kfEiDs6IEfKWnMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW42LmMxMB4XDTE4MDQxOTEzMjIxM1oXDTQ1MDkwNDEz -MjIxM1owEDEOMAwGA1UEAxMFbjYuYzEwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAJFHcBG8RLrI/ifvapZW6f42HHBEf3U2FFqAwpZMX8IDVDddF7OQNPBEUrNQ -2dCFX8pkcEJcnnIlGP0NDN4JEV2N71NjN/WTwBFoHDAXs6e1aKaLRv+Hz5xqIdtb -5QfqjbuQsHFoE3HVrbDZn60pENN6JFn3ddt1nGYamMRycS/7AgMBAAGjfzB9MB0G -A1UdDgQWBBTLEf5TUnKZHJfUMV/CQDFT+5k9FjAfBgNVHSMEGDAWgBTLEf5TUnKZ -HJfUMV/CQDFT+5k9FjAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNi5jbHVzdGVy -MS5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAcDU1 -Z/0qJCuqIXugAbeBQLBiDwWpsPC2AatBslCaHm6prnA4QCGwZ7wI/FnunMzmGf13 -VvFEEWnW786+mh6WkI5iEba7sHf5vWmKO9rBJHYm3bcFnh7A2D8hvf7DwFNG3Kpm -+/iqy81ag/BOveDlpu5fJPzAvJwTpVJ6ie0XQ8U= +MIIDTjCCAjagAwIBAgIVANLIvdFnuHRekTC0k1oKx8PcCjk4MA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW42LmMxMB4XDTIyMTEyNjIxMjI1OVoXDTQyMTEyMTIx +MjI1OVowEDEOMAwGA1UEAxMFbjYuYzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCc6djaJFv/WVs7h/ovZq5KT6fIdV+PBqb5nazw4WQ7QqtADqFl8pCe +lXFYG581/Xc1SQIvngWIIq1Cdm6yu7mW+sLkuDM/milX07HdbE1Oyi0qiqOEoplJ +OyqdTkY0Pi+aNUUV63sUmWO8gv468dmgMLZ5STS2QUoZNYdDdaF18TABWjVLXS0Q +ZkIpgHz8HolmQZfcrsEh5t/8lMBVylAG5gJSr+SAVuH6Fchgicy7+6OQmf101ysq +Ln7IhExKz/ZM1iXoBfZA8mOmfD7JndHsi9IZNLPFo19olMTOCZHMr7NPjKv+9mc/ +w12+rKxJrQf0i8dKUb5HRD0ndb46X7Q/AgMBAAGjgZ4wgZswHQYDVR0OBBYEFEoU +y3Rv8fC7nUGGgcbTgUzNudj7MB8GA1UdIwQYMBaAFEoUy3Rv8fC7nUGGgcbTgUzN +udj7ME4GA1UdEQRHMEWCHG5vZGU2LmNsdXN0ZXIxLmVsYXN0aWNzZWFyY2igJQYD +VQQDoB4MHG5vZGU2LmNsdXN0ZXIxLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAfuNjWsPsYZdd3ORwNr9qKt3TPGbNRSONzKGYB/Si +I0ULuKYBKQqXkBFOj0QAWm+nmfBWTkDdZYkeDNAQ54jpxznL0uqi0AGuxstBPYLv +TLVjG025AHVbKQIBZiTdsunb+e/uR8GzJHMpl1yuUVHuoFiWUzCvb57mxnkYKerV +Pzb8suDF+gV33BepudoGKpmkNTE453Ic2QgTz9+LHHBZWzozDWhj71t8sYtpm3AG +s9+rgkL0WIns2qqqZ3goC3SbRez0M2Jv18fzcDFLEkcICuJIq+sgnzGcm+9Ya3jY +DuinoRoK+fR77kaNKC6KSakkjNM5nIssL5g/9szbDFxVKw== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c1.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c1.key new file mode 100644 index 000000000000..5e044be083be --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAnOnY2iRb/1lbO4f6L2auSk+nyHVfjwam+Z2s8OFkO0KrQA6h +ZfKQnpVxWBufNf13NUkCL54FiCKtQnZusru5lvrC5LgzP5opV9Ox3WxNTsotKoqj +hKKZSTsqnU5GND4vmjVFFet7FJljvIL+OvHZoDC2eUk0tkFKGTWHQ3WhdfEwAVo1 +S10tEGZCKYB8/B6JZkGX3K7BIebf/JTAVcpQBuYCUq/kgFbh+hXIYInMu/ujkJn9 +dNcrKi5+yIRMSs/2TNYl6AX2QPJjpnw+yZ3R7IvSGTSzxaNfaJTEzgmRzK+zT4yr +/vZnP8NdvqysSa0H9IvHSlG+R0Q9J3W+Ol+0PwIDAQABAoIBAC16EOFYPFmLWkes +pzPpvIXJBEWyztkzAl3B9wdxK42JSvyrwNc9L/DmOw1gJAqPxWbqG/oVAGFuO2zR +yk9+3NEHKEkFocTDN0w/DIOyCc3WBrUXrUCZp3dz8Gva7bnidAVrfvRVZTyZIDDY +fjozMfGdbKVGNELGNWWNAADrLLpzkf2auItVP2MdwbhGnSUGjRiF+4KwCPhJR5d6 +OuTatvyw5bDAB7tG3LJSqfJbwRoGGNErxQB2OKGtApzwhthRGVM+1Beyt8kigdzd +EDvfFvK4d8VdOTxVFjroq1obv0DHMGPQ37AXgLOI2AoxKBCOWYNhirpI/uv1uFpZ +31oKdSECgYEAuSW2tbvAq46Q5YAhthin+cocYUDFK5cWXBwiqlVh2RkdBXdFYax7 +dIL1VKK5/99GHX5zg7+iR3bsKrz4TjBthWvhDTdjZzYHUcHdgD9/abhs4NgnIgqV +J/alo2emQdTx8rxBBu6p3JsIWLGmGkrKiQBAm8XhmACkAE9DHEGZ3JkCgYEA2PYn +VmYMVgykDGL0+558tI1OGtzGP+YqJSTBFrAVtk9k12uNjBUwsH0XtvuLcok15zrR +nbDZp/DAEtbp4gDwXlQVerivEsBGAW51wa5GllPh/RAkQg4aLWri8WGMSz5ORiDS +452BOslDC3q4UFyonuaPEgT3qtvmIUtuXvXrBpcCgYB4aRygqJCvgjKd72z9Pp/Q +Bi/a2ALjTImQMgQhfxH53SlQqrcGxOQtbBYdiw8oZ470E48QugrSK3eAfBGI2FoC +EZz1uXX+Y9byxwBFLgaxPO4idLFOpE9JBFHJKdmoqtMHqIMp/QGfpXwSIscCBm4e +F3ntVQ5YiuIeCW596/Qi0QKBgQDFrYGp9tDGlj3iC0eq+nwlBZpkX0NCdTNxJEvJ +oCYoNa5rmFMfjy8Nnuw8jHRBp0K1q6HRS5EEsxZOFAKFKNXZHEN1zU+2hflC1KH2 +FhO6ZDNDn7Fpfe/b8cdUpsxswhgPTVjYiml0ZmvvyaCll8cLAIbapCwEQNcs/s1R +OD8rgQKBgBb0NoHC9OQUbVBFkNdmeHG0x1VbB+ObgGg5yXSLeQd+Y+Vxgv363i3i +sBv7sMyS9E/Y/sSY7JUmnZACpl+IIsBa4NK+ulzobeos8sXXm/ni45E+GPelhd0W +CBmjvolZzi/TQejB30DjizY9fOFafdUpaVN7ZD9Xwasw9WOkSxS+ +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c2.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c2.crt index 9cf522d3ec27..bb152cab30ad 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c2.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c2.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAKSYs2CPma3+QQ1KFujo0UdYTPChMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW42LmMyMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjYuYzIwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAMJ7+brk5AZGhtD9dMKVhkHCmsAhULpd94aF8nK50Vw/Jgf/SdE6yYePrBxB -C+Or+o5+N8Dwe70escEOH6o793gg8yBWyL1AagoJZDV/+A41cxropz0jVW5X4SJu -+y9tEgVA7m6cdefow1SG1EiMLMtEHyw5wKNmUKrl544O+MQvAgMBAAGjfzB9MB0G -A1UdDgQWBBQGIpG1JOyVo9JCyp1/h9yMhHE3lTAfBgNVHSMEGDAWgBQGIpG1JOyV -o9JCyp1/h9yMhHE3lTAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNi5jbHVzdGVy -Mi5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEADfpT -pfnIHFoNw5xvdxOelm5S3CKOk7lBroCjnowGaR59D9yoPRlyaX+YaRO51Mv0rzvS -3qJjjrKlCXeGNF9rKU0OlM6MVuGmlMhgj6nv+0eX9ntHVYzSvbBxXUx0/jvQZYV7 -C2t1qljMjpFoi++p349dWsMfosx8X97HhXA7P8w= +MIIDTjCCAjagAwIBAgIVAP7ji9cnys138Y380I6ku9lV4ZwKMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW42LmMyMB4XDTIyMTEyNjIxMjI1OVoXDTQyMTEyMTIx +MjI1OVowEDEOMAwGA1UEAxMFbjYuYzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDWfZjEJbSQJbOa3fZmzSZW+KtZJuMAc6aRdWOR5pfluo/W7VlD0wOz +gdvDmbQDJJW9UP59pB2u3a73dGxVqKS9aHaLrWcbqjHzVWQcrdeYHzOBRzNqOWP/ +TkX8lWhaE/tMphI0qgJSrqFyfq7h/j3rYhUgNwn2OuQOL+pKhgK8dMigsXmpCYwR +vvX2sJu8ti8GUEPwwekUinlBzB0GbtjdFEHApcJmo7NMwZuCGnSwKczWypTQgBIZ +s34Pe3Sen1/nQj5iWxrNMY5dib+7Gy9Gfva9H3cbHFfhX8TWrJwtreLYt8ByGFHB +ec8w+Vm9uJQNY/m5+Bfrf3W5YUpQVQpvAgMBAAGjgZ4wgZswHQYDVR0OBBYEFLrN +K93Y6ZEYk8ybn4MjszXVvGkgMB8GA1UdIwQYMBaAFLrNK93Y6ZEYk8ybn4MjszXV +vGkgME4GA1UdEQRHMEWCHG5vZGU2LmNsdXN0ZXIyLmVsYXN0aWNzZWFyY2igJQYD +VQQDoB4MHG5vZGU2LmNsdXN0ZXIyLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAgi3FqDRIZLBXLzIkXvquNA6pL0Ed3IHpZS7wQHOT +AboqH9nS2rVPeRaW/9K+X+dPexLfuxl6fe2vBdBX+ZAKNurYGW3ppTWQQI1dzYDw +UlZE3nvcZ/xgiUcqe1g3+trx/HNcae/YTYGOUEQeTJGPE2CHbjKhsbbbX38cymKY +l2TcN/jFm/yZ8mBo4nCTKQey6bdo/aPtauDhsyNPn6K1+JHkZ6TWSDkrHhE5IiH5 +ddRa7Rx7ChUDIXVFO4N1mRSrA1bcqftSZ1xm81ZY1NwKAiPX3fblcYJ20DWbQh4i +wHL1hkP24ZgGAshGF4FlqUj0iq5OY6wce+ey9FDo2nyj+g== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c2.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c2.key new file mode 100644 index 000000000000..f43065033ebc --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA1n2YxCW0kCWzmt32Zs0mVvirWSbjAHOmkXVjkeaX5bqP1u1Z +Q9MDs4Hbw5m0AySVvVD+faQdrt2u93RsVaikvWh2i61nG6ox81VkHK3XmB8zgUcz +ajlj/05F/JVoWhP7TKYSNKoCUq6hcn6u4f4962IVIDcJ9jrkDi/qSoYCvHTIoLF5 +qQmMEb719rCbvLYvBlBD8MHpFIp5QcwdBm7Y3RRBwKXCZqOzTMGbghp0sCnM1sqU +0IASGbN+D3t0np9f50I+YlsazTGOXYm/uxsvRn72vR93GxxX4V/E1qycLa3i2LfA +chhRwXnPMPlZvbiUDWP5ufgX6391uWFKUFUKbwIDAQABAoIBACjQTF3iiNm2+taY +rta/6bA58CzuK9q9pu/dxP0RwkyZ893jWw1/XSBusNkm6ayBbSbRfyN8M5O/Jhh7 +JFQmW3aZJpmMfOa0O34AfSTB5ZjSMxRB7qQjWIZPqByYd3tQewyKO0OfKAMArMLj +HTbukGy2RKvVHUWnzhZvAqd8mY+vVfQ4k04s6IuGgu3WtfuKn2/L6BIRYneMYkaz +cDCXQw0Q4CFMFq5w4+kJinLsla47gX6ck65QQJUyrTlcxEi0hX1T4lGnDrsOADXn +QSus7K3ChgJYX/2UZI5U2ixLWl6AulVpq6G1CInTddVOSw1Fk5oeWYvX76z2gqWN +CQMBL8ECgYEA8q71tx38PS5OyT89iUmaJeJ+PbysvxbHWdEBkJmBCp7XOkOBV7hX +tdyXu6NB+rRYezIMFo72+lSR/aeMSc53zjnLJeEumcsF4tEkTtR6/8v0S5DqbHoN +Rr5NLHD3KT2038L52GERwrGJiLKt0o3lHaMhlX7YU9SvcGiKg/Efx0ECgYEA4kKa +ymvDImT2FSnM4z/jjVhDd5fFDRFr1ubOCmXRGpzLPw5IvgP16DFTtjVvFNAeeG37 +ptUtb3qWNRi5tUnPZTdQ+01XkJO2Z/sjPtqdWGNZw60Tr6iDWPjBUBU0uNRW5FnY +SN70DsMphqOnelhONGmAMwUyOx14YfBZ/c7jla8CgYBsvve+OYKJC9IkHeF+PmwD +o+17zqr96cAkwCJaYF7RmoHJSuyPmTw8cv/PBgYPyHaJpdCC30dOv0y4BDc573oC +ACXG8JPIOJCHpb4GUPJY41Sx89sIBt0iIS5Fxs0KPutpBV6dlkm2G4YMqjx3yDut +vybSbYeUcQ64aTAAlbDuwQKBgCovVuLkwqYYAtWhTIUWNVv2F+YlUuUD2YJg2AuS +PjFjCHYCOMfMvz2CbOQtBD/yKaw0fVX2qxj81yKhMIa/cz6g2KVyHyBimgC7DzA5 +3FpNc8AK9f+rQIoHHyvKOGE5ndXHdo7nmsNh2c0zmBc0fahB/1gD4QCqlvZTtMOg +nkgbAoGBAMZJAsDOj5pH+Nxv2wrZ29hQfgUyeSmZxJvIvkgY/AzZl4GaFyEvcp2d +ytMAREFv+zU6pPLQcg7XgQ2wUeOQo1yu5mjmkikd2SajvcZnnxx5ABIx1K354Q9M +OPoLY6Hn6f+2TVaupSOYvADRiT0ljYytQh+WF8ocLQIUB1gQNFG0 +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c3.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c3.crt index e6347d23ae61..a76eee10ff29 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c3.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c3.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVANfjBjDTVL4+UG3xEGlDqhK/fhH4MA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW42LmMzMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjYuYzMwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBANBW+tmJaaum3CPc+7wj+qrDkbvuDue24I2Yodp3HSsChx94KmFDYhGYuRWw -sDF3Cu8iPyUi1xk4fCQallLCzkwny9j6dljAHtAYkVfNP07WBdPJ79IWUn4qbldq -LTam9ZiaQZ0jPtHa6PUmj8doc0QWM1ySfJcHqCP53GkqpH+vAgMBAAGjfzB9MB0G -A1UdDgQWBBRiy14zafEFR9kFLpd7PeVFrLdKUjAfBgNVHSMEGDAWgBRiy14zafEF -R9kFLpd7PeVFrLdKUjAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNi5jbHVzdGVy -My5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAZueI -4pUaJEsWQSdPFg4L64T/ec0ETzolzIDb/uxwCBT0RGHRP/94x6o5EGUdA59V2PMT -5jWOAfa5TgtZYuuAXRyB93Z8wSdTpeHRyxNiVDCRy6Jou28VDIRpFlaJABe7ZfUF -+TEJ8FiDyjABXkUw6TkIIuGkKaGO4SNCX89P+i4= +MIIDTjCCAjagAwIBAgIVAJJJhjqi04Rj8I1hF2Y25dKTOJIYMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW42LmMzMB4XDTIyMTEyNjIxMjI1OVoXDTQyMTEyMTIx +MjI1OVowEDEOMAwGA1UEAxMFbjYuYzMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCuyLsP+wqu28kvKImCzxCPChhZRmClv9Q6p2yvDiJ7qD1rZ2JL2W14 +OLuMMCXFEXBi8mZp/OJFj6iz77YV206lAxuz7k3xpVO2C2qY+nl4BojHrqbcnED6 +Nt3JpikOYVKoClG/bB80bGBhd84pEwMygc42LlWG6sxM4A/L1CaQxG5BBF1X7kFM +75GQkHvkwOM5vRsgTudjAlk+TAF1UxeKkdRc3oMlbFZRGVUfL17DdvPn2R0eAmer +bhdJD3ozyZfwR8ZmVjrjzVyFN2IFwKeosaSOgLpG9delea5KOv9kNe2X7aEDqmH0 +BWVGsVonNEMeHcvc9nI+YH0oGL24Ury3AgMBAAGjgZ4wgZswHQYDVR0OBBYEFPc6 +vuQtIXXwdTvCsxr5r+9T51DcMB8GA1UdIwQYMBaAFPc6vuQtIXXwdTvCsxr5r+9T +51DcME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGU2LmNsdXN0ZXIzLmVsYXN0aWNz +ZWFyY2iCHG5vZGU2LmNsdXN0ZXIzLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAnCPSWqjvHEEBipVBCAlPc/KD3Ja3dWt4o0orKgls +9Y69rQzEABHltW26s9+bf5GsnyI6Dfi8uGRaWi8phGbXbcDcsEnMnjlhSCba0212 +xh9UQPWm8GQ9+dE21bUh20b+V+68JZeJcLSzr+mu3FCE3BJailIygoc4UHIfHGuX +7l4xWj4CrNAdEMAnNwyu056f3xbD+o+sqpUCAcDAqf1SUkSQ/AIGG/KVqv99lXDh +XCV32ppcF8WKBbo8Dls71JUNxxxBc3d3fjLN3QuEhFr8J14X1zW10R/QEDLUerKx +uH86es0jQDT32v494jt750kF7pb6jTVK9tRukLzFlk9vzw== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c3.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c3.key new file mode 100644 index 000000000000..d8df3a4890e4 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c3.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEArsi7D/sKrtvJLyiJgs8QjwoYWUZgpb/UOqdsrw4ie6g9a2di +S9lteDi7jDAlxRFwYvJmafziRY+os++2FdtOpQMbs+5N8aVTtgtqmPp5eAaIx66m +3JxA+jbdyaYpDmFSqApRv2wfNGxgYXfOKRMDMoHONi5VhurMTOAPy9QmkMRuQQRd +V+5BTO+RkJB75MDjOb0bIE7nYwJZPkwBdVMXipHUXN6DJWxWURlVHy9ew3bz59kd +HgJnq24XSQ96M8mX8EfGZlY6481chTdiBcCnqLGkjoC6RvXXpXmuSjr/ZDXtl+2h +A6ph9AVlRrFaJzRDHh3L3PZyPmB9KBi9uFK8twIDAQABAoIBAA4MVTiyGUg3oyJ2 +tCK+6NpOWemnBfKcif7S4SHq1Wux+InO6/AjwDA3Ax//6LE/txRE3mVrbXZg1xLr +U+vKzjXmG45pO3uOKb3Kih1UmHnEsK64A3Jc3r4dtdLU15zvxph6B9sGYjWw9kgv +PHNBs3KWS1BAhiBLc/ND2REtcpIVDWP7LtInJmaMKms8vEIqGL/rKa9on8Yd/8lO +/K1GQp0vv7mak1grlb/uwvAmwK7gS5RYZfsSUL2rXOF3UKE8XYGt2AYKyu7Pf8XB +fR62ssBvTMsHRKoOeV6ajXRc2QS0rjrOfN20Yg2NCmwspU5z9tw93zsraiJZouIj +twaLtLUCgYEA8ljsOn15TMUsjuzNv5I5lWsTbihQFyKRmVlnn9UUVZefuHfNaoFd +TbuQX/TblAtRSSG3v2Rq43JH8N2UMEpxyiGdmba7juGsd2sc9W4CysuGoJpbaoFC +2W5QtAgCjNF/hE+SkvHNrgNed8k9TM64hLE0j+ZwdfKRE3POz8+XPs0CgYEAuKFt +K9ow43AXI2zPnWS2QXAXdTJlvTzIO+4virRYIdbWXNsQnN6HILaO3mzBDXDT7Jh/ +AJWvunYjCYkX/pRE4wTcZIhkmxD0bAb3afuZV3NjSHaunwwW5Ms/bYqxTAfgkybu +xBdj8aQN1ts5OQ5z2v3+qwk6LeElAqJ/FvBmYZMCgYEAqp6oUdWc3MvEvytA/6q3 +F9c26ss+98rCrzAeoIynsEr7jCfsWxuQ0PVIhAYjwWrmY5J1hy3vgwomVBHsDsOa +sTBRgLMCAWoovjqPPMui85uuwMtrqOQwaFwePWGWSgYEvOY4z3Ol1ulQTQ1FNxoj +FmicFOOsMZ4BPay+4dfJJ9ECgYEAryL5oypzS5utZz41ZvD87e/9YH+xAKRisG7N +jD5sh5T6fNSU2qk/9ykVNypkqnJHpVw2xQA41KBxtf7k0uokulCHG5pCDNI1eS/W +jCNNHyZmnHtwQ5C9OsUN4sQ4UI38WroIrdweQz7CnfgQPO5XuflMFURIevLiH7Cy +67iY9Z8CgYBAjrKAQVyS0UawQAg843jSxtgI+XkiaIbd2YTMpXUig+80dBOphSIi +C8IpcIe0T4UtF5LB/pziVKBQ4HPevTXDsQnkI/6cTkzKxDKoqogk9izi7Sxx19eW +tKuoONkdlLjgudlSo3mmXo+iWQJ/0tRz65ckIBCQ7MnhnLuiOVXxlg== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c4.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c4.crt index ba5d4620dd15..2b0dfe3a64f7 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c4.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c4.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAIXNMMJ8xgAMFoITRuJVgQZCsNEAMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW42LmM0MB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjYuYzQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAJDY20sl1QeuOSxilJdb2r8PygM/o6EPlIJk/NHc6Lh1VG2bxqsS091jCp1U -Y5uHdynMKXxVhgbIG+rKHgP67rh+3dTWzvThWDMZ1ljMMpdf4NNH3caM7WdgPzj6 -FpJbuEevBnNp9ENRjKFv8DxUDiJPFsXcCZ/sGuK0HUZSzla7AgMBAAGjfzB9MB0G -A1UdDgQWBBTPFm83nd3zU/X8KaQpSIF8w8njkjAfBgNVHSMEGDAWgBTPFm83nd3z -U/X8KaQpSIF8w8njkjAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNi5jbHVzdGVy -NC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAKAQP -iW5nHwgAXy92DaO4eLdzMuODEMgR+w/qSkMw6kaTyD+ZjeyTjlDbldXF5bomQmnR -w1/90j3MPHoelX2POi8D2BPT350hSXouts7meM55U8fnOHXMLb7CpbNO4TTdYLIO -2+emw36SIIxVBRz+T+7lB7532vM6TuJN5AsW5kA= +MIIDTjCCAjagAwIBAgIVAIe0NImvOxe806rwkTiIZ/RB/LIxMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW42LmM0MB4XDTIyMTEyNjIxMjI1OVoXDTQyMTEyMTIx +MjI1OVowEDEOMAwGA1UEAxMFbjYuYzQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCefHL87xAOATBaSF5UeFUEvr2DNqUSqZ6DMUOObhBhPhhAipEsyvmk +fetlBL8fdKml+Sf8GYsGcnmcNqHDTmqKfbP+s25pNXf+EOzlGpc1zyRCvhsWzai0 +vODEOkFclBwSKadjIsfYR2Tewn7EYrzyDDd+sgr6fEI9STkQojxR70mVIRKRVNWj +GqMrO4xxnwP6X7fpcY1O2z6PaNlh3lPxemHMiWE/lJRFpHjtbK4SILwN8JHQrBs2 +E02PFL8TtiO2HQbOUomw28ShDYozbjbNWTzknmC47+TPqR/KQRZtb0TxEnreBA3J +8kOp9bfdO9OULqtaEdwYEaYsOjzEW7/7AgMBAAGjgZ4wgZswHQYDVR0OBBYEFDjV +0vJkaBa2bYNqN6Eu9tjW1UlnMB8GA1UdIwQYMBaAFDjV0vJkaBa2bYNqN6Eu9tjW +1UlnME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGU2LmNsdXN0ZXI0LmVsYXN0aWNz +ZWFyY2iCHG5vZGU2LmNsdXN0ZXI0LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAnlglJ56A2OELsMWc6wXeAbaH2L9grrXFD6+qqbe8 +epdPchhqk4XNCzia23fG3xnevssq4ZgPRzSTX+4/mfTpJyvEhazMAwGxIgo4RXU2 +lfVV95wXNnkPqIe/f25Z+WSkgoqOsg/2fKsKUtGC0qCvjFmzHEJG5KAfuXLTd7iL +DcxCOJPliYFmzsbKQ2qlCKJMzA48Pj+CyBiuMomoRMZvaO94IkGobfghElg+cnO6 +jSX1QS+Da+BpDW6hd8fXfL17PGdtxkORJd3FW1Vet4Sc0L1R8Lel6VN6RvnppDAG +qXn0Lzz+67liQerOPXqlPVLR79N77PyM1yR4N1MRbhOvgA== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c4.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c4.key new file mode 100644 index 000000000000..03d4a2105dcb --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c4.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAnnxy/O8QDgEwWkheVHhVBL69gzalEqmegzFDjm4QYT4YQIqR +LMr5pH3rZQS/H3Sppfkn/BmLBnJ5nDahw05qin2z/rNuaTV3/hDs5RqXNc8kQr4b +Fs2otLzgxDpBXJQcEimnYyLH2Edk3sJ+xGK88gw3frIK+nxCPUk5EKI8Ue9JlSES +kVTVoxqjKzuMcZ8D+l+36XGNTts+j2jZYd5T8XphzIlhP5SURaR47WyuEiC8DfCR +0KwbNhNNjxS/E7Yjth0GzlKJsNvEoQ2KM242zVk85J5guO/kz6kfykEWbW9E8RJ6 +3gQNyfJDqfW33TvTlC6rWhHcGBGmLDo8xFu/+wIDAQABAoIBAECpMLb+iHPWS1aC +cS4QfEt30NVAjb/cAWawSkpJoJg0H+vxPQLj+09/6nLoKVwZmn+zdHtYJo8mVHSG +G6CWFUTUvYO8zF7TaFX5rNVacHgNP9OVQXMfzGWVLDUTOYieiwdkBD2fup6EnmR+ +QP8587Hjhx7DYFRLLzFW9xfQwUCB9hZeyO2L40ht+2IQ+jCi96Bo2/DBs8YY6fjY +G8jegUE0IvIywOqRjrVmeeiP31yio7Ov4GvXhpjOoJr+HW5HRM3sfNDsYLB5/rzp +nJe57SZKOtvm8qEG4Myl9hhE/F2MurlNqP8Y06gFofqyQxp+cHjt0wa7+TOuZLRm +dVW2FNUCgYEA2RPO7Zv4tFDVFy2kYYUHShKacrXvBgrbUKecEe37cAOHxJDvX4jS +0oPOTG7FaI2tuGkRJ5YbGcP6zGNi5cqL/gqk4egGkqbCZmjvKdqB8T54tLdGt/5d +gKC0KMCtkXZpEldUHAa41izXGIfxUb7uNX1VFu9oENAAkQUVewqFQW0CgYEAuuc1 ++WSCNpUoyuDZnXHr7wKH1/fLTnMLq+JQKPOs3xjogdZ6FJTiMQ7JnUEvLZ2DuQis +a/M12aW1SHiXuLN0ojp5+uc2ySgM3LDMWC/YOD91zVDl6ZM9rPTs1FGxdiEdxry5 +X+ks43U1JyMlBmImgdi6x6q1vkh1Vw3duTLPDgcCgYEAk3P+Gq7Y6HsD1EO/pjJ7 +uhga65Kjs0IKqaXAwPkb04HCZoZg408IjdDUgFy6IYQjneFYssAm1oCmStGz6w2D +vGO0jnNSi6lU4cWjt5VisOFTksXv9qmFEAuZdRk0jVuWkcvaKVNzmKBK17H3/SQp +oRHqi7LByLHRwLBoX/TMgZ0CgYEAp5t+cJasmuwqN71qCWJHFQBs98S0YjaTSeHa +xs62wR6iKwU+V0SojxvWu5ngCAUQQhfAH9JsATE0H7hdPvpXZsAP8Wv1WnD7D9HQ +fmjxeoeWSbf5Zfxvu7GszD4yaWALRtSpXJRLd/L85osCwfW02I6V+UZiUzDBYX8C +zfj9a3MCgYBFwdBbDrqkDZVmCdFtOmYSgwrEAHEJ80s39XLnrJ8xndzoATrV0xlC +5ng0mDeEBPtDxBqgkJ7gLvlfVFeslOTt6zbOoWrjFbDSLQLd+Fmdfi4Zlr283mcH +KdIcY34lChJkc+zFGN1Z7VmYRE4pKWucRXbkI/ulOBHqf68rUfAtpg== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c5.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c5.crt index 026d31155ac9..914d1ee1d7f5 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c5.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c5.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVANAqTNvSXNb+QuG2Dty261T05fD7MA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW42LmM1MB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjYuYzUwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAILKAZL8kVxWDh/9//AAeG5U/NF0iQBSruy/8x+CA2yzV6moe8V8Woou7AH3 -ksjTpg8NyWUejvC2WQP+dqvr3+t1XhAuUdpYfcRVg3zS4mFcg2KGrvnVctXpd7Xr -/L03FIEpaQhUsw+xKrd9+d3VxQMxFyNWAHhB4/rRIqWNjfh5AgMBAAGjfzB9MB0G -A1UdDgQWBBR+wkLQRaVnAEyo6Ign+W/48tJpWTAfBgNVHSMEGDAWgBR+wkLQRaVn -AEyo6Ign+W/48tJpWTAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNi5jbHVzdGVy -NS5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEABjLS -Ws3YyRfF4XhvveU82BiIKS0B0hH5P/ozXiJYUjBZxYc/U1XZR6m8LRQLCfc/kk+S -KpZgizFPsJvaqxoEFj9YgDFraMD/3dqd/R8GiCeOQYf+LRXSbG+eHnHk9ouzqciU -kZXeeU0dnM9aVlBFrjlQLk4E4GQhdTZGCxXuuWI= +MIIDTjCCAjagAwIBAgIVANgI4RhooF1HoE1oK495zN9pe8fkMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW42LmM1MB4XDTIyMTEyNjIxMjI1OVoXDTQyMTEyMTIx +MjI1OVowEDEOMAwGA1UEAxMFbjYuYzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCmu4VjyAtgsJ4COjFr3uGQ+kUTJROqQaHEnXZXexzpPD85W6aVzn16 +rjeXIhWo6L3m2G2CHFgibWozo2MkJiYy/owfTWGmfzSmHirnmibgB9CwaE4SdZnc +9/wQWg3GbpHEOwEKyA6wLmRgrgUJHy25e3rc0zj/Qtbu5vG11KzZYf04kzT1qFxk +yu6Hsw3yUXIEWxJoB7ddcW8czsSC381LfrPQrtfB/QLn9UHWIEPKSIWfbx5uwrMK +js1Qy1Bury4MmX97gDSVVKHuPjXy3cixZGyaejmztDEySq9YAjm+hjZLj+CR4/a2 +/vLbJSiGnnJcq/J+3FRm+WeSukKBc3hJAgMBAAGjgZ4wgZswHQYDVR0OBBYEFAyg +VeLvqzbOQC79xMR45fLR+k+VMB8GA1UdIwQYMBaAFAygVeLvqzbOQC79xMR45fLR ++k+VME4GA1UdEQRHMEWCHG5vZGU2LmNsdXN0ZXI1LmVsYXN0aWNzZWFyY2igJQYD +VQQDoB4MHG5vZGU2LmNsdXN0ZXI1LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAAM/YxLaCsiL1b2CSk5dmaAPxE7TedMFHkI/xvgGU +3IYLO078ACl4XnTHmNoZuAYoqOQ3xAMtoIbw0eMyx68bSDQ4Tjd3GICZn3tg+K4/ +lDSFjecyfX75JRiLbFQJBUdD2pkh6lKy2wgvIosMWdL+JGVKvbBSlwJG8n8/iH9u +46An1iCbYEbX7UY4ft3bA9LNaOpTYwcIgOn4QkLANQYa874CCzTkIYavClKk7Url +DiOCQdFHMp1KCXavoTYSrRZt6otR20gpPRXlmED5j9XKhILnFsPT/HhmkU7S0cpy +V2hS733rAC7kQZQv1YWdLSQZMVBA3lzO5pqPk+7O6wEYGg== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c5.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c5.key new file mode 100644 index 000000000000..49b7e20b94a2 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c5.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEApruFY8gLYLCeAjoxa97hkPpFEyUTqkGhxJ12V3sc6Tw/OVum +lc59eq43lyIVqOi95thtghxYIm1qM6NjJCYmMv6MH01hpn80ph4q55om4AfQsGhO +EnWZ3Pf8EFoNxm6RxDsBCsgOsC5kYK4FCR8tuXt63NM4/0LW7ubxtdSs2WH9OJM0 +9ahcZMruh7MN8lFyBFsSaAe3XXFvHM7Egt/NS36z0K7Xwf0C5/VB1iBDykiFn28e +bsKzCo7NUMtQbq8uDJl/e4A0lVSh7j418t3IsWRsmno5s7QxMkqvWAI5voY2S4/g +keP2tv7y2yUohp5yXKvyftxUZvlnkrpCgXN4SQIDAQABAoIBADIRCj+JwPziXDhA +ADorneFafaZ/kjWkaq6vN+uSAP76g+O15UKD1IMj0HaWGhnz9Z5drBiPaVnBMExc +Sjd/KyZR8IKvylxkQ0uEsI6GaBkKz7xBPfxmCvNjFfJVEGDfL+9UZn41z89QxvyP +FN0F20Wg0iaAWNOx3vezKq7Ws6a8vVGQY/YCWvRCivW1oxRbP3d7HCZ3YuigOeOR +HEuiq+qIR7IFqfk7O5lWUmnaqQ0XOVrs2n0+b4F8aW0H7lPAiGnN/yRuDixDckMF +ubl/KkmS2FJOS/1v3Y7Oa8dF0RrZsQDEzeyy5JvKE/qdw3hRF0nNMD6GQ9cUrS7F +cxKtofcCgYEAxt0Dr3o1cViVHvHTC0q5GWeWt0JbPof3UtVzSmFcanxciEwuA1rh +WHE8L2Ha+/2iVB/k8NrX6cagxXS3bou71A5xO3YEQPUFUaU9u//2xiClhIqQ6YaA +mftEGYEmRMAAR/Izec9S0dOCXAN1+oeymlPKyy3ARcKYvwgi22cJQQcCgYEA1qMw +YjsIjGc0VOE+q083xgyw2SBpTEXz8OXxiA2jAJsIM79973SJX/uf+bq0N8UMdccV +ZUxby0AKld4GOAUI2jTQQfXlwTAx4bNdnseG8FgNmkAFiabCzrGceWTOaQi2IRoi +xXsozkT4Oy0F7Ot/tAQCFXq+OPDs9tVea9SBOC8CgYAf+3OYSSNHM2PoMiQq8L2M +JGjM2FMsX1mbbNC56aafSZjoIrhTuc38F1oClM5PMJ9mwCplwgxvgI5isz68/KQc +mqYDVhuhBsbwNo8TKTta6e00v8RGimcsXIi7QVYSQH7LE1zL/lfnfwi8G6JhxGkp +AMjgnF8L2RwPvRM+DfgjCQKBgByTKzzYqSGwF7p5JlO+PymhxOWTOBGMepaGtmor +jpjlCumAw+5eMG/T2w9vOND37dm8QG/1A3+CriYXofJRUUVjpJozd3wJw4pNy9f4 +rlFPF7iCb3pM9kB4SuGqdwRqwXyUsy5hYCRd8EJIkzJgg+beZbygU7TW9mVCMMjg +SLfHAoGBALK8J/1XrtF8jZO9VQ9QSle4bQoH3IdNqdIB+3Nn/R3m4qqO9bcdPSmK +8rKFPqyFX3hD1KLBn2nCO6t2I50BVjv0Xkmff8JLaff6GInq2rUhHk1TACugaJyh +/Ag4UPyP3fIM8b2DdPcPFYyL0PqGhjcwNmTP9OnrCC8vLO2+KvPt +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c6.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c6.crt index ec90f2789849..0f128dbfffc0 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c6.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c6.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAJBP/I3D0hBi9f12S8zW653VRl2iMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW42LmM2MB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjYuYzYwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBALAFA8o7mX4GYsexgC1JaQzogoJzVK0XGeeWUMKIU3kjhfjezxwY8Pqk1bCo -hm+WTdhRUxtO4Ku2AMz7FaOYnhf1gAALT87M8iEQD703Fd83jcmrAuWG0zibkzos -0xxzOrg+yMU7Tj3YGBqDKuOiXR9vNR5vpscU81bg4ZTT561DAgMBAAGjfzB9MB0G -A1UdDgQWBBSQZ4R1AxlxN/+tAONyY27r6AKU7zAfBgNVHSMEGDAWgBSQZ4R1Axlx -N/+tAONyY27r6AKU7zAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNi5jbHVzdGVy -Ni5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAXwKj -9JQDAud4CMyHcSz7SWNvode5Pr+1VO8OUAP2UARgtiEzV+ACIgXsWMKY04Y/uP0+ -0bWJv9MRUfw5jIK1Q1Rh6fjrfspL6uU9PidXDxqlfno7YybPCj9IUeJDImQC6LrT -0OQbhodDwWhFXP4ibbk/tCbiWX+fS7mBThIWZT4= +MIIDTTCCAjWgAwIBAgIUZRawUqAmCNbgJ4EEmOCAJ8ukyp4wDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjYuYzYwHhcNMjIxMTI2MjEyMjU5WhcNNDIxMTIxMjEy +MjU5WjAQMQ4wDAYDVQQDEwVuNi5jNjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAIkxQCz3mjLK5bUEw2CALimAYLDLZr4IwPAh4tDxBH8aDaAdhQa9XbMV +HbZL3SZ7cTMxlwLyZ/3XIpIKJ87OS0l+s3XpzSrFwYg410RPAoBU7ljof9nF0Rzg +UAEg2JTfg7KJ+7T7yOp2uyqJnDZ9dJvb4SgMuPeZIu6ODQgoVc0n3xSKj3BCSgdi +OevcuaZwEIEF+tIzH11JHgxrI8shGh+a59CtkJ5FPXp7/Vx5jkZi/iXOJygOAf4Q +/p08x1Kn0ulvH7wosYilj8yQ/FdApe0SNxFsPCFxFPzO/G6Ozdz4lC1bWG+FUsW3 +jdB5mhTbaA/ADeApY7ivi1M8hgg9TmsCAwEAAaOBnjCBmzAdBgNVHQ4EFgQUVQxa +VrpLCplK6OTZFaa+rQxPpUEwHwYDVR0jBBgwFoAUVQxaVrpLCplK6OTZFaa+rQxP +pUEwTgYDVR0RBEcwRYIcbm9kZTYuY2x1c3RlcjYuZWxhc3RpY3NlYXJjaKAlBgNV +BAOgHgwcbm9kZTYuY2x1c3RlcjYuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQBmOf2DFuSZYXre2Htmj4+HRSXjjNjCKjaO3N1cP4RB +u8BsKlYPVUTjgaDEOwobuk5S39uEpj+hZ0sl+K8w1tp/e/fPLcSxl7eNod5FQhoc +hf9i9IRDsv89R8/KmQ5GHKLwLeRNOmhabEKp3INtJ8x2H8GLjzmOUcLdI1v9GuEK +Nun7AO380aJMY7Tt+QQed3zwgIHX5npeYWd0OC1wsjwRXCa7/E9sNGPiGjglKw0y +yCBfUmnR/1xzO4P/GW9kYJ0aOoMT3TqkBRQuCHczxIlkovhInrXzXo4VpjEnf/tv +vM2UY6wq36Oz9SLRjN/5TM3TAQ8TVszSwdnoceK5+D9b -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c6.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c6.key new file mode 100644 index 000000000000..27da0af46900 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c6.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAiTFALPeaMsrltQTDYIAuKYBgsMtmvgjA8CHi0PEEfxoNoB2F +Br1dsxUdtkvdJntxMzGXAvJn/dcikgonzs5LSX6zdenNKsXBiDjXRE8CgFTuWOh/ +2cXRHOBQASDYlN+Dson7tPvI6na7KomcNn10m9vhKAy495ki7o4NCChVzSffFIqP +cEJKB2I569y5pnAQgQX60jMfXUkeDGsjyyEaH5rn0K2QnkU9env9XHmORmL+Jc4n +KA4B/hD+nTzHUqfS6W8fvCixiKWPzJD8V0Cl7RI3EWw8IXEU/M78bo7N3PiULVtY +b4VSxbeN0HmaFNtoD8AN4CljuK+LUzyGCD1OawIDAQABAoIBAACMxKlnmImYOhCW +tETx1hultgyPWzRyqvIgJ7P1c9CpTrBhGiU/F/DI9Z33/gzWWPEg2gUCkCFCH+26 +zVZ0p92YSAc2ZsZSfuKkgcxfu6M3RsDvHs4urR36e5iMnuN93fg8UFiiU/PbcxaK +AANQ9P/XCr6arFUeMZI1Q5s5rPw+5bijYPQhIfCUti38EZGyet9L8v9SHe1E/Fp2 +WzDjAWqbIu2onepPdUJII1jxvR/0NB0abeC0SbWvwTfvwqcg/YfwXd346bX9wYGX +qPNuy4xhjKqXCSk1Uoip5b1Y4XbiqPlzH/yqkqYm7vloUwOzEyujUe+PtKTjtPO0 +g2dYfjECgYEAtT0YjD+BpB4odNBr37LhFAhRq7QY0G2Xs096TCATQmRLu4SPUcwd +YA6aMu8kiW4xChH/VrpdF604ckXWfHGMqr2U2QzsYx0a/XQng/Cu9ZzZ8IIAexpA +PZtVEF5IhF4YA67AX0gHlr3MHhOAKyq2v1bmi0h/sJNs/lzVu0ZVIfsCgYEAwcjY +Bl09yTBztFG/MhBnegJSvHdlI3o58IHU44cW8vf38WuMIRTAYH+WtTimNu8JJ9x5 +Cm3uT9czcu4xop0eTCgN1NgJ2cr315Ls99ZZWm+KWjPe/106b+qk5eRFubbwEKju +eflXPxgBf9vtabtctavnFRiZl0b4lFuQDGSBSlECgYEAhValsMkoP+5QJWERfErG +fU6Bh6nAd9epPCrE9yzB/p57eUJVjwB0vC8CE2cNom6Hdbk3+j6yLrEqTihDXNrN +evBviiCP6XuIV+AVEmUI03rKfL+oSulNSgX1100GXRw83ri6n18G9kg83APMz/Vk +7fCpPN/h0a8lCR6TwGxGRfECgYEAuioo3nRVFyLlBSZDd+Uqg5wXv9cz4pFZeCRC +URNGiMSP/e5EAMPezNXGAvMZO00IjrLP1aUmfK1RT+hk5sPqh18zanbhf8ntr7wG +y1GxJaBiWaXbZZg6u6nAINrb4T5OArNH5wYTOxnk1uCBhEHrHty2cx3e7+XOiTgt +erpTOwECgYEAjQWwnt3t2Vio5MEix9OVzdWHDFQw+Dnx1AcSB9dA1lYW02ej1UFc +j8sJDfOJ0J2vrW+QXz//bdafI/BpnuyJfaQdyHIzerKIQzCtNiIR0bTjrR0xYgtt +l8BBOauhN2+nRxSLJf8IHXpc9ncExvOy8WAl6DOT4S2+OYa+ahd2PEs= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c7.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c7.crt index ba90a4453daa..c8324e452b94 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c7.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c7.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVALhovlP3SbGFE87OOAIBX/CaSQaRMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW42LmM3MB4XDTE4MDQxOTEzMjIxNVoXDTQ1MDkwNDEz -MjIxNVowEDEOMAwGA1UEAxMFbjYuYzcwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAJy8A2khQkFtzq1bi9E8sSKSrJ/v/sWAVASrv8qwuiyRh0F0sxp6KgtJ4ogp -iI5WbhbIoqWqjoDwGgGvcvf24m+/sr4JDdKb/ZyzkKGOLhLXugQwZABOCPf54DJI -As/JqoxQ/XhbMPhqLTz6x7qPLj3Tdwybsbj85nZyedkx3gyBAgMBAAGjfzB9MB0G -A1UdDgQWBBRq79jvLNS4zDUaRG5JNpPgOqXlWTAfBgNVHSMEGDAWgBRq79jvLNS4 -zDUaRG5JNpPgOqXlWTAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNi5jbHVzdGVy -Ny5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEATtla -YQx5JvRrZoMhpXYTb40/mFNgnCdT4YktqAL89IrjnU4Uc0xZLNVKt+4wCAx4Uosw -wT/aymJY/FtwEpTT40l9jYxMSYVFLTiheDSgBzWUHYKZQ6by72ibUzWf7gXALRMV -pu55YQZJF2ImsjH6gJRniJ07ma2nDHszMi7NWJE= +MIIDTjCCAjagAwIBAgIVAMysDPIJK/5wTrXLAgzKRLkztMGcMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW42LmM3MB4XDTIyMTEyNjIxMjI1OVoXDTQyMTEyMTIx +MjI1OVowEDEOMAwGA1UEAxMFbjYuYzcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDUhxxCMek8pcb96Lw3XNtvSXRRck3jYcnCaHPOzwxyCv/0/aKHgEqu +OKs1mXmLuWu8GniBqqEZVst1jnHSfCapQ7ROMxb5Q7cHQmmm9eTukhllyd62zsst +9OSZJJd8Wzcq9rDpaGkZ5fe/mJl1lYizCoqUVwkuMSyz47UBtIyD8TM6j5XtdeKG +3ey6IDtniho0A94PK2pDIMlKbbhByAtGO539Du/LnKAa/0XnTqIZb0TWFsQezWzz +AXv611u9RmFFsb1DrXqjVQVMBP3Q2zle3+cjGZcBIOaIFQysZfbHUhE/hv0+IYMf +p1WNiFnPZgkZelD4N11WtmsD9orELjx5AgMBAAGjgZ4wgZswHQYDVR0OBBYEFI3n +ewtwdui3X/peOJ/2Xo2OpGqsMB8GA1UdIwQYMBaAFI3newtwdui3X/peOJ/2Xo2O +pGqsME4GA1UdEQRHMEWCHG5vZGU2LmNsdXN0ZXI3LmVsYXN0aWNzZWFyY2igJQYD +VQQDoB4MHG5vZGU2LmNsdXN0ZXI3LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAVhqMmErj1b2afa1o1eaJXEyIV4LTH277Nn7KnrHx +aU+KzhLwQT5oywjP3lPUv5u1vRJ51+MEzruA7GK0WWei0lbuD2lIU5xUeW0ykIYF +s00tcEVKqwCBkj16NY71glIn2tYnp29NJ8bknKbpj7MdYGx2IM8OS04GW9NsX4SF +pGDOQsfcbapjzpZZXMAVc5BimVrBH+SyqVLhFheY7Ij4BxxQ+Agqw70IAs93S7YH +4MY5cU7n8fT+n5CMNZSlLVzZWG2vwYSdSuOWvZgKf1OKIrIrYqp8HZFQxpepcpJ0 +TfkqOOzGtVuEM9BzDwNcvAi35GbvAtmsPTjDOPkXok+fuQ== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c7.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c7.key new file mode 100644 index 000000000000..5ac9601f01fe --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c7.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA1IccQjHpPKXG/ei8N1zbb0l0UXJN42HJwmhzzs8Mcgr/9P2i +h4BKrjirNZl5i7lrvBp4gaqhGVbLdY5x0nwmqUO0TjMW+UO3B0JppvXk7pIZZcne +ts7LLfTkmSSXfFs3Kvaw6WhpGeX3v5iZdZWIswqKlFcJLjEss+O1AbSMg/EzOo+V +7XXiht3suiA7Z4oaNAPeDytqQyDJSm24QcgLRjud/Q7vy5ygGv9F506iGW9E1hbE +Hs1s8wF7+tdbvUZhRbG9Q616o1UFTAT90Ns5Xt/nIxmXASDmiBUMrGX2x1IRP4b9 +PiGDH6dVjYhZz2YJGXpQ+DddVrZrA/aKxC48eQIDAQABAoIBAA4TXrtBm/1wy1Og +uOp/w2bE+ie0xtTS8q5mBZmCXAQnl1kt/Tg1Nn0ActJrdkHbR3ffVvDbkKzhczs3 +cnFq2JKoUcZGGvTqV7Jq4x10NrPYSEdWDeKUop9KE2PTKRdK4UX+ravfxuo4sWZm +oG9JtpKJFbGKbCi7iAboA2J4B0Brt53ERtUsR6A8Qu852qA+vfrOjdH8fZuaHZT6 +zf4DOz5Bfzi1NZPj/fGnZH2+qUte928ytBlmlYY2OdOSfquO+A1LMMuT20vpLqHY +x+por6ygDDnTHLIyxK3+bIo8iEt0noKR75mQG4D6zyYkL3gvSryUbtaZz9BoWuMr +e8AL4+kCgYEA1J7mlqI6MIIwGS7MyW1B1KGfevQRY4LhCG2QN+ipy0p71AbHW2ga +hXVTTnBBROZM5aSpQffYwotbymHLS/bI3rqnaSkNomxOi6Lsvq98FKu5myWmcA1D +BdOuzhjSWMGdLIcjqVrHqwTqhpY5IHE/2xPd/61Ug9a+erbsL/WVgT0CgYEA/+Nb +G+5sKKeHGKs3f2tLafWQYNUxWN/RvvhEkcmMldQhXyNnJXFOJyvyrKRAO0FVOTrq +Uht5Ml1IF1ljsNC+llCyB/ms6nuXD4aCKHiYnSWWAzeN1s+RJxxQoGJiMNKbJqXm +sblvEqLnB7UGwupT5LfWF7oGH9HhglUZR/2nY+0CgYEAmFhaxGfs5cECkai010bP +unkr+j9TDkhC0XUfts+giacO3rcUtOv8xM9yJ2vLeIDDbG/1Tu01hP+xf2te7Ukg +WGbJeVIPEOglx2uv0lmOwIji7sWlYfg+o7gvMkEaxrnD0pnp9Xi0RFe7az/MIvDC +Xkj38jDRs710shzLUvnWa8kCgYEA55Yr0gahatXb5c6nprFDin1NkuXACJBgcvac +kFkZ3Ewle8Y9xtxaAAe9VNQ+saI6cFZK1c65jIRyB3fGSrNRmhckWSV1gq4iFFIH +IHusm6T1tKmlvEEVPJFawxzngwpIktzZAgYyaWp3rzj+CpArJzfeEpEwk8HO17cO +72G5Ci0CgYApNFK2yRMhF4wc9A0JhP/gbPtZw6s7Pt9svhYowEeoBdk4oC2cy8zh +CeqcK8X6jc2PuMW5taZc9H5WfXv7tW3/5Yi7GqfU7zGe5bWqRzQyF1Ud+jxqcwoW +RkFGVRA3rUqlhZHkKSPDeo2TObS4Txo3AVC4zIMwA9HalV5a2GM1hw== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c8.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c8.crt index 74dd306fe341..2dbe23b5ac84 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c8.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c8.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAKF/pqC6jeqYbOiIgDBmcXZgUzERMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW42LmM4MB4XDTE4MDQxOTEzMjIxNVoXDTQ1MDkwNDEz -MjIxNVowEDEOMAwGA1UEAxMFbjYuYzgwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAIMcNI3+aYOuGJx1YVq3VntERfFqmnAsssk9FiJMCzbBG2yD+hPlrlGcCUpG -fTBzGEpvdd7ftSoy+2SJv2tercjCkg1enCS/hhnUp96NmUPm6yq6XOV6ICtef0hU -VcuNnXwJitsPFpVJbNmECNE7EI3au1JOKXBBlfRet7IlXax5AgMBAAGjfzB9MB0G -A1UdDgQWBBRzmA0meyNb9xdcsfCBrjVMIEyM7TAfBgNVHSMEGDAWgBRzmA0meyNb -9xdcsfCBrjVMIEyM7TAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNi5jbHVzdGVy -OC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAeNbr -Fh4rvhqkDxGYUdKzTBi9BcqpAPtl5YiAE7++vH+/kcCtmNppimggFzAxIUPhAdDZ -QkpmuHX55AGsZf7PdHi/N3BjyB9d3EVEWItV38xGSsJylNK6VF4HDSWtvSDZlgSj -sQ+m2kbZOICzFodFo2tZ+CfLJWHm4BekVeYC7FQ= +MIIDTTCCAjWgAwIBAgIUelGjzYJW3Fp+xMI740rHUhd848gwDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjYuYzgwHhcNMjIxMTI2MjEyMjU5WhcNNDIxMTIxMjEy +MjU5WjAQMQ4wDAYDVQQDEwVuNi5jODCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAMHpSRtMeXjvtTMtEQbHwmnae96EH1TToIw7ZLy4DJiSySirw/9HJbex +w9/JWizH81HtlqZkzeIdFcwVUpzeHOr15VNrM3Q5KhXBeULzLTbTtGuk83X0wCyb +oIR3v742dFr71oNf0B5dWDChWx/FTQnPoyb3lmLlYBdgvEEpb6mKwANyvik/Lp1A +PTrDGQ04OpjYONSRKdDghlBXfkghaN0ba/sYZTlvQFC+w0oNbI9BjHeEOH2g3Vvm +J+fmp0TwSPvFrbgxt5J75FWPcBwWIeqGhc9dE0KqF7lq5bHMomvnrQ2fFQNLLXLV +rl1rEeeu77hwDzjcCeaHqpKucG86OOkCAwEAAaOBnjCBmzAdBgNVHQ4EFgQUPmdf +teaBulwCla6KMWbLLeIPGCQwHwYDVR0jBBgwFoAUPmdfteaBulwCla6KMWbLLeIP +GCQwTgYDVR0RBEcwRYIcbm9kZTYuY2x1c3RlcjguZWxhc3RpY3NlYXJjaKAlBgNV +BAOgHgwcbm9kZTYuY2x1c3RlcjguZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQBa8i9wzlqtfEjU+kPVO4gEsFz0WGKdRZHeLSyaQeRI +fXx2KDwpzZJpvHZbh5Dbdkml60wb1uax3MViiHoTipo4q3StlVZ1l1pMflODkdb1 +VApjK2gi8OjJpm5f4f9iMJuwhXojU5r56S1M5JmMMIxTgXreqijL+AymSQX3M9ds +KlaEMwEf9PGlxDTSSZKmgn42IQVlTpivTPS6ZV+XTcXny7c1hl4wK5MZcYbRIWTz +QKeUpCtKzN26ZUgazqhq9SzciXW4ZvWXAff15nzr2KmN9zHpH0vxL9TpYhyA1+fA ++/3jT41scb9XK8hU5U5OUfb61aGuOYvEOu7cIpnUlfFZ -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c8.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c8.key new file mode 100644 index 000000000000..f06d30472dec --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n6.c8.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAwelJG0x5eO+1My0RBsfCadp73oQfVNOgjDtkvLgMmJLJKKvD +/0clt7HD38laLMfzUe2WpmTN4h0VzBVSnN4c6vXlU2szdDkqFcF5QvMtNtO0a6Tz +dfTALJughHe/vjZ0WvvWg1/QHl1YMKFbH8VNCc+jJveWYuVgF2C8QSlvqYrAA3K+ +KT8unUA9OsMZDTg6mNg41JEp0OCGUFd+SCFo3Rtr+xhlOW9AUL7DSg1sj0GMd4Q4 +faDdW+Yn5+anRPBI+8WtuDG3knvkVY9wHBYh6oaFz10TQqoXuWrlscyia+etDZ8V +A0stctWuXWsR567vuHAPONwJ5oeqkq5wbzo46QIDAQABAoIBAAW7wdVcjL/qaINn +mvgGGoW6N8zjv7BopHfY/rjEwVyWhHufHoezARa+GxJIGBI4ZEWhHoHze5Hqtb0H +yV5Jg/XfiOUPR6FxWkDz7EOyQH09Q7mmEivD7ZW0EUoxZzo2znhtN1HBmLv5GxUv +BCx7jzRRzxtlGS155j4ytkY/9SUAX89H5A37OJpB/mIOp6a1/EHi59QkyLs+GmIC +bu90htiF2Gks3KixUHwAZ+WYFtAFQcMvn/7FiwasN8YF23ezcCYCOTvVVZ7It1ET +/AoKXd7nSdzEoEh+kPRGaF483BpHYjVMqvr9r58IFdJPZt7awIjKpRafzhvuCN7+ +hiEPHo0CgYEA5zX7AcJYP66wItyShVQ+ST7zkBTjVWneo7OEayqakmLKZ8yjKuyp +16sj1xgMe5jYzxEz55BavoCoSNZaOIJ3oeFxsyi2cBmmRlm5tAIIx8beqWwi2lxP +x0Znx0j6E87XBtlTRS+fTphw9rpIohNMZUTgFerBGPmNg7nH8GUzTLUCgYEA1rOM +Glm4c6yX4QZMGEgaBGRoWK6wDx9kOduRp0stKPmlrQSTThwGe8te5CLx3Bt9iwod +TATzLlFqqhfGCOTETode0Byq0AbxSAGKvuflIr+orzsZlXYcS2JJBSgoFnB6o0YM +bVlyH21XOnEiNvzT+UEryGNbNz6JjXYgpWp4D+UCgYAN4QGmQ2JEwFm2XgM1ma77 +epnoRji3Qdmpffcu76nDq8Z2oKQSyY4Qcid+m+Tz7mLlWyUAe3vW/6YaaTOVPM+t +mfjXRlRdOkzu4CfzlEOuqlNnXCRjczMexS0pm0nXsozZzmXfO1KWxzPQu7HseLgX +RCEnb6YTt7c09wv3dDu2GQKBgQCwJ9J80D5ZkjNu6mbGyWmnTuaaVIGZds0Z93/y +/ehIpUP8yDNANBhRu9PYZJ+CU3/W826EL8+CokRc2HKgW2qBb+on4E15ryiKRtGa +vtMuhogVde0Qxz6Bz1tQAtmZR6InklKh6XCMPn8CMU68RQZ6jLEqkfKGSiY7zb8N +WWS2zQKBgQCuPcci8Lnt+HzU2dUtVPKE2/P8kRV0r3Eg71MvN2szQ6NbLLsGzQEW +bhuz6b4FhUdTr7AQIMEDEvH5pMg2+JY47sj6GycBODyFV62hAYogK11l4u3EJJoq +ZRRqbfFa4n9CjHwqeRwwg9cV8UHBFHjf0obmUUp4Ljq7hwcyvZcK5Q== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c1.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c1.crt index 3f67ed8fecc8..a31de7858b98 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c1.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c1.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUHFRaZgHge/5D2Djq/MM3MhXtgSMwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjcuYzEwHhcNMTgwNDE5MTMyMjEzWhcNNDUwOTA0MTMy -MjEzWjAQMQ4wDAYDVQQDEwVuNy5jMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAtut2Om5X3xl8drxF/Jf/qq+xPYYGs+soYtJM5qAwkC0NGQwPCIDw0pL8hpuJ -XT4WrtGmADZ0ERvPULGTMcBVzmDgt5KANUT8kt3NqzdDS9PXj3VAYKKuDZ6SdqqW -7dEKhRmZ4NPERtHA12dYvBgHmMoiWOI+jvas2PvmJ0/dId8CAwEAAaN/MH0wHQYD -VR0OBBYEFMcRlGJupnRk/6cjwiEb8f08SMxPMB8GA1UdIwQYMBaAFMcRlGJupnRk -/6cjwiEb8f08SMxPMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU3LmNsdXN0ZXIx -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQCWG5eE -eptDX5VWR8tbmvciCAt0pEXCO0y9UIflCbnCl5LaaWCn3Dg5LzrPTQxZryksuVSV -w8RAHUqG17Td3UhB5mgNdOD8MZMoSanO3yFDX+6wZ2pfPSYnLUVu8UdlgTI7p/RL -r7+PWUgXplJVWpME5FZzVBuC7Rz+n5eaDQzVlA== +MIIDTTCCAjWgAwIBAgIUIZKOqIZkq+GTCtbKwpkI8f1YdUwwDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjcuYzEwHhcNMjIxMTI2MjEyMjU5WhcNNDIxMTIxMjEy +MjU5WjAQMQ4wDAYDVQQDEwVuNy5jMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAJ3XPmT1asE8rLqrdksISEXya3I33ZHSMnvPDJwgCYwwbnksGYqWGMQQ +srh73viNWg6E9uuEbmd4LfOF2mya38pIlM8jMkk0xw+mxVQW9GF1WI0wQ6gjmCgq +MOt0BO6qq/driQiGPLxKyCOeJrpumrJEIkeilEzIj1p+1PACAxqTsmmSQcPHjZJJ +kObqfAEhxkCJG8gxuw8nreKp/9LXMk93W0xhUJYiyLWWZiKplIr9HC50F5wVECE9 +1g372Ci0xzmNmIf3SUOz14qOEx+9LjlvT1waQO3uO+kNVjYFK09+zwi6m9oNBMum +XTDo3RrPIsVtN1v4pro2jVbbbOWkuvECAwEAAaOBnjCBmzAdBgNVHQ4EFgQUhR/3 +omrrcRjaDwKtG4jwzC/JB90wHwYDVR0jBBgwFoAUhR/3omrrcRjaDwKtG4jwzC/J +B90wTgYDVR0RBEcwRYIcbm9kZTcuY2x1c3RlcjEuZWxhc3RpY3NlYXJjaKAlBgNV +BAOgHgwcbm9kZTcuY2x1c3RlcjEuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQA8b1/phwUIjOk1tsA4YuxUB8O5eZKWtqJosQJKonOd +wMYnFVVNf01ulWNiCZt3/x29wfneul6msczghijz+inChSJzRdXLAlpB7AE4dlaO +IdzsJz5dyfQ5omZj8VpZuXBK5w4l4NtCyc4ivPFvGwK8kSr75iEwP4awxo2GJJV1 +B5miof8K7a21hTPqb3GRhue56+afa43/dNyPGCaIaQlKtH2RQaRsvktBESVDCEHe +TVz1zzjxRwEVHV8y1JL1ac5RKDxbgXG5Cb3PwhPfR+Jp5cdqI677dU0TK5wXxzhb +zOEryab77X525Z6BN47ub4vnrfLz/GGmQ830MivxY9YK -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c1.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c1.key new file mode 100644 index 000000000000..baf1be28aa36 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAndc+ZPVqwTysuqt2SwhIRfJrcjfdkdIye88MnCAJjDBueSwZ +ipYYxBCyuHve+I1aDoT264RuZ3gt84XabJrfykiUzyMySTTHD6bFVBb0YXVYjTBD +qCOYKCow63QE7qqr92uJCIY8vErII54mum6askQiR6KUTMiPWn7U8AIDGpOyaZJB +w8eNkkmQ5up8ASHGQIkbyDG7Dyet4qn/0tcyT3dbTGFQliLItZZmIqmUiv0cLnQX +nBUQIT3WDfvYKLTHOY2Yh/dJQ7PXio4TH70uOW9PXBpA7e476Q1WNgUrT37PCLqb +2g0Ey6ZdMOjdGs8ixW03W/imujaNVtts5aS68QIDAQABAoIBADgqFzGlgk8wX+Ys +jPM2B0GceU8zQcxZl00m573GSJ9xvF2xq4T80+Mwbefna/fVlG4GMjar8P0wbmlw +/8AVe/xmx0zNoV8bQJn5Jc4+PMdREilrOyfhQotIOBAQQJEMehSYVzKJUaNOmNjT +tcQxByrXw7rpxwtrclcpOmwMyz3xMUR50TOl7cA/RlYGq22vWeYAnU1vv9I6n2jl +FqkOnGlaUYht7RXGqNgSMvu0lUgJRB4CuW6mYKYVIMO1ZxhNR0bhqv0AgLxMwmKe +vB5h8kiNsUTavarVypOvn1rskPYp/JFZsW8viHOXq0Mnt0k9woVQoLIBPHyd4Fx9 +KPLMauECgYEAyKDU/uMWDNoKBv9LgZJTKvElUCGgzPpzJtu0Lud+WK3enTp+NobC +acdSIl4OfPdQztQ5BGdQvfRyWaqjwiC3JdEG1FOafBVwYgGsJGQTwhY7rvJowpUC +VybMvUOJuJIzkRRey0kBbVPh1bZz6ICw47XVk+TazomsoN3Ln1AZAAMCgYEAyWdP +r3P60Yke4nVl4NQ147RE7xBsbSMUww79jJ1NuX0qJLQDAiHHIyNdhhin5zloi80z +tEGAyztlzRZBv6PTexKJ6FBW2r2HLxvLuj9T4HlEYWq0Az+Ax5X3UiKQvwbEq1f3 +quGnp5e7wnal2GaZazOQi/zvfcHFD5NY/JG16PsCgYBReS7GC4YK1gi4arhI+Bka +GS+j6OQaGaLlptb3LT8b2A/NHdHfZr5QoobyOev8R5rjQz+cUDBz028Uhr1Vul9l +1grF8uWLelFXG4LCjHmRmeHG2947FrQKnFEMkv68m3Khefo853rS4PuXBC7xvkEX +V+JR2uAYe8RE1403NxnP7QKBgAnpLJGsumX/QmbQch/7MV+wKkg9XaiNNmNW5IE7 +6Gw9TYqa1R8QvHTtM5f6tKB7QbrHcUjohEb7+El8U+XzUN/YjLLC6VNASlu2GXNj +1GPSc9v7XTpfsJ2hspb3HnpYAeuQm55sFkz3+YRbRFP+0IPm+LD61Goy09itXo6n +OPVrAoGBALrYTRX6B3yPgriZAHMQg1mwErT7Xl9C2OsYcxp65iUhF5liUq0s37xu +zlFy+CZ+OOnEhAYv8r1AfpkZxbPRhicvk5jR1qsaaW+NrOgDXJtG7yxn5FeDd5UA +kmWVkf/rX9DQ/U/K5w5+yfge+Ibgd0+DJmj5Eah92oJAjr2BliHv +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c2.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c2.crt index 12ae16246e3e..176083d09d78 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c2.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c2.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVANLcg7s67r1TLdqNOBRZ45hhVL29MA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW43LmMyMB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjcuYzIwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAIprTC9xhz2JYqSV6b7jQrVBeDS9KjkTdYigPMEnkBCW+Gv84TxeU6gXfyMX -dDmL1A9nbKhpkDYh76FKdd7Ik/fvt7Ixa3RNMe8vdgMJCs5dog7IzdIuncSCGhGK -VX92FQzja3I9JG2/jjwRtuvKbLRDX5bk24bkyPEWid0p3tsnAgMBAAGjfzB9MB0G -A1UdDgQWBBT2lPX6qZfTAO4kua3T9ianDXkCOjAfBgNVHSMEGDAWgBT2lPX6qZfT -AO4kua3T9ianDXkCOjAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNy5jbHVzdGVy -Mi5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAaeyI -QHWlllHZjZ5M3zy1fG/v7gH0uVWDvdmcrchvYRPJw99CDXUVigDLYD8470BwUqaB -tygQozU65Y/jazIge9q53REAw7812BXO4lGUj820CxV+KHjjbI2+VnUVDZguUBwP -ftjrG9lhWAt637WSdShW/Eb5wFSj/MnYY0Ilvl8= +MIIDTjCCAjagAwIBAgIVAMxKU8gjY6x2PZ3flfJA8HJ61WsdMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW43LmMyMB4XDTIyMTEyNjIxMjI1OVoXDTQyMTEyMTIx +MjI1OVowEDEOMAwGA1UEAxMFbjcuYzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQC6pkFGrd+lACEiziZrWNh/JFM21T5Nkl4XNGuc0y4oHHRhUnLysm31 +3DIeK3OYebIqmE3cTZULx0lR/sX5u1qkbzomA21XcKxOyFukiKa5proUxN/jAIr9 +03Zd9o2dhTxHFR/iGixda1hjQFCM/4HWo+UtZzHduwFd21LPilyLdcpvhj/BqoRl +ZuPYFn/V5687BXqUvGsgcCMWgxj/0AXz5eZ5YmRGXtCuw4XhjaVDFmUnF0F3j4uE +gyGWnw0+8FP1YlT3kvt6eDcgHQiRSp695f+Ce0Ek/igHJNiNXP5O6r00bwjzltJI +AmAcK93crqn16GP2ICVXP4Vmneoy+20nAgMBAAGjgZ4wgZswHQYDVR0OBBYEFCIm +M3doAK3dJlsGyxaLHzU7eQg5MB8GA1UdIwQYMBaAFCImM3doAK3dJlsGyxaLHzU7 +eQg5ME4GA1UdEQRHMEWCHG5vZGU3LmNsdXN0ZXIyLmVsYXN0aWNzZWFyY2igJQYD +VQQDoB4MHG5vZGU3LmNsdXN0ZXIyLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAFIj09y8KcXFcq+arj0ZT7kZyQuoPKFVGRcIYXoF4 +dj4Cu5uUf7Y1m5r/pr0MSwUMxB0tKxoU/9LfIugKQX1+uajs+NM1cyeWSd4OnLJw +KFiLpZNtpaob3RPfz246CTH02saZgxvtuD2yjjsHWPU/yLVjhIZqEE5zb5yknAEa +4f+AxdR1UyevCRk9JZr7bCy7A5NyRjCgltbXIaQeHLb7jRlVoStjVTc63C4QMFaV +QcsBZggsqHfoJ0iAo3uY/3qfheaDHo3IjGd5ry3skssaxy0pcTm8REKyuo4Pgm0F +P9UKkfIvA5XOPoRkxe9yN9WMkda5uZk+EjXebkc4tyHBtw== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c2.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c2.key new file mode 100644 index 000000000000..b284fea1d7c0 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAuqZBRq3fpQAhIs4ma1jYfyRTNtU+TZJeFzRrnNMuKBx0YVJy +8rJt9dwyHitzmHmyKphN3E2VC8dJUf7F+btapG86JgNtV3CsTshbpIimuaa6FMTf +4wCK/dN2XfaNnYU8RxUf4hosXWtYY0BQjP+B1qPlLWcx3bsBXdtSz4pci3XKb4Y/ +waqEZWbj2BZ/1eevOwV6lLxrIHAjFoMY/9AF8+XmeWJkRl7QrsOF4Y2lQxZlJxdB +d4+LhIMhlp8NPvBT9WJU95L7eng3IB0IkUqeveX/gntBJP4oByTYjVz+Tuq9NG8I +85bSSAJgHCvd3K6p9ehj9iAlVz+FZp3qMvttJwIDAQABAoIBAAGbpKmFaybSjUNU +GVQ70lAOGEI7RhHvAxNNTU879oRvJYWGuBzLjFNdthGfRQJF3ux4N9UZ1s3gkqSI +ZSlvA5HlFdAo0v+5pcWSYYLvKaVYM8Qxo5tWCg+zvni19g33jUeWsM0G3EwvVfYz +VXGEuTHUyaJ0s1ZHFSghpC3+vo6PZz6SSF0NdlNdAmp+uTu2WNql5G0eG8lzOwmb +mDSqGtFRJpUCmqH4tix6OzZOdg0VNpvZxnZTlLD/MyWAoZvMdPTOe/PqZOfjp2ud +lvHjHzTxJ3mg4mUo2It80J/7EI37RHZIRQgYaG3tRVFHUKl7s6c4SdtogKbkI+37 +ZJmHSckCgYEAyT78KDIbm8FpAuyrqPaToEIfOZuDSsBhU34/z3uBkhLKUN5Jklo5 +rlYdq6gCPNnOtRPuQG5p2AZ59wujO2ofhEGGiG1LIJLPOtfQt8rwgkH4EcwuvtsZ +WvkFJF6SYOuAJEr2vxceqEf5aCi4NZFbQ/Hm6LpmRqX7eaW6v8JSVSMCgYEA7W6Z +oOKc4kf6K4FBERM1VPt2f3M45c/0HDR/CqVyU37SLSfvW66YmWs8WMG33LFvXZ1H +VPZyaVK9BIAFYbuTcF2CZz8gwr7HrXJe7HGfiEcrUEsJ2IJHx7wp/aN18hDiXuWx +L6tkP25aBQhLVIDYddSDk9r8IeRS/9GEgBTCEi0CgYEAkaELlgbUpbyrQhuECSwO +Hj1bHTPlKXXXuDS29mz4kX0ZfLWu5cJ21SqqI+rbW699/tliYqvD2jitj9GTGM6t +6eYE9bqpT/QNFBbSif3IV2pnmnF4LlKcoZFsYCozoQEqZJj+v796Rup2mCJHUzRR +p/RoVWd11ZANT7VXU6bKL6MCgYEAgzLYZZSTSF6pmJylzb+ler4xbp/G0wakR2mi +wclGcWjkztsAYtMJs80T3Z+974+EkGA2aHZ+JkD3/aKYVrcF89NYGaFCgxpU0D0g +wtrOYsmg3493c1IaQ/5v3Z4TsJ0v2WxnCoZUsNBTofBBYAKKmiHCS1N4e2nli0LF +StktnEkCgYB0FSjtEmA1OTZK/T+He0e9nQAsSRh4Ugn2iF05/J1fphyYiaPrRy7m +wD0Y/qVj/Lkcchl3i3UO4n8nsD6Ahh0cWi8bKX6ZD15bDrCIZI+flAZWLCUoOQPF +ObFdE27lM9173IDp/6qNvsSsj5kPEe56/zDzlbwuLhdnapeqxfJaYQ== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c3.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c3.crt index 6ab5f2e7808f..51736b559407 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c3.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c3.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUAcrmViyO3Zxe2UJugNb8VtZNomkwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjcuYzMwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0MTMy -MjE0WjAQMQ4wDAYDVQQDEwVuNy5jMzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAjOfHGF/Iku2fsDm+qYyGVEd8jw1eQ1PGVoaG5cTWy/H4Ho+CRB4dldki6b6h -9A7RW1qbvZJgsdeLBj42tblU1nOYnF3y5SNIcDhzxs79SjNxjNIHgdcjeUR6pLMv -DBPk+1dXLgoHMd08Un8hanIK/NA8ZgP8j2j57xS62hFGiHECAwEAAaN/MH0wHQYD -VR0OBBYEFOPlr+WAnl/xLz8B0S9VQPl1+XrXMB8GA1UdIwQYMBaAFOPlr+WAnl/x -Lz8B0S9VQPl1+XrXMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU3LmNsdXN0ZXIz -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQAoWtAw -kGlOpCoGhgVmws1eG/47m0o780t+Mq5Lh+IhooviwvaHGlSJfWieOePuiXeLyeb+ -dXTKkgxnwPozNh5lT6X15C4XDIMJ/T3fr8Ggzi+4n1tY7MNW7ll3pdGFvoiKaSNN -2gQ8u/UpR2Dv2b6OHExiQP3jy/2mtIAQOQ5fPQ== +MIIDTjCCAjagAwIBAgIVAN6fQcwJXH9Cgrh1eMh/vzkI8+YpMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW43LmMzMB4XDTIyMTEyNjIxMjI1OVoXDTQyMTEyMTIx +MjI1OVowEDEOMAwGA1UEAxMFbjcuYzMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQC0g1U2ybKwsvOAnOC1nxzkSUZKAHTgjmKUA9HtmkcV7vrdtxKp+5mX +JVoHRmjpGgYE5rMkX8ofip83G8MY9tCduXk6kKEqW32/vKG9ysK5midA7yInt3AY +DfpnWLktFfri6GIB3G6vdPUNe+VXy2Z4mV2lKsysMWG2mkz8ORFnyrOfY1ZkOKy6 +bsD977pNwKbcfUT6yw5B+TyT7uQwWJ5A8dcdX66NP1eahxWer+YrMeo3un4rQedF +bwsxr2t2CTKqpG3fmtl1ZVRFbzH4g80xiUbo040mqF06SGFg33bkPVVqW2icmzdE +pXDPiKg1GFvlPhX9ji/Hvssas2cDPz6LAgMBAAGjgZ4wgZswHQYDVR0OBBYEFOrm +zHIXmeTefGMUUVvML+hWLedsMB8GA1UdIwQYMBaAFOrmzHIXmeTefGMUUVvML+hW +LedsME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGU3LmNsdXN0ZXIzLmVsYXN0aWNz +ZWFyY2iCHG5vZGU3LmNsdXN0ZXIzLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAjuoPQDRuum2vaFMYbWIbo3GKqLHcPU/6APHWSMhB +Z1CI0dRRE9a9upW54OnkoMThYrXEVy1xrC4CY/ujL0Dcvhx+b4xVTO7S3WL5k9yk +RYDXHI0EvHgTfE28pZhqcMgSnL7/kqB2sfvQvTdBm55yZgd/mvcfysi2v0V6zOF3 +kzq7SrdeWr2W3+ZGNoplWDMnRSqqftRZ794i94N+l3NpJDg0uCnA7cP/pS5ssyq7 +F2NC8D+NXQXNsAgsshEspEBnVOTzJf7AkMfyw97i9Ge5bjqeJOGbNZjTQOfPnJUk +pUJhQTxOuMVds+8CFqpLY1ICZB8fIV+khJc6fN5sHkvPTQ== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c3.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c3.key new file mode 100644 index 000000000000..5c679c74d723 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c3.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAtINVNsmysLLzgJzgtZ8c5ElGSgB04I5ilAPR7ZpHFe763bcS +qfuZlyVaB0Zo6RoGBOazJF/KH4qfNxvDGPbQnbl5OpChKlt9v7yhvcrCuZonQO8i +J7dwGA36Z1i5LRX64uhiAdxur3T1DXvlV8tmeJldpSrMrDFhtppM/DkRZ8qzn2NW +ZDisum7A/e+6TcCm3H1E+ssOQfk8k+7kMFieQPHXHV+ujT9XmocVnq/mKzHqN7p+ +K0HnRW8LMa9rdgkyqqRt35rZdWVURW8x+IPNMYlG6NONJqhdOkhhYN925D1Valto +nJs3RKVwz4ioNRhb5T4V/Y4vx77LGrNnAz8+iwIDAQABAoIBAAb0EinAnkIBOrEH +Gf7VCLtgeTUrjFUEFxcFiKYi7b8SDBbTUQQ8PV/dn/isxSjJiFbS/i6WBAawA88P +IjZJd6LDvMA9RwAHMzJaFU7myxZ0MjPlokauYcKDc1RczhFHCfmIoxHYSGk406G3 +1o9JtgJKQhm1KtotMsriabnDhwCc9TucNoqZ55vs9TtA6ndEOBGsGEi7N0Da2Evn +VchKaNm1qzT2T9q9KLCh1T+JoJVgtBYJr98uOKw4H994rJOyP9AJX2Txzg6VCFgA +7vcxnnlMlEjzuXVO/eBFknEf6QRb6xxdXxXMCcVRGVAFb9rM7KqEoe5DWmaYHssE +h8S0xwUCgYEA103MS27LI9/g0fGUXvl7VGBz1JHtwGLLTtmaJvvSD32Ded0HJ2vq +31tRIKzttafpldLZyL8/O6uBfoC8AG4JbjWMJIZU9POgZ5lj1kAZTpDvsVdJcVzc +LL35Kpfoc3mXsg15M6KZAXgV7/uULhd0hyvcGuuZvXSNN5mn4jwHYm0CgYEA1qIP +s2nMBhuMjIK70pB8wePpMkPm431P/FsY4/r1R2l7WkwbZgQSqg79Yl/y9B/6DDgU +SGx1Ekf2KOXUVQoLeprMmAH+JcX0scBZI9Un9IU3AkXWuuQrFj7P030G0X9XJxyL +Daojg+WFC5cWAR00vmus39pn+3tzJZDU4eVUydcCgYEAtCAtU8qMfyX1JA+1bsPQ +L2ZGYcf4naTJrN0WP5kcQ8NhNJn7OzrpFBaEyA6FDR4+qQ7vDykZUOWKf/hZ0Q4+ +K4r3BTOxbgFI6IOG6B9WTMmrDrYVBpeGyJPjGoRegAVxKRBmnxFgar85sz3J2cnb +vp6uFYOYas1Qu/x8XSSVlP0CgYEAhysuyiWxuJL287KNiLE38H+Q5/f9W30at2nD +higQ+nVCmv5dLuUXwAOKrb6Rag8NUU6r72RmYinxq5X6H8ggrIj6mix/GtjNHnht +avSdog+XMhY7gh2ZyPurtJwDhEI258nBBM0GatVL8Z2n3PHAur9i8uNAIw9+18Sb +XNwphqcCgYEAij3cAWoMjv14P6I4Y7M52t1qUXmfW6d0Y7iAAa9C5kO9sSncuPDi +MOUwnhK9Y+L+5slkuiCMYy0GCxrsBUhwn4h1XslvRrRtuWl2KsUJZjWH+qgpSwNV +o3YAyrkh+bdm1UIF+Zh69M1mE4i0S7UAVnEMxRv9NOUh4G/XOH9//4g= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c4.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c4.crt index e5014272b86f..e1c34264a003 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c4.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c4.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUCFu9yiqlax/R6J1jzPe0n1gF424wDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjcuYzQwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0MTMy -MjE0WjAQMQ4wDAYDVQQDEwVuNy5jNDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAiPGG+jtQyATOybaNzp8DBRK4rKu6Fs4YVRVanfCB1I3FE8YRdh0uyQQEw4AN -gxwaJ1MoXHMeXhcrjRC3saUgHeexKy22sn0YKEDTWeAKVhg1C8BppXRZTGp4UwhR -xGdgQ1oFLjQn+NcTUo4SxEEp7aamFFZPq11YtYsp2sbAgVsCAwEAAaN/MH0wHQYD -VR0OBBYEFDIEc9vl3VibxzWsH8wfzm/XYh4FMB8GA1UdIwQYMBaAFDIEc9vl3Vib -xzWsH8wfzm/XYh4FMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU3LmNsdXN0ZXI0 -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQAfV2qT -G96FWbvq+o4zxrW6TVAlmFQ2AWcu35JcD9O9kfl/bd25k8mhsac1P/cIPRxKcFIN -9EicJpp+EQ6tespPU5xb74eoEro9V9cflQig7P1xmiqVitmxN5zjJBWowOoPeB56 -zx/f/byUY8MJ8t1pAFLoeYCAOrD4eKv2eGpB9Q== +MIIDTTCCAjWgAwIBAgIUB3fQByCC6qJge9B5NdJS4Wd8ZfkwDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjcuYzQwHhcNMjIxMTI2MjEyMzAwWhcNNDIxMTIxMjEy +MzAwWjAQMQ4wDAYDVQQDEwVuNy5jNDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAJNX2kBvqwsvejGbLK3yWotQS03cPKzVVk9dSqeZPWYNTA3GwdFD5P2L +zgH5KrbBnJd6p5UTPQc1nee67EBk42wRmGtyEi6w0j/JPUuWDVKUZsLP1mUeBSbj +3hEQ5XPVKv9XsCfWaKvVWWv0c68CKmhZHr+xXQPWcx6IOva7OvpmY33VattYVwFD +MTA0hsqghpBnKp/Jp2kRHnjMUcZQIWNOc4rrkgja1q9xHjPuvOHkMTg3lK1gh/HG +za1YZ7xQcAQZkHi13xsUzf0ceKy2KQVFKm8y1jsCkRF/vKE83Wa5QjvkL4qjra47 +iFAQfYDz1BDqrTgV/LrlxbNOPhzEYOMCAwEAAaOBnjCBmzAdBgNVHQ4EFgQUe6PW +NifbX/5GSPe4xzqJ1H3AV7MwHwYDVR0jBBgwFoAUe6PWNifbX/5GSPe4xzqJ1H3A +V7MwTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTcuY2x1c3RlcjQuZWxhc3RpY3Nl +YXJjaIIcbm9kZTcuY2x1c3RlcjQuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQBWJ45t1hEWIlpijAMK8058IbiafCFAwfPBtTeJp1+F +147MEfklLNkglAlk8JwubIgl79yJCncAqgx5ahZdTk48F05xrwjDGap7dOizn9mO +3kNtScMRyZScZrpq2iiPV7DfiXEH1Rf0vw30sYqlC1+HUeQxEyxjdGvYbU4wFM6k +7fSDJscM/S0aA2Re8JEWKUfWcYXprmWGgnqp8p1E/3qaUZwZ7hLbNlir8kvPPyFq +cnTqVdRDXCjw47wH7w/wFQiaXob+msqYR02K1MpBLFyF9Tjky2iwbtVahag3blou +hnSTKK2ACT5J7Qp2Li6XG/ShMIrKwGVkkzOGwPhTpvdM -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c4.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c4.key new file mode 100644 index 000000000000..1a2d54782ff5 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c4.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAk1faQG+rCy96MZssrfJai1BLTdw8rNVWT11Kp5k9Zg1MDcbB +0UPk/YvOAfkqtsGcl3qnlRM9BzWd57rsQGTjbBGYa3ISLrDSP8k9S5YNUpRmws/W +ZR4FJuPeERDlc9Uq/1ewJ9Zoq9VZa/RzrwIqaFkev7FdA9ZzHog69rs6+mZjfdVq +21hXAUMxMDSGyqCGkGcqn8mnaREeeMxRxlAhY05ziuuSCNrWr3EeM+684eQxODeU +rWCH8cbNrVhnvFBwBBmQeLXfGxTN/Rx4rLYpBUUqbzLWOwKREX+8oTzdZrlCO+Qv +iqOtrjuIUBB9gPPUEOqtOBX8uuXFs04+HMRg4wIDAQABAoIBADgr11wWVj5K6Mot +jrB3wrX2s+aobgczw5Y5CiNvw5z6t8JOwgf92rggSras33BRylljXKzz6yGq+ppj +w/pem+bQpGtdzvm21kiT9wawWz7erXhaVLEy6yLi3v9hkiABJcW3j8P4NOpBSThj +NpEDAdjLFPJecwfqAvQiWlxCj57ywlg0B08UCq35953mml6O1YHsW3dDHccUZt2A +Yu3Z4e77pNB9vmZMj874Ro4/O/n7gMFGsdV3SCjcukS6XDgidO/67czZdO8uuFF+ +07o14rccPS3T7zkcFUFJHidPnBbaU3tj9Fpho4cMm0ctFQkcgp+LsB02+ReZAd0C +wrfhi3kCgYEAtfNnp5y/bjZinc/rr3pRqZ0Tr3ez6470LFU91++CFrAlMe2s9ShR +2fD1sq1Bhj89wZDemPKrBPGZSqExtCuZAR9EgV+ZLty1yLbJHRq59lBf2rBjtEop +SZ6L951F9JWPGgFKH57uKixAWhCGjfGBeOG1SfoZaVA3lKNOMuwEVBsCgYEAz07X +p0qXmazSvK41/sk/sVRCCv0QkwidQvFPnDKidL2ghAOr8Ufuc2BCyxVVZ66zN05f +uBtcl7O0jWgwDpAukmiLnYjwKPb5FwJJ0Ok97y7mSC1/0fZjjhVnggU7eqNuUpxY ++EDbN1GtscMqNlHVvQAqEvU0tRSUTfzedC2gotkCgYEAmGTs/YMZBwuAH6OlSI1P +1ptcgIvm4zYRpwwiRzkwoSYZkS77vT5vB/Z9I5Ffnzxa0kQvXLVrrjFY7hwC0F/v +v/PbYeybFZgAncMSwVDATuDU2AsDB19SI3oqy1Mr44ZcnDBWUBPCcQotQAI1YK/V +fFS3jeVu4TkxxEnfVmE94t8CgYA2wV5Yg20zW+B1kZYVa9Z04gpnmJvuGhJfghlC +6mTabNOeytF5c0RaGQzLUyExt4UCkKLbn+5+6yo/R0uaLR7/8LkfrPGyADmeiC4z +DcgcCsksO0hNjsSQ3LpwLy2coNoc28rsF0VPgTVdhGz71/WabBaw1y9KvNrHFokz +KnIJUQKBgGmYad8p87Mo3q639xryMuVyqil/oJjY9+UNikXBUefHDMvmcr2uV7zD +RVufuvqNqyIf8FcshtrPx57BAcIX2lQoo6lXAi8UQzYJ29oF3gVaxjRo3QCKflMe +KqLGiX7BTnVR0+88srvnm3kKSD3HG0UeW/4ZoclVGAiiETTJGw9e +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c5.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c5.crt index 01bd3a9713f1..5f2cfb4cf8fc 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c5.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c5.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAKI2zR76R0ojhtQ6/nGpY/yzXCoIMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW43LmM1MB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjcuYzUwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAIxI/vm13r7qO0JHwIU5qFMH9FnANuPqUFnDaxM6uircaa7I4LwxKMp0SSYJ -Lh4+ThihGBOk8GofaFj80UwQMI0s3yqxn1JaK+DPvvStV5Px5CYyWHOkzLf6Qhh1 -cGPRrsZrrYmp642NAQWm/8v4SV4BmAzWuPUhjfM+k3FnXbF1AgMBAAGjfzB9MB0G -A1UdDgQWBBSX8uUfOIjJpIqow4083qlVg8FrXTAfBgNVHSMEGDAWgBSX8uUfOIjJ -pIqow4083qlVg8FrXTAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNy5jbHVzdGVy -NS5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAiXUe -wizWQNa6GwXFLFZOBPsYCuVJPepjy1Zu7AWuH9WEOQk/QhlVc47CigYnt5LN0H9k -OYrGMugwt9MydZvujO7K7Vya2AmvGXtc4rzBZhSzzKw2OqCyebV4q1BH2NPTzq5E -InDLZDgZ8ClZVJEoXKlgHGetW9nwwplBHCN+Qxs= +MIIDTTCCAjWgAwIBAgIUBDVFOnWfggQ/dqS99z+vrPZaOXswDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjcuYzUwHhcNMjIxMTI2MjEyMzAwWhcNNDIxMTIxMjEy +MzAwWjAQMQ4wDAYDVQQDEwVuNy5jNTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAL+TgD9d8kTxbabIe6rOyNncdONrXa/6OvDy2yUk9uxGjvJNj22F0A5O +VYj7YYIGqZ4W1Bg8oMlIEwDVXE8CQWkQR25tRl1Ap8OuxmbqXQjJPFTN8bAc9Skh +LWpo3vLtXvDga+vh23HAHjme3kj8BzaNHgx0f3lkhXHTNQOPRqDb/aGlftqD/ynH +kfz1O48Fsa4GNMNS5TMEVoTZNrVZIOZyCHi2JYH68FL9WZDkX3LIMj+QuKs+JVKE +z0umfAsChKs8WGE7zMF2uh92ug+tSKhcn/2D/Gi1FmNzIi6NfgMv37fm1YQJzeIX +kdkCGF9Go4tRd01BA0dqcvwdw13cXkcCAwEAAaOBnjCBmzAdBgNVHQ4EFgQUY43r +gQTGh+XRtrm1v8yHMQk6LywwHwYDVR0jBBgwFoAUY43rgQTGh+XRtrm1v8yHMQk6 +LywwTgYDVR0RBEcwRYIcbm9kZTcuY2x1c3RlcjUuZWxhc3RpY3NlYXJjaKAlBgNV +BAOgHgwcbm9kZTcuY2x1c3RlcjUuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQB8gY1bsZydbsLimr1P2tDhuECn0vp1qGhoqS+wFrgV +8mGS5efggjELlK8x8JWHl253/58ZUXR8Curc1Ep+nkEsqwvTTz0Tm732yIsWeQqa +boZVS/zLVSuTmNR5mZ4Vadw+XZYkvpLPdbWXNmkUpsQzWJX7H5GCdJPMHkbx5Shu +kQ7PIu7F1Xx80pCnw3Lv+q3NVkC0cw4NgRzJUXfs8pBI/IPqmDvdmhCYHoIGS+fv +BAGBPHeU0dvU3pTHXjrAmIVln6UL/lphs5MnhRLVMNpzjeoBGkyujSn69UMAONTd +hyacN6SzdRtm3HmqkRLlGhMn2152wUgQaJ/DIe1WRhTB -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c5.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c5.key new file mode 100644 index 000000000000..fbf35def1279 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c5.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAv5OAP13yRPFtpsh7qs7I2dx042tdr/o68PLbJST27EaO8k2P +bYXQDk5ViPthggapnhbUGDygyUgTANVcTwJBaRBHbm1GXUCnw67GZupdCMk8VM3x +sBz1KSEtamje8u1e8OBr6+HbccAeOZ7eSPwHNo0eDHR/eWSFcdM1A49GoNv9oaV+ +2oP/KceR/PU7jwWxrgY0w1LlMwRWhNk2tVkg5nIIeLYlgfrwUv1ZkORfcsgyP5C4 +qz4lUoTPS6Z8CwKEqzxYYTvMwXa6H3a6D61IqFyf/YP8aLUWY3MiLo1+Ay/ft+bV +hAnN4heR2QIYX0aji1F3TUEDR2py/B3DXdxeRwIDAQABAoIBABwgFgx3iOlpPm1l +co51lFQzwu9eZF9BD+Uu6lBuOFgg1byp+VwdcJmhE3qsWTjjLIS6mkHCqOJUFAIj +0M0CqkK38py6K0Hmi5q1bohOcQEothRHNevinHWA2u+L3OtyQOWBNSbovjRZfbZ8 +5ep2IcL2/cnbUpmqUdMPrHFM2BQmZrXnjP2rHhI6l1nVlagU4eahkccxxgGbICUU +thVHUyAs+/+ZAz/tZSxvNPB5XYdVidc0S1wCW+iaS2gx03NMig5yMMZOJtILomro +e9txf1Q2+074bKkEo6zm6ZR+0+SGvzDkge0jtJgTFOTkdqk4c8a1ttf2QemVzMAq +zZDpZekCgYEA8XBkU9R3aHoEAcNF1g5ERKsCE72JzC4h6NgJEkP/HR96waNktoM2 +c4H61wlvIaL8ji2gaevrBmQqF0uJtaRNhIKrThT+OtrBY9lwtDEsLVs5naLZ4XhQ +aLdKwYhkPg3kjGRePAHlVl5em+3ibmj5nyUOCTd6Qja/P74+G7kHy4kCgYEAyyFF +IqL2Jwi4abNElSbczhL0XeEXzzbZXK5R95wxBAsTvducQbmPBJjON48SorNPkUQF +3vaD6DbyRbyT0xN+YVCJgSpAfkzC0IFcJJouZCwmJfFBHDe7cF5iBbH6JR2S2lOw +HbA8VyqLUSehyM7hI41SpdlLufOwIHilsszBV08CgYEAvz6PufdqPk+oLN1IzC2w +1dO/aXORWzKPmajGRrmlsqPk0M1kUU3OK+ChmQPYC4TXGKf7kOtW11EwJhOnmUH0 +RSpcFxV565Sr6zOpmpH4tFS2Zq9AduCekNPfWz12adEHYE2Ad3fo+Wj29/yOh3aP +hAgN/i77oMdM5mkDOWJvUkECgYBbxa8/g9KkNgtjLev5WvJ9ZBDlbTHdm5E+jz9X +GEkI45xKtbJ1+0jDMepRLaGkwlBbeBkS79sOieyRVy6OOYtqmgFVyuoSZcnKe/dO +Th5RgaTGlaXuArv7e5jbq9ow9JiJbS/vadUJKJ6Czt4IJHeRIWNKrM7Kt29GbFuo +WVusfwKBgHm1jMIYaLUNk9RVdV2koEJU1BEQFkuW5+pcKD99MVbQeSKCyY/kvLRH +KX9ZQ+5yXjvSOwxNYhc+gn/GqI28Mzk91R/LF4ThuXy4kMwCZVYaM+52vQoeDqpT +dspZbTGe4AC05kpwEVHO+uyVN4/F8hokUoz5mrHeuW3nSEejBMMR +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c6.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c6.crt index a46ad1772f9f..eb4df3893e0f 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c6.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c6.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAL0x6vL81rrpGoM8JmeGDFzfPEQZMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW43LmM2MB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjcuYzYwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAIu8u2hGHsJKq9o6VOKcApMaSDf+rjr38JfwCNgD2opjq20qORPIcrlrggJ2 -bfxE5o3hb3bdTCEpY9gW8X8IRwwAeqgB9aNLfIf5+RoxpawNRLJDQD8RmqONygrB -YpIt7pVkDLHRnHqmJqnTGJMGbPfenUxHzp+rruKzShdN8AOPAgMBAAGjfzB9MB0G -A1UdDgQWBBQ8VNuo2DOKDS+BMuRuCDxohRSFsjAfBgNVHSMEGDAWgBQ8VNuo2DOK -DS+BMuRuCDxohRSFsjAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlNy5jbHVzdGVy -Ni5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAgzC/ -i2TQMQEMbDRJew02HbwJyAVR4v00Ep8/sK6fgzj0pB3blUgI1CFhu/iN+rbMkLe0 -1IFgRbJT6xhhu29TCWWC64uggySFWQTwujyphu4ATGgS7zJF07u3fUDB0JEtFecb -UeQC0xwkQUk0RBnnSsakhfAGQ1iDi5/TPA31irk= +MIIDTTCCAjWgAwIBAgIUQ2Pmb8vrcM/5MQN6cm64pgoPFSAwDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjcuYzYwHhcNMjIxMTI2MjEyMzAwWhcNNDIxMTIxMjEy +MzAwWjAQMQ4wDAYDVQQDEwVuNy5jNjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALgUib2HKHUQOJfvFPysBK/dL1QtwBIIbena5gG/ZAljXvSdm3lcYH6H +OLgvzI4pV6y+dDT2u9SnBRt7ELSP3kJM/c4m+Z9pzmwSSlVh4ay910OzCfxj1wPR +PTbVPRJm5stUqqfmTjNdWWjp2Zt7x3pUqSU6JzKBRBKY8b9ygk8hcjX/Gmdb3pY9 +zkYvVa/+Uns/1M+wce9GE7b9oUgNNPFVIh/uNDy3ukd+XQBjUKw2FM9h1IeXlpAY +Lknc6Aa2Lk5hwB3QCO7doeLUhuQwfSZd9YdwMHMbiJykL0adf/OPLeNmozSY6JhY +oegi6q/eQVTI4trbpx60vFOsrRWvJTcCAwEAAaOBnjCBmzAdBgNVHQ4EFgQUEZ3K +nDLsBuvS0s6KybVwfdUbfU8wHwYDVR0jBBgwFoAUEZ3KnDLsBuvS0s6KybVwfdUb +fU8wTgYDVR0RBEcwRYIcbm9kZTcuY2x1c3RlcjYuZWxhc3RpY3NlYXJjaKAlBgNV +BAOgHgwcbm9kZTcuY2x1c3RlcjYuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQCMhle9VxOKcdcZOT8Uj781z72hVc4uj31Rp+/W1VQC +ePKMw/0ZkIJidl6sDgF9xO+sbrH4QojperCalwpvYOMrfSsILIyAMeMMnpHT9Z9p +iDTuTJeQDXZxQl0RQRn8okDT6RNGiqpGXkpTnIZl2ZxOls6ZdAduzyWdXoWo1gqU +1UR8F8Yp+osL4AzCfviaExoKXlL0fbzhEwh2rhMMcNEVmyx8sIlEdRDNLUZRQz/y +XoBaDjZ92X9cRFZ1ETGU+lwa5CwZ9ln7EORT8+7yNE03m6Lq1B6B3eVH7kcgO5Hg +j63lsCnfFcJfO/70XOSrknDe+uqV2cxZAE4cb8829dRq -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c6.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c6.key new file mode 100644 index 000000000000..409d71e1cd7b --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c6.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAuBSJvYcodRA4l+8U/KwEr90vVC3AEght6drmAb9kCWNe9J2b +eVxgfoc4uC/MjilXrL50NPa71KcFG3sQtI/eQkz9zib5n2nObBJKVWHhrL3XQ7MJ +/GPXA9E9NtU9Embmy1Sqp+ZOM11ZaOnZm3vHelSpJTonMoFEEpjxv3KCTyFyNf8a +Z1velj3ORi9Vr/5Sez/Uz7Bx70YTtv2hSA008VUiH+40PLe6R35dAGNQrDYUz2HU +h5eWkBguSdzoBrYuTmHAHdAI7t2h4tSG5DB9Jl31h3AwcxuInKQvRp1/848t42aj +NJjomFih6CLqr95BVMji2tunHrS8U6ytFa8lNwIDAQABAoIBAAgqSQ3SH/ZJWOlM +Xpbynmaang3S1bE3FIEKB6fOMIbGFKEvGIVilpkmMFrjK+SmatArxoXO5/f/zJcy +d0Q7GXXBaoSpb+slS1W53fg5lkQOS60iFPBQEWqG7nsvLD1TuSsNv2AhNBjNLDke +HlvqOqfvd21CThjMaROWGysNzj/qPXQh89CE6QqDwCtZpwFzzjMXmUJX0eRnXlNj +6WyPHC6M4Ud5d9qqYXkD/wnWUhjxNe5bhaanqgCfN8kr9u7ivL/i3re7z3ZjRZ4h +veyNvXoI59ZOvYKLvSoVIN2o6DG0gQ37uq8/NNk9tYHkJsbRriL5bXqp0pqrfMTl +6gM4YiECgYEAvh8jEjMxBQ/BWGk5CM9NEHnqLGAIWKkftHiQnSfzO4PLUJ8I7ZKM +sLai3vQ5xdGowVW6uafxenELyW48mMg0Vx9iEmoAkpQCU60z5XN0SqrpbQyEiDs3 +S4/EmT+ZyyGOGTC7nWXUNpXvSd2ssBSKsgUVQRUXSWQT7B6z0Cm6BscCgYEA991+ +mf68uKPq62szNXGSuwNguRXTOqFcSmTjekPO4RBzM3yaTiNpHBy/qNxmfOqj+DEM +dw9wgmz3C/e9iY4lOueGXLgUOPUUMaQ0Cy2pFbSTGjTFdCXREPJAouc+Xo1t8sAp +KJhESmhW9fk7QXpwRbiJDrLlGiQetIVm3KVLvhECgYEAh1qfm1t6sMHQwuLWyiDc +GrjRMRqFlqmBevjC7D/++QYRf6pZrtnA9CZyugxh/SYQ4OPp9jdMwgEthRDIb3VJ +XiVnqvhww+o/5dbPtbGGv4oCebKJwHt4GIGYVAplsaqIAgMh7yMJ/HquS4ZYZVua +irb3tMgRNhzb2iTZm3+5gw0CgYBiY/MgcAU8Ch1zrA8c1OOZBuM7NVWu/WSkR14h +J4+UubHxKzIZeGb9+M9/2VV5mq7SE65OCLvxmg2LG6GKW/mdY1Brt8qXPXnxpqve +EBcBfbey/GWxEF3oKzHLOJ4CbIPLi8oKyj65hqeWZoPGHzM4CeOmCQ92jAYARMAr +MBZb8QKBgHFCxFA4GAIQevsSZqFRtYXPI57JXHURS/xZagW2TqN51SlzMHGjqSBd +qLI1Bb/BxyDHyKYkjF41Q0oLeFCj1xRZ8Uz3ijNMaEAQGANHwbou5sMF7yirt5Fm +mNESeEq+Lm+th6hOV0ngyDfQlDbUMONDr+thpqamxmvxV3R0ufu6 +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c7.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c7.crt index c1281ce2b0cc..a648e6794e04 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c7.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c7.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUSDyZ1S/oPMJsyaBKt48INtI6P4UwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjcuYzcwHhcNMTgwNDE5MTMyMjE1WhcNNDUwOTA0MTMy -MjE1WjAQMQ4wDAYDVQQDEwVuNy5jNzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAoN6Ca8GpbkzxmORqC/sl3GucqCU/ha1ToDUuJxwzS6QTlJJ83DFbqyzPpIxx -5YAhYiEfruoIVW4hF6L2KrQbKfAVWsfWHFqBpMycmy64yFCOK91EvHJ9Rs75+Klo -OtSHgB5d4akgKOOBVyLHGeH3Dyl7nrUfi6FtCyfVCAjA8VkCAwEAAaN/MH0wHQYD -VR0OBBYEFL4wJd/SFWld5TliWX3H9JPUcTD4MB8GA1UdIwQYMBaAFL4wJd/SFWld -5TliWX3H9JPUcTD4MDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU3LmNsdXN0ZXI3 -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQAk4x86 -/2n5y+Blg4FkJ9tpWX+nfkJa4SLXINfJxX3D1MNBXIkNlzPSC96XZ4b1zkdiZVTi -EZiIsEwxU1EVjvBYmwGqfLjnqSzNYxl5yR3FPUC1Nhyn9pZ2nieB7tuCxwsrJ1OS -s4xZjO8As7AYNUpytRRYvx8/KmBfZEW/aRHEcw== +MIIDTTCCAjWgAwIBAgIUZqDTTVpePnrL7QjGOwjcDTjTRAcwDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjcuYzcwHhcNMjIxMTI2MjEyMzAwWhcNNDIxMTIxMjEy +MzAwWjAQMQ4wDAYDVQQDEwVuNy5jNzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAJ3advzj9X4BP9SkDPahhW53YVJcyQ9LFIUYQsc4c0W4/+eYaGjp5+Yk +GiOcs2UzydpAKgDKpKb5ftyjRX5Izg1kBLKKWF4Y+TRSQ1rVkesI2c9dKx7KCOE4 +oa7fkuAu2IdBuqmh83fOALW/t5iKXAB+e0bpoUFIdoQ7/xwpvBNr6uHnnS5cUrt7 +oLJlp1an6oO6eOWFhpA7yKhpW98XnqjGJ4Zj21dl0RMvRssw7I8rYEI82v7IvJST +Rr5Y/yr3wSyajuB3ukasuQbJbofIUb04xFp72jMHR54K2IXyrHviQco5+QZwSurZ +9Sn5cIC3WZ0CyBD9jajf0ZM0xP7/ViECAwEAAaOBnjCBmzAdBgNVHQ4EFgQUcG/5 +VZ/teprNolivxZ8yRRYCuiEwHwYDVR0jBBgwFoAUcG/5VZ/teprNolivxZ8yRRYC +uiEwTgYDVR0RBEcwRYIcbm9kZTcuY2x1c3RlcjcuZWxhc3RpY3NlYXJjaKAlBgNV +BAOgHgwcbm9kZTcuY2x1c3RlcjcuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQB6l3m2Er/auUmVdUYtk9gWyxSKn2TcXSAN2CJ+HDZO +lPgTSn9h5j6xQ4PuIqFDre4PPN1ORuTCmvTSdGX4OzlJp3a0yrzKUA7aTt5SOJum +yySQKQ2lmkhQOL2VOxkcXaBmAHKTLtc7Pm69jOf+j3t2xpIRQ3lk7ErKzOQcMY3W +wqZgwydM5L46a/tLxST8/uYbwmgjhyha0ChnNFrY4L5Eb82z+K16luJf9P/xrSBL +VOxKRx0bIAdZ1EnugwwOQJUHXohcp5gIO2bYk9hsgmirTziSSPiuSuShRwCJx2/y +qDTXDRRG3ZDDPXaujVkim4KfEWTL4OOntwGpc+IwGQsJ -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c7.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c7.key new file mode 100644 index 000000000000..dc66a5d8e472 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c7.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAndp2/OP1fgE/1KQM9qGFbndhUlzJD0sUhRhCxzhzRbj/55ho +aOnn5iQaI5yzZTPJ2kAqAMqkpvl+3KNFfkjODWQEsopYXhj5NFJDWtWR6wjZz10r +HsoI4Tihrt+S4C7Yh0G6qaHzd84Atb+3mIpcAH57RumhQUh2hDv/HCm8E2vq4eed +LlxSu3ugsmWnVqfqg7p45YWGkDvIqGlb3xeeqMYnhmPbV2XREy9GyzDsjytgQjza +/si8lJNGvlj/KvfBLJqO4He6Rqy5Bsluh8hRvTjEWnvaMwdHngrYhfKse+JByjn5 +BnBK6tn1KflwgLdZnQLIEP2NqN/RkzTE/v9WIQIDAQABAoIBAE6+/KvUtpNY4TRd +cGAanJtKb6DsS5rW/BIXU7zZgtvN2c95fuCTi02KRgXs9qtuReycJLgHX1My1KAR +zSmd/Ti95AADNukeMTdyMEqgjy3+WNXy5v9xjzN3kmTQAI/Ynsj+t7VG5tOlz0Hb +taWhpBrU/f+WrgyIryLxYSb2BO50lnST3w0jLUPvgJHUTKfVnlcNn8XbBi1zg7R2 +t4WsOISNWcNLiG6MP2DgnZAC2LawHB4sefJzHID6vrBBzj28VFqf8Q7p7MdmQfd3 +/HKgPU/9wBGX4iY/Hz92l4pS4FzcR/tL4zX6YpfwI6BpkFa7D++CKSDSV+MFokNE +k6fkne8CgYEA3sL1w8RtbGkRNe0/iYOzhSv/FQhEwp6kP//5tlKZILkAHVmHU3x6 +vw1OhFpOzRKVxcLm7Ln55UgdSqZmuKoLmVjGDH4+Huuj0s54lG9FORYf0okRh7W2 +3oSI3ltPmahu0lPvBqKBStwFRcNRcckTtoxSDoZfVWBhyMRMNPcHP3sCgYEAtWgk +zM+UmvZUSODCctgr4kociMDDlwV+u+lPj+cdwD/S5Qby32e2qKpOeWsg9JuO/AiJ +mRlic04Ubfo5/T8zw1NtLuDpYEX/lRCg2NKL//VDUFvA72pj2m1MEMf3z8XzSjFy +Iy+aWaVT0EOuQdwhEr2MI/mZ31X92aGliiye4BMCgYEA3hXM4+IfnaeaSwM/ByBQ +iAJ3C0b1FnsHEe9ny8fJdoA6hwhNDITb80GW+Aj46ycOPt2zUGuRom8lFNmVK6IM +clO5D5eE41rWN0M1n/bXMrTGASY6XexiZrIHqneqgm0UH4kikqDM+g8lCopR1nk7 +uBFpdaWtwfaXRou91kg556UCgYEAnbRR+bOkTytCjrDGmqyPSgR/1esunkXqrVXh +Ewsq9A0IPI7T6Z/yTaLw4eFeHwh1P2n0pSJKUk9Yt1tHun+Ncos+afihKk7wKBjy +nEw95WDFCkEZ84kcEbHe5N/S8c/291MRFMG3f6OYv393KE5EelRKVqQRPvU04FQE +46By5f0CgYAQroAYIFryyLdlo+UV8qJe9LfGBJ3jl9Is3dy5EOTmhwFlNBipRGi8 +ErDclUd0jaLBfg2Zdw0jdeHa/peRwI+UYFG8HDHf9sTD1dfb42RNnemtNaf5inFc +Hw7w3YcdeNS04aYVeQaXKuUajvizCgb8eUvTYDMvzQKBBLVRnoZh4g== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c8.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c8.crt index 3e32ad646696..da894f4eaeb7 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c8.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c8.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUKgOBruyfZwrIdrRexXAJ8NwVvJswDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjcuYzgwHhcNMTgwNDE5MTMyMjE1WhcNNDUwOTA0MTMy -MjE1WjAQMQ4wDAYDVQQDEwVuNy5jODCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAkRgHBQ5c5YTSDcgW0Qm7ptHs0LIp1kqBmqFb0IBRSOwkymlR960ObUpGY2qf -xxEaaMWRsa3HEmx/5xVHirf1ozhh0LGQwI6ZqBjwDLUvpMGD+Lrl4ZzGIOq0GuMp -QkAF7/gV3316Eu6DJebZ7+REFChgBCeWr14jYrWUgsl+XeECAwEAAaN/MH0wHQYD -VR0OBBYEFNQ7qK1CaJfgGGralMDWPU5jCRWZMB8GA1UdIwQYMBaAFNQ7qK1CaJfg -GGralMDWPU5jCRWZMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU3LmNsdXN0ZXI4 -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQBXqo24 -seeiTtrmmzceAHa7ufi/F3OZ/aikFAa7ZXRgGf7bwJ7dSJNDFBsbhkRnvypWMKQN -Di9cgsNiIfLzrVAt8VKFfUQwIvw8MT9Uh82lJ9ZqdSR9Uli3D4c2WoIhz81w7CCV -cAu+0WiDoKqszaxiyxAt9vhi4Q+rV7KrTSgOnw== +MIIDTjCCAjagAwIBAgIVAOqM+FqobIhuJ2Jdykg46NR9hRJxMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW43LmM4MB4XDTIyMTEyNjIxMjMwMFoXDTQyMTEyMTIx +MjMwMFowEDEOMAwGA1UEAxMFbjcuYzgwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQC+/XP0g05K0aqH2B5hgHhpb1Kf3dJx1U5dlJMeqUZtz6v9Sl7g4EDu +6biRBFmkHJp86VCwA4VQGCqN4lJB1pUnVYU0C0Tn42k8E4CuMNOfoN6/Ffa1pN6G +jcfCWZc8vVOMpv0LO6I1OIILgjzVjmQ3k4FLQjgUzS8a0YVHZFmZ2IkiZZ4Z51Na +VPYOGayGX8Up1ymr3UfziaGi6p+4gLVTcW0UUl7JS+fQ2XbpHBnM/t4T5XVZxhjK +6pYwSxMGIuwHrzlzORVObem47SwxgAi1zK9Clhpe0fFxUP/Xfd8PMEsIGnqEWzKp +LNMaxWBHMuTEf4sdyISgioG+EIEiyY3ZAgMBAAGjgZ4wgZswHQYDVR0OBBYEFM1U +cXKwu2p0MMbF0WRcI0bTTOd0MB8GA1UdIwQYMBaAFM1UcXKwu2p0MMbF0WRcI0bT +TOd0ME4GA1UdEQRHMEWCHG5vZGU3LmNsdXN0ZXI4LmVsYXN0aWNzZWFyY2igJQYD +VQQDoB4MHG5vZGU3LmNsdXN0ZXI4LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAf6g+NPWoc8BeOQs5R4UUgT+dFj5AzJxblwjrzkJ0 +qJvCTyrT3B9WQQP7aYnglArqy9mwuMmPE21UYh5Kl9a32T/ZvK3kZmBVWJZjx4L5 +CG6DUPi6Y80rExtkUv+D4mlZFSlgJg/n5pF4docadrUWxWaC8IHMuSUursVU9gi8 +zHQY09m/CcVzerxaZ1RolHlEROkV0Q+gFzD4EWHFFMXTJzG4AmCOCSWJ1Hc0ntuI +lOjrT5vC+jTKYZnWWPX33bW9b5SfM0QufgNA9y3UCXLHAO9s/e+RN1qHipwdI/8c +zqa+EtHwsrvZhU/BAVvv9ASrdrDeanvKCHxMbIH29ybbBQ== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c8.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c8.key new file mode 100644 index 000000000000..de6ffa27d752 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n7.c8.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAvv1z9INOStGqh9geYYB4aW9Sn93ScdVOXZSTHqlGbc+r/Upe +4OBA7um4kQRZpByafOlQsAOFUBgqjeJSQdaVJ1WFNAtE5+NpPBOArjDTn6DevxX2 +taTeho3HwlmXPL1TjKb9CzuiNTiCC4I81Y5kN5OBS0I4FM0vGtGFR2RZmdiJImWe +GedTWlT2Dhmshl/FKdcpq91H84mhouqfuIC1U3FtFFJeyUvn0Nl26RwZzP7eE+V1 +WcYYyuqWMEsTBiLsB685czkVTm3puO0sMYAItcyvQpYaXtHxcVD/133fDzBLCBp6 +hFsyqSzTGsVgRzLkxH+LHciEoIqBvhCBIsmN2QIDAQABAoIBAD6GvIEGwcl11cXu +Z5fVFtaxttOWq/BPfH3fh8Coe2G09OISCHjHggqVYrK7QVucb6lIOv280Va06q2T +tAlQ5ni/o0G0FMVz6n5cBitA2e9vG61kU+YIyT80yHqkmQIuG123R613QvX4f36k +DyV9Hesh89i/H5wTxoGZqXADduAJ9pRWI6hs5AwgE4jiF0SQC2LWF94wVSoFkOFU +7Am7dI+Mdqmemv9N/7ybgKmwTVBkqcYdrmg6xjKr/5IPPl3KzI2kpYKRH3Awi+EK +oApBYLIblLOB43DpunDlwPIm5J9z1mS/PbyWMMsV6VGklY3c+lLO0sNaJDMz0sdB +Lfh3VecCgYEA1i04YvexGObrEs8IjMb8nuCWl99t6Dyv12NuQIb99h1O+5owkRY3 +0ql6+6IFe7jOLy2kc4TpRK3qUYGWl9A3A+I2D/yqMsvnBrdQQ/ht/hesKj2z+jjh +MoWtbHYDtOc7sqgbXOtYryzkdKr7VltQICvFETsPwppqPeS6pMYYtF8CgYEA5Ekg +FRUwFXZjnBYanSXMkQW4HdHBEmGAMLfqIHwSY4pWCIZsp8y1LXght16I1VmaWqP9 +7tfil6mAB52OMYUrcd/WcSPdNdAmCVqInchb4Zc+wDL5arSkJWyI1S3ZzLoZ3BME +i3LdeSGnOhF7gIv3eqcRi/EvHQ75HIoFJXQpqMcCgYAv22LiGl6kJdUZGtHaQwxn +o9+CiBZS8lyIUFtuZ7uPpvTjG40GnmRS2jP0zjtTbfQEbrp0ZZqKEiJtm/s+aPQi +FgXmhJqzRwSUZ5mz1msdf8Skm6z/mH4Zf+1EcwpY/eJFBXM3zA7A9dMGjr4iibzZ +s24Rb5UR8tcShW1/LBSRGQKBgD7HmWoS3JlaaIRZU9JZvcs4Hm0c7RLH+3/qZBmw +5UG4nnZTuXfgBQdPHT6XYdwCez9oEAJqHmeE9q+hdaRKsirr2sZcjY4qAvvZiO6T +VilQ9bHXkt30dZvRIEQ4CsiReWEbXcmXui6AwcNnYWZ1l4aEgX/RwRolrv5ZOpcV +PgurAoGBAMZ7r6975ROMV4prT6yaJDvClDrziBG66k9NQoNVIYrdVyoVntgKLFO4 +us9U76gELoMkm7cQjH4rBSoWO6w1B8E4o7yuIijaY7pcpSC0VL3n2QKEmGawSxaR +BV7QZKG+V754hvp7l2Paf5aX2dP9coxcD9dHhySeEmhl4Q9de1R/ +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c1.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c1.crt index 1b172d4bcc67..3663a6c30d17 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c1.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c1.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUQK7PM3T7+mYy8ElPam9Rp233WEEwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjguYzEwHhcNMTgwNDE5MTMyMjEzWhcNNDUwOTA0MTMy -MjEzWjAQMQ4wDAYDVQQDEwVuOC5jMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAgXuRBm6AhPl/n0w+6htbZqVgDzHBd9Cnp13KVWFj/aRR2v6ppf2/ZWSPDAp3 -zcZALQrPOnwlNcAlTdoFYbTNhdiciWb3+jMNSuwZaiNODykWoVUXeF+dHsWFmSje -QydkxPK8AfPwS9/8/wTvR2qjFPzIKYk/JPubG07c/BxC+QcCAwEAAaN/MH0wHQYD -VR0OBBYEFMLuWtBmdv4emh//95cK1tzKVxLVMB8GA1UdIwQYMBaAFMLuWtBmdv4e -mh//95cK1tzKVxLVMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU4LmNsdXN0ZXIx -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQBqyT1Q -NDOp5FDMKVowZqJKG5UqenIZ0FHk5YGSV+TJnLE6LdGLsrEie+zBirY/rWYH+vJs -Ao3HrA/IM1hGVPAZI2g7nFsokqOtTNgrm0/SSRuMSf1J82KhrG0cHcuXuMb0Z9PG -d8yP0524XXfbbwN0fT/AuEWa3hmS2NWXiJOoKA== +MIIDTTCCAjWgAwIBAgIUTAE53k1+p/Ugrtc+HOoZqiqc/skwDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjguYzEwHhcNMjIxMTI2MjEyMzAwWhcNNDIxMTIxMjEy +MzAwWjAQMQ4wDAYDVQQDEwVuOC5jMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAL5JyZGkRUqy3lFTMVs6tPbgmdiziLz9I0apRDt0+4yLeVk5iqy15TWm +2TwnJEFj+WE7MSr8KNZvDponEzvBFegTCuOPVF6BVvD8leICw/we/BGkCXK9897t +upNlnlaow6v7sRs7RTLsxZ0kTPQH1ruyQahcM6CJRBEDZRPN+lI/UevQUogCmn0z +wHLy+HF43b+BI2P9iw+0PGkaRCOR95oMxpKMbknoKeWbHgFECPMJ2evC+wH4jzp0 +KJzk5jYDHgsZBumkQhCuN1dp4dKLPGJbe59g4RPEOS6bBDSDVjE+0qgT0DW8bE/R +Z8s/nGTaZovlC8WyIxEjjga87YtGdhsCAwEAAaOBnjCBmzAdBgNVHQ4EFgQUpfvx +Xkzh2FhJKu+Xwag7JqZ8H0QwHwYDVR0jBBgwFoAUpfvxXkzh2FhJKu+Xwag7JqZ8 +H0QwTgYDVR0RBEcwRaAlBgNVBAOgHgwcbm9kZTguY2x1c3RlcjEuZWxhc3RpY3Nl +YXJjaIIcbm9kZTguY2x1c3RlcjEuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQABwI2KYvdWalV4Bl4VSKqeFi9VENM/3ZEvEDMA/y5g +DqbFEaRMFar7e02ZYQGVH+WC/j3r7MIod+xUhp4Llq/k4hRDgGWkaFxVnOJGNJe7 +0H+QooyBrNQy0elaJMmv6SlaK3I4QIBFq14Ru31KCRb9LlLM/ao8TlBNG+S3WP+b +Pa03Vg+cXUiKeBCE+pGematEoH9w0UvdDezkn+Vd8SIHGVkTnroNMUz+TL/FB1Yf +XVLbaZfxtgb8WtQzM8du+saV0VsYn+hEdb4WL9T904W8ntZILaVsUsCyf14RIvJE +P//GZcJuUIVlQgULqy46mk3LaNpOV8XWKAXnTdQeKVFn -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c1.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c1.key new file mode 100644 index 000000000000..aeba4b96eb0e --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAvknJkaRFSrLeUVMxWzq09uCZ2LOIvP0jRqlEO3T7jIt5WTmK +rLXlNabZPCckQWP5YTsxKvwo1m8OmicTO8EV6BMK449UXoFW8PyV4gLD/B78EaQJ +cr3z3u26k2WeVqjDq/uxGztFMuzFnSRM9AfWu7JBqFwzoIlEEQNlE836Uj9R69BS +iAKafTPAcvL4cXjdv4EjY/2LD7Q8aRpEI5H3mgzGkoxuSegp5ZseAUQI8wnZ68L7 +AfiPOnQonOTmNgMeCxkG6aRCEK43V2nh0os8Ylt7n2DhE8Q5LpsENINWMT7SqBPQ +NbxsT9Fnyz+cZNpmi+ULxbIjESOOBrzti0Z2GwIDAQABAoIBAD32zzGi+YFqn6SB +37eidV1TJCYByIwqLjuLURG/dU59+IUKjNLS5JHFBEZnRJIamRGFcexacn3paire +gI/bv2N0F/6kJMglfAc7aR9Q4TsPZ+viq8T2gBxe83ueAqKbkCRH8JM6iL0dhvg1 +4u0wREeYs3K8m38yU7zqixaY42O2dOFR/dgvTxJzleyOfcG/OAAhdcb1ygPB5u1+ +KeU8HU9Pe21eXe3vDMjVC2WvsOcI+P5bVSh4X+UOBFdNCncX3Tz2KTsP7N6djxAG +pJslZ5HprBGlenZIYKpEjqNTgdqyXt5UZGYJzn9kQT7WefNS2iLK02+eomOXySev +LV7db/ECgYEA0Kyyw/dF5BB5cJe17o8UOrP5UXMVSgMdBDOpZWUb5DaOrj9OWImc +U7E9TgsjW7PPjP3YHIghI8IFOcivTeTsEyLoMc490cuwyxl7u0ayLlAEXmfhg7Wk +j6PEqbejL+mrmJJyieBwY5GmJEf/gXI7vxcfwuy624GJ/Tmqmvq/StMCgYEA6XGb +WozJ6rFb9hpa0fUkheEbt046Fsgk8L7l/ci3qWCtEsLODbwDlw+HnVSzWlwswcym +RYJgQyLC3Kz3hSViwWMzpi1eKY93TYm16D5CdukAD8cdhs27xlkmBJmdkkAWTAic +jTwLKsKfU4zBHASLLfrlGCnmZshyAcJCbLDPipkCgYEAgMyHjoNahTlR65gIeFWh +09XwJkDVPBiN2WEVxnrQ61xtVsnGTsmpTtMQFtKzIU/r5Idt07aGVe7UkOT4pyiM +OgeKr+svpwWFeuagw2gQZJuJBsgxnr0H9IzNvz+UnucovgRuZAG+/QYSOUSjRT+/ +9uOL/CFn+EKNbFoinoP0fHcCgYEAlwafqjpRW1u5EgPE4/aD1XVoN/QNErYTaAko +xEI5yAO6lNug8TPE4tLyOrAUhI4DirG6lFefrW9sv9XLIMGqHVDvlj/dnmepmGlS +XVkCKOne11ZLXO324IDGXs1/KqH5iuE7XwgMdXKKH0R9noG2BV4/Hp3k6HQfhdHG +pYjSDGkCgYA+Ot4GzSsyrothknGda2XnLQDvyeOsDLpkboLvajizKiZshiiTjisq +bvP1h5KVd8XmBzopmHAt+8YNOoHTnVQa6FjmJkJdXGMjEo3q/u+b9rQRWfY6DvS2 +41xB9cVG2zcbP9sAd71sCjkHU0r66LxamLyQIlu93HCbWZd+k1I5OA== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c2.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c2.crt index 8c3fe5249b55..259cd2e81286 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c2.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c2.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIURuq55mbrZcWnTGXp61EVqnrGxngwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjguYzIwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0MTMy -MjE0WjAQMQ4wDAYDVQQDEwVuOC5jMjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAtfvCH7wJN5iSoyeWtfeJFIs3BrCtS/cb4LI41nam3g9T0Ag9WPhf1aPP5QBg -DQ1umY0kuZEhojduZHgZVoG7mNVNTgqSJCJKooKR+SrQ4gqaIlgIwSpKwo4u/za0 -4p1OqRhOJajOMCuC4zEz9yPt1YAfXCkYryBlne5m7Y+mVncCAwEAAaN/MH0wHQYD -VR0OBBYEFLpfZ8SVBLNw/xipDeUe27M9REyEMB8GA1UdIwQYMBaAFLpfZ8SVBLNw -/xipDeUe27M9REyEMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU4LmNsdXN0ZXIy -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQBODSa5 -2GRYIcw6npTyw+Ds3UhiluYMpYmopi+igLRRndBVk1Z9gCb4hSBi6MjcMtfX89dz -55MBnIMNCSqkW9orrPGKVCGZNpyxIkDqvgiwnm4vIURoHkujhX1UUCcf0Q+EMclY -EFdEaR/DV72n2uBT7xETbOJEngsN+cgy5t6Bew== +MIIDTjCCAjagAwIBAgIVAKfPuXVkRZmYoULIbNNTWluGX37IMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW44LmMyMB4XDTIyMTEyNjIxMjMwMFoXDTQyMTEyMTIx +MjMwMFowEDEOMAwGA1UEAxMFbjguYzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDR2fxTbZWOLAn0LHE3DQ14SbI1Dgs1f3vl8zgFr/Q9gwVmNuohKjyb +P08y9l8aRKc+RL0to6BnwZVIELwZi202YvpVcQ97oPLznu6j+6AokqeFP763vhfE +Qosb0Joq9uBYwbY5DKrH7ZZl/97rvh0BZjoBfE7IvQICU6kXweE7KoQuEK+bonW/ +27Co6J06GaUwOwZ/Yu+HnOu0qZH3SiqkShYrsUA4FGEQpiueFrhkMi1HuebCTQQR +JDJVQpxqyLuVVhpucEME2/kRVgy+cr4TD1JIwQKjtwe2PPe+GXGCxVGeN4OPjIRY +S9R9BNEvv6aQNSMdrbVAmJGxFcGgmIqTAgMBAAGjgZ4wgZswHQYDVR0OBBYEFGXW +KB1bRypP4kIuLkSbv6M2w5xpMB8GA1UdIwQYMBaAFGXWKB1bRypP4kIuLkSbv6M2 +w5xpME4GA1UdEQRHMEWCHG5vZGU4LmNsdXN0ZXIyLmVsYXN0aWNzZWFyY2igJQYD +VQQDoB4MHG5vZGU4LmNsdXN0ZXIyLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAW1IS2Xan05vs11P/9/NLnMnPoMc/MHM6hGXLu4r9 +qgSL2uGq0eEHmTOOioePcKseihyY9Z3tcaVJWrxT/4Ghe315bhbqnUal5wK6lRy2 ++lRr9OWHspWs3fJYAHDlDxuy91uXXLJI7TKtMmknZ5OvRNyjcgNfcTcBRFMyvKr1 +j5Oc6PFnRLt9n3hRYDdkhuBA9IY8d1A7MR+uXTJXTmWMK4P2u5+8IT+nGUknOAuB +sxsU695kyeG3Lmu2/SK0gSPH3cDE/D2EhCAGqvFvpLsQ2tYmDQTZTx0lhzS1eeZO +lMhOzKDReKhKI7ayLeWkCb1o62Poblc7q8GPiSs91gTWxw== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c2.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c2.key new file mode 100644 index 000000000000..9252495f56e0 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA0dn8U22VjiwJ9CxxNw0NeEmyNQ4LNX975fM4Ba/0PYMFZjbq +ISo8mz9PMvZfGkSnPkS9LaOgZ8GVSBC8GYttNmL6VXEPe6Dy857uo/ugKJKnhT++ +t74XxEKLG9CaKvbgWMG2OQyqx+2WZf/e674dAWY6AXxOyL0CAlOpF8HhOyqELhCv +m6J1v9uwqOidOhmlMDsGf2Lvh5zrtKmR90oqpEoWK7FAOBRhEKYrnha4ZDItR7nm +wk0EESQyVUKcasi7lVYabnBDBNv5EVYMvnK+Ew9SSMECo7cHtjz3vhlxgsVRnjeD +j4yEWEvUfQTRL7+mkDUjHa21QJiRsRXBoJiKkwIDAQABAoIBAEZEwQpKoo6lznt1 +uj9d3onN25+KYVR7qMg6JiM5ZQaH7fHpJ4MNepfdni6JcxT/siTWraYvqVG078De +4a2IE3znrb2Yiz+xiEjtiilc46dSXEfxLXKnc5vjUGNy+XyziDIQQ21dAX7K3tau +1D2KPnouefPvZQ4wxBB1ZSSwZjQCD61aAWSlYTiFsAX3yioBW+JG+0e5ytBQKHYp +l2Eov1KhoSnj+0G5VdX0QTIZJUB81+GD36cOe55FEkrJMgwUnoNerp/bHisdDXpx +XVTAORCdjwmofsArhZiahYD/G09vwQQ/PRSOWxQGVTb1bPlQdVY5VxlATlXUF2C3 +R4gWHpkCgYEA0sFCwjZDcp9ODVoKIIUu/40AMpNUAhJMfW9AEmm3po+XUkSn131S +LBLZJp9RIa2MBukuJzkiHdNe8N2/XETdLLh3OX43iq18wlLhz/bPctctMZZ194e/ +/F0qiwu8M8CNroqk0feE+wfMLwvMc0vNd4AO2riyTJ5TtKwampjot/cCgYEA/ucT +EmcxkJuaMgE8uD/8pfH2RlFTbtPmCzQofdpOgw/Ar0KWDAbqe0XYmgSosGq6iXO+ +uX5vB6SAM29UWG66hVsB9/i0lZlBmY6Oppuv9vzD+fPk00UwFG8VXPE2AanG/Uk1 +SLYlDe91wEf/l2eNLRVqGjcku5vz9eSCG6ndc0UCgYBHgPBAah6qFehqWUEUVXtj +kpgdkKSw6zpuWD9Zv+piJVeGvupfDxLBB0gLdYQWNAZNFfHlttmfWuZcPC+/g1Gy +6Ybxx6Padg/c8jfQ0gECCoU3zL+LUtBNM+LKOBoLSOoI7NsMrbi9XqDk2VDtUSuN +vrHLfVxo6FKn+rgv+W8DcwKBgQCG3tTkg91h2jL4CleKVbDkTOY1xooSteOeQfa6 +wGuSRR96nAywSGiA2TAGLjjjP1V7rNZZZ2Bv9sYucNV8BPqD3OdpwRornW9sRFWO +Kgos8Qjwk++bKZyK80umZaphrucu1TFwS4/Hk9AF8RJ3lb7I/++n901kRtwKWuHM +qVAW0QKBgQCqqW44MgNrH8m77s4PdkcdJH2pT4RgMFFpryqIX3MYAUwVr5YioZMn +jpwedzpza/rwDFMUvLmhQyp4yXI87ysITeqrwnX1XYOvkOteAdjrO4oe3fNkZZ4O +LqYNCWbZfF9W2OpqgQqMuSP7cZhNG66mM77hi7tUolUhjUh1sX6gzg== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c3.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c3.crt index 14582bd47bbd..b755e49d05e9 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c3.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c3.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUPGx8GdQgamiFY9WkFeBWsOMKRGUwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjguYzMwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0MTMy -MjE0WjAQMQ4wDAYDVQQDEwVuOC5jMzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAujTN5UqfbXzpFTjLfl3XOWsQEHD+d5VJIkwTcA6PxUL9CCJCapgylbJQ+nzd -pfOaUPZkSV75pLxfuimDz0Y8KtlVzEry0zhTgdMSos/ZVdQCn8j79W2/19HVBAMH -Rd+RDH6oNR3BPNLiW/hycXOyZUOBUtHQ3OMP7d+7dsqGMNkCAwEAAaN/MH0wHQYD -VR0OBBYEFFoGk7U4lpbzmRjPIEvIsZFoTYd7MB8GA1UdIwQYMBaAFFoGk7U4lpbz -mRjPIEvIsZFoTYd7MDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU4LmNsdXN0ZXIz -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQBsqZfT -V6gBfsknJp0g4vbZ4ipMOxBQpmEYUUFTUn4vWX1UAhw9tdfFx6p3yLwrPBBeuPT4 -WfL6goaR46aCAELLA9RvcvGGiWysAfPChdHY5ELdDN5ACOyLa9aCY6CMzoMqho+5 -v8F4i6Rg/hFT5Xl6X+DtJrevjyyeECIS7MkVwQ== +MIIDTjCCAjagAwIBAgIVAJ+XUJpxk3mz3LBHWK12tjA88jfdMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW44LmMzMB4XDTIyMTEyNjIxMjMwMFoXDTQyMTEyMTIx +MjMwMFowEDEOMAwGA1UEAxMFbjguYzMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCmC1dZeWOzmCFeXyfjInFjPU8WTIgIEqrI/9+3M5M2KXBX+OtnF2zk +1/aoQJIFgkaNmPXKT2CLZCjS2vmG7rihTSfRi9iEy1d8Yqh5J300Jx2Tgfq02BZO +xF01Kljk7LnWGLlSrqUM7E1hl2Fm0pL8cXefATtATMyUYgkvjgZBlWv2VugfZErO +w9EITpGbXbxtJ1a4tYS9CLXyb6XI1AWk2+D3UV2R33dykm4Cqcc8x51pOTKqAxQb +wPo4M9SDY68RZGpaWKGQLzktEhy1/oFQd7LoBbL6GBLImamINvbCVrWndcMxv4B/ +3QSs6WQX5UfEkvFsqs/yEAr42Y9T8TwfAgMBAAGjgZ4wgZswHQYDVR0OBBYEFM2z +9IgObMoSrNSJ8uy8ZdT3rCsFMB8GA1UdIwQYMBaAFM2z9IgObMoSrNSJ8uy8ZdT3 +rCsFME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGU4LmNsdXN0ZXIzLmVsYXN0aWNz +ZWFyY2iCHG5vZGU4LmNsdXN0ZXIzLmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAFAonW4Cn2JduGxqsI467CKuzECoJNrjq9LGD4tP6 +qG1oEEago81jlSzfa9ZddfTUKvUsDJhRrtLTjw5PcMMMR+KgBkQj1gLtA13gv9vE +nhAIBGIfHzHWjndelKGIGSkTvcZV+MRxEDkGdYVvsRLzEE58ORAgz4dqYE3VOF+d +kCHSv0Dm0FqptEuamrDAgSYMJDnDSZHIlG4i99imZ5EVjZbRW6FYgCst3DQntb+H +C5qSk72n/gwpc/g+yWDKPB+r7RXcRA0ePUQ3AeYVMAGeoQOnx2clqJezOT0ty8Ks +jHV7O4RlgfY7caDWDUdOSC+2kSMqjbI6qV+46PvFy/OFww== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c3.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c3.key new file mode 100644 index 000000000000..61c0582d3bf9 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c3.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEApgtXWXljs5ghXl8n4yJxYz1PFkyICBKqyP/ftzOTNilwV/jr +Zxds5Nf2qECSBYJGjZj1yk9gi2Qo0tr5hu64oU0n0YvYhMtXfGKoeSd9NCcdk4H6 +tNgWTsRdNSpY5Oy51hi5Uq6lDOxNYZdhZtKS/HF3nwE7QEzMlGIJL44GQZVr9lbo +H2RKzsPRCE6Rm128bSdWuLWEvQi18m+lyNQFpNvg91Fdkd93cpJuAqnHPMedaTky +qgMUG8D6ODPUg2OvEWRqWlihkC85LRIctf6BUHey6AWy+hgSyJmpiDb2wla1p3XD +Mb+Af90ErOlkF+VHxJLxbKrP8hAK+NmPU/E8HwIDAQABAoIBABODMq0fkEFbJ9S0 +vvW4pvWAD2qx7KxyMSK0uwAG7g5H2V1lMZioTpIn9OL5z48ZK5xZNM0k/dXzm76H +oBoktIq0C/Fai0Ozt6C04VJ3ewWlcRrUA/e7iwePmNp2EwM39HC98A1Q27GElWVa +CkawiThkRyfm16UrQUFQsf6mJIMRUSauDxsW73Qx95Xtxm4E1RjwUBrW8F8lQvef +vjHwmHbzYr3FPDeC6Bv9my+CBKbzEZqvYX+aWDRw6MTsqf2QHBreD4+0haA/8si4 +GtQhsonYfj59YA2CDhSZTsTjvn3uWZXftE/C+5sg1v9K32QgyclHX11FL68J1gto +UijLLm0CgYEA1Oo9OZjnaVrvgp8FLNv+jltoZejftPvyiQs0RwlsfHDkrwYqnMFB +UMLcdMDejHFjhznvkXMNJPjV+YyQqb52Z92Bk6X4mIcQITVse7rW9D+UE7XNoQdc +XzPuWoGQ+9q+9IUqAmNBPQTBY5waagpuMfm2E6QtlF0DZODrOl4+UrMCgYEAx6UI +J5Tc5M4t1GPBvqLXyddDOAprc8aM2ycQGOddv/b9xTD09azloVzvdPT4A/+8r9RX +u/C+7YgH9uuCwrg5yqsyUY1D4uqnfnVMhtRpfn0aEQLuRnjBIiH+cm/aeG5FYmzL +Wz34qKCa0aRVS2y+EpUf4/6NhaJUB4//1KYCtuUCgYBfwfXFSMqTLK3ZvnFSaqMD +HYNHikPKnyHGlv5tEIT1NsTzHr/dfmFE1E7YR8IPwhN0Rw9DCE6aGTwLHzgUz69r +KUlUwWmcF+qeNOvUfgSdF1cewj54NNpLiwyhMfKmAwys5pMtPbyil9fuV8/db1F+ +MoO8M7O4LVkQbsP8nVREVwKBgH/pH4lkmgZKhxht9aLzaHgcYCfWnTOnQXXj2C9s +hSdJ0xt1G20QImu5RH5VuVWukzDARpnWTC2Zmyi7iaLCL9zGxM11Scpn93pRIv+7 +tf9hncLKculZ0rvQtdqSCZJhbDj8ZWoHEXrQs4Cf3jod6MW1H6KO5BN7hoT14L5Y +wqJ1AoGBAMWkikWfPYXHA4cAAjHi5OWM35ILvkNvs0QPSH7AlV3grvZXynMJWpPl +SeplNpjnp9gUfjnHtsKXTVw+e5Tq214vpqLIgM/+hh4jO3q/qNS+bLZwTMe1Hudx +yC9lUs48+l+1FuRiApXRxyUsCjc7uA4JOFVx3Ddppj7cJ4q7+Zzy +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c4.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c4.crt index bad3f9a24f20..328ebea2ada5 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c4.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c4.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVALsTUZNLfQ0QhhChFKeFVPkVAJ9rMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW44LmM0MB4XDTE4MDQxOTEzMjIxNFoXDTQ1MDkwNDEz -MjIxNFowEDEOMAwGA1UEAxMFbjguYzQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBAIhWp/YCB3cVIrTK0QP6LC5zBZ8avn9isi/QeLXwxw69k+qhHEOReoybnPgX -yr2wyXLuL6PwyBMfjINDIEZ+N+4TsbYMsE8HiPNYQyZm1mpBI7ux2FbgCbpuJRfK -A/Pztz4arlBXcHG/tJKistvF+SSrw21t2bLtVZHXBFR0c0NzAgMBAAGjfzB9MB0G -A1UdDgQWBBSapaWx6oupzaeRg6DMkAo0HwOYrjAfBgNVHSMEGDAWgBSapaWx6oup -zaeRg6DMkAo0HwOYrjAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlOC5jbHVzdGVy -NC5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEACX/P -iZsjozczvsvkMhB8RLFVo29jjrk99GWKjZeYlmN6ntnmuQZNCgaCPaPLMZnEz33s -tN7i6DkigTkcDfsLnBzFHo9d6tpi5CeE0gCnXvSfFkAT7ZxR3hC8EJOquGPBy2Jh -A2L/CTwXmbxmEj/qXqMzsjJr2BDgNjATKv6za5s= +MIIDTTCCAjWgAwIBAgIULGlImaXPFbF0CMBnnucF4yXruGAwDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjguYzQwHhcNMjIxMTI2MjEyMzAwWhcNNDIxMTIxMjEy +MzAwWjAQMQ4wDAYDVQQDEwVuOC5jNDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAJ7hLWxAt9veXNo/CU12KKHIG8oHWdnPx44kl/yHweHRg3vRiE5PO+8s +KFwROVfD424Su6+GTBJy08M8epsaSrgshV/lRDHMd2234QZQbhazxgAxGJ4CM7pl +WbAa3XMAUnUT8WCug8rNq71A5oIFnfwA+TcjLtiJCT5TaY1cA391rv6d/ikQVL30 +Pg8/2spxE6hkFcOySwutUXrW8AHUN3cSnTDbdNGp3//RrHTMgLQzySwJKFBrl1C8 +A5XI1J+EL3FY4TM4XXkIJ7kcgtGPWGTxrMb0aXpsGgb5rO1rxRHxSj5JVH9sZHXo +aZLhSPHpCq721eP9dBSU7l9mlnB7FBcCAwEAAaOBnjCBmzAdBgNVHQ4EFgQUQ1uP +F6SOx+aUAQlLKu7vG9wwioowHwYDVR0jBBgwFoAUQ1uPF6SOx+aUAQlLKu7vG9ww +ioowTgYDVR0RBEcwRYIcbm9kZTguY2x1c3RlcjQuZWxhc3RpY3NlYXJjaKAlBgNV +BAOgHgwcbm9kZTguY2x1c3RlcjQuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQB2I8SRunyypVG9WOt4ibEXe5MKkEpuIHOqrcF71iwu +oaperrQdVtbB8mX6qIxIYVzJtjaBP4zJXf604bzabe8lzA5w6xGx+lxirNWj3KfZ +LEq6DtV05ugGQZYljOm1aSNNlTdv5eQYd0IgSz7QwliaCB8qspH+6tdEYWtmdQnf +j+9BNtfaIsgii8Q345ok8mNzhwHr+2UDAOjl37DHknFCcCIt+371ogk3kjJ7p0U6 +VUxtlvSSvnhzJQlmbzn/dOdUL7jy9SqKxC2nVeYVrPnOwhUwtYjFbx17/rHU6xQI +kl4zCvGmLbzG4YBmZ82ZWU/yJDuGv9SWPCi54XBW5E8s -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c4.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c4.key new file mode 100644 index 000000000000..6ecd96a040aa --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c4.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAnuEtbEC3295c2j8JTXYoocgbygdZ2c/HjiSX/IfB4dGDe9GI +Tk877ywoXBE5V8PjbhK7r4ZMEnLTwzx6mxpKuCyFX+VEMcx3bbfhBlBuFrPGADEY +ngIzumVZsBrdcwBSdRPxYK6Dys2rvUDmggWd/AD5NyMu2IkJPlNpjVwDf3Wu/p3+ +KRBUvfQ+Dz/aynETqGQVw7JLC61RetbwAdQ3dxKdMNt00anf/9GsdMyAtDPJLAko +UGuXULwDlcjUn4QvcVjhMzhdeQgnuRyC0Y9YZPGsxvRpemwaBvms7WvFEfFKPklU +f2xkdehpkuFI8ekKrvbV4/10FJTuX2aWcHsUFwIDAQABAoIBABh3cWjv7SBfyt6/ +wa1Qb+C8vILNGlTE1DnwgAifLgfvcsG45Qu3f0b3/t3Gt+pAZp/xzz4FfruB5q3J +0iZb0yaE50563kM+SySgoiI4dUDrvacT6HLqFeXeFloHQ75M+LsXhMlbC+f0P5Pt +EFJKOSeworA6ASHOteyyKFlg0m0TMkQu9Fs15FvrXHFqSKQ9X+WetMPxBiXNUdXC +bDjQ2YgUVeDd/gvRSbV295hjqhYC7ewnN+WvtzGXaLr3jwwn6AB5FL0YGJecZ+Wc +C/uiVfA0yVNrzZertSHGyDPWqufSAOQAyNgNrX/soHqoe2SNuUkEcYZiY3/0CvWI +2LFdKaECgYEAtbicAp0nm+4iF8AZse8xIopXXdecRA0uZG+w/LOG3WZMFMUSNJna +G+gu42YuLg+9uzCF4c0vtnhOG56SrGtzQ1mnZ9f0+fbu1higKOYTn+K+DXdHGbjd +gmcxmefbog5B3QmH5t4kuZsL4GEwzzcW22DQFLhpF9QgFr0HJq7nwScCgYEA39Jr +L6tFlIBQkESLbUWIMnOK3qGB7DQsxFRWw1vFAqX6/7YRo3iAtnxSJ4tI4yJZ5ngL ++pY3owVuCqxmh3wJ2YG4XBagqqqq1FpzxWLJlEXDD5EGQrS/hPn53ANRNfNEd2T+ +HZRl1rJ1gbq1N7XN2fBl2dSEk8tP2CTTVeUGC5ECgYAboNG3ZAVdo7rzOXWuo9so +kTfvQJS1k/t1sbWK9hCPILp4fe3iqXGLxKU8VZC32BTUnO/+AKA8cdYORPGv3TZN +Cxh0pVTNnEl7q4rOLQzWxjZ2/aeFtaGX7fCQobu2y8wfuw86fpO+1F/d4Wj2WD3V +ZMa+H7t6r/mg+2RW8UizCQKBgQDEAHFq8jJhhCiI3e23SC+n9rTCl26+GUamuzCr +rX6N/ioaqicyH0GgDKipBnlSpm9RuwjKbafVM9kUsbqrpLy/Y7C9u/cA3anxgjBl +x0e9d4YE6LwxyDT7Tyk2ORVyCFGJuMAryRuRcwAiFg3B9oyI8NNQeaGMB3wpTL5h +NskfcQKBgQCD8q3U1wG+VibYn3ehoa5+4F0OkinFrWL+axtrTMNI7rcqKOZlqSmE +tQfTEXc0A2zpzbOKiegfRn0d85tD6CwjmYjXoXNUGFPg+bzNBX1cffEafgfv/RAH +adMmqkSHGMaobLdETVgQsM4yd5GHXx2ngP+ywwDWOQUrhp1VXiyzPQ== +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c5.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c5.crt index 8124b32c7cb0..cc36cd053562 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c5.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c5.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUDXZKYe8N8urRIiGxYze5nudE6ZswDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjguYzUwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0MTMy -MjE0WjAQMQ4wDAYDVQQDEwVuOC5jNTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAkcBH4VLWVYKTBK6qDEk5PLnxZVyMiQXZHSYKzaOYJONym1lXYkvpXIDcbzwy -NyFrMbkRu2OX7gJvDJ9RXcZHXmwJMWrYSIYz2wLO56ZJyae/51Hi7Y/3Ov0QpIr8 -Jw0EVt/1UZPhZO3l5tiPp4hWPPFJO3s64gULPTfqcv3vkPECAwEAAaN/MH0wHQYD -VR0OBBYEFCt7NDuXp23piqYrueIProkyL5uqMB8GA1UdIwQYMBaAFCt7NDuXp23p -iqYrueIProkyL5uqMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU4LmNsdXN0ZXI1 -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQAz0nxM -esENmZpDl9A5KJascywHWPlNHziEzwV92IWXZTNts8CUixW3VvhPypWMOWOdvo7g -bI6KQY82WgFT3GN+X5M+qgxLh7R13mRyOW/ncxbc3dpZhKurlW1bPKhchSR57WHr -1KSKw70C05tqEbMl3CweZ51EwMDq/1IqO5gpuw== +MIIDTjCCAjagAwIBAgIVAJ8C8GcCcxCloFQSscgM8PaIdkuiMA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW44LmM1MB4XDTIyMTEyNjIxMjMwMVoXDTQyMTEyMTIx +MjMwMVowEDEOMAwGA1UEAxMFbjguYzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDkCUBbRjghReUrCo9FcCXqKWLfLqI/Gi31YuFiaa83Uqk8S/2T0Y1G +lgjHSJvwvE8hXCwlxGibs+gOXnKtS+S45JBf93H+2O8H6VXsVHNQip8Na3QiyAYB ++t15jbP+9VxrPz9BPsiinnkK8tvyAWRXC36F4STRxWT2lLQZ7j0gk54PCg0uevpF +kmDeEdcbpppon0se099e8HbCZZM0x2N2Vra1pQwH2yttO3wRaIjLHpbPvHJU8xsw +ZrxuxDb2fQxRJdXb9+PWPtTE2TJuC1meuUBZwYedBL1rT2mtKHKAVcXWjht69B22 +DLcpniRbgTqFQL65goXuguh0w10Vpj7XAgMBAAGjgZ4wgZswHQYDVR0OBBYEFIi9 +2+E2kl2kLgEleXT1tAHTuBQiMB8GA1UdIwQYMBaAFIi92+E2kl2kLgEleXT1tAHT +uBQiME4GA1UdEQRHMEWCHG5vZGU4LmNsdXN0ZXI1LmVsYXN0aWNzZWFyY2igJQYD +VQQDoB4MHG5vZGU4LmNsdXN0ZXI1LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAOAirQCQ1iJpJlvosQ0bY2qUo7G5Wzid4K4GX1nKQ +K5opmr2lpi5E2pK/CDPzOfw8i3a7K/oqpS69O9gIZHxGtWypkfr+v6vWMSsQBUHm +0jC2q+n1LK98nIoS5uFax6W7da0e+lARd59i8C7BItqgb7+qd5MfyUwt4Gv+04Wb +fm3CpD2kM838U+/LuK+TfAcq4pFApftSyFrPODaXKxiN5QIBPGorTAVHvuumyOc/ +6s6NOqkkZDLZo51W6bApfgo693dW+RH07kucQQYyG0hyb5W5M93KbqiW0QoAAXXp +eNvVk45ZS21JnkUq+ZgQeSh/tTua3FgD3qGebersyIz4ww== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c5.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c5.key new file mode 100644 index 000000000000..132e7eb70824 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c5.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA5AlAW0Y4IUXlKwqPRXAl6ili3y6iPxot9WLhYmmvN1KpPEv9 +k9GNRpYIx0ib8LxPIVwsJcRom7PoDl5yrUvkuOSQX/dx/tjvB+lV7FRzUIqfDWt0 +IsgGAfrdeY2z/vVcaz8/QT7Iop55CvLb8gFkVwt+heEk0cVk9pS0Ge49IJOeDwoN +Lnr6RZJg3hHXG6aaaJ9LHtPfXvB2wmWTNMdjdla2taUMB9srbTt8EWiIyx6Wz7xy +VPMbMGa8bsQ29n0MUSXV2/fj1j7UxNkybgtZnrlAWcGHnQS9a09prShygFXF1o4b +evQdtgy3KZ4kW4E6hUC+uYKF7oLodMNdFaY+1wIDAQABAoIBADIzVS8izL2KcRJi +HztGnponpUQDtKUWotJ4A60SHggcUvRauEa/5Y2J/wG8GtRK6FKsQ23HCM5qMyZi +pXVAFWicrKnrw/U6FZkUxZ0i1EYBVlVyd90mUqhB7VrSlrMEmeVkRY0cALhIEdaW +s1PZ+ZhuB9r6PwxNG9Zq0yE4QM24Ny1+Co47qfXEt2gc9yKBfeNsE7r9cyAlTLmY +xlCXUALm+mu6BHrR0fAfoiTfWeqZZQRcVLmpos9gccJXZ+NDh/PSgcN1s6Q+NyEA +J28M7ySMEUrLrSFtBQFCuiVkMhVtBJ0MLTrmlaKhhJ/0TvOWGki6Y70vmxvrd4GV +Blq/RgkCgYEA+cwSpg6y9mnS4KAv6pCkS6M6jyK9u07/9Z5VLFqqc1wKa8xLz+3W +4ssChDSzf/gwf3jx7j5tPcpqwpZrlPwxcwYH3eaEStvb8CrY0W3Q+cNhmdEr6ou0 +ewvyJ1Zq1o4EjlGSMX5FQLIOu3dwt4bLsqoA3un0opyhrUuCFBh2KB8CgYEA6bLY +vZOOajukS+QywsGzyviRRxPwoNOlpTB+YUYgcMTMnFft+mhi3OuebpGUYW3PmufL +/v28tjFp2kPIVdgzrRtp0DaLruAA8KngkvtXiHkQaWGzAfLfByEq9zoRwEryOWh0 +oTD8iV33+zDX6PydOU6nF0/zqWT60bDKi0sJckkCgYEAqLPnYFkLCpp7vhLRaaSY +ITEzKlo3M1puSY7OTS9KcLCXpSgLh3lEqFSc9Xa2R9XxwxWGpvVoR9P9+D8oly0f +pGRNSdMWQ4xq5K+7UO3Ote/7aau38ia6FGQD/gdCDndJomh6yIxT7xaSFo/bA0e5 +KIfiIKp2+p+4HVaimjnPCtcCgYA+4ZKUH/n396hTi0llQf2ApbVhbKoYfBpcAlpf +DiF4dlufzcEI2cGQNA3jLmTAem2b/rbTG68FTvJuekKTZqMw9ulXzvVyzm8yUmNM +6Tg6TmjuEg26foEoR/Jpp0RTe1cFv2dyX93zkEdDXXcbB62+V7159ldXwwYCGhHF +vSEDkQKBgANovyaW5eX4S1qpyUByLuENM3ss6RkMDG/L/H8kYTgAKG6VzaLMwb9b +WKManFbKmrXWUXmOL56p6/64TZMGC3nNLYQbHLKP/kYMstmB8L8KJs3OxmzW1Gg0 +owFZjrriIU0amWhMXPtsSCKlIlOOCiwyPhsSldsQCQiFVHV/mc92 +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c6.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c6.crt index dc1bd9c514ef..bcfbb3a90f3c 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c6.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c6.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUBntPQqqFKoP/ZsDltxJ/FYW7x0QwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjguYzYwHhcNMTgwNDE5MTMyMjE0WhcNNDUwOTA0MTMy -MjE0WjAQMQ4wDAYDVQQDEwVuOC5jNjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAo8BTGj7hNOsuZUJnVaiuK4LzSMpph4N6WPUXUC9Pt5qN6nUyIOhCCMpaXnqf -Yl4mJM9Tj0NvDwh2Ar5Kp7O/UIStXHxVOxv+WOq4XTLNZPOOgw8pr9i2BDwp92sE -xydrnRUX/u4hW+/Fssiw3WpBCGiDb0JJerDLDhGMBVEJW8sCAwEAAaN/MH0wHQYD -VR0OBBYEFDwuoW6Jy/OB9E4Ztw3fXlA4PjrjMB8GA1UdIwQYMBaAFDwuoW6Jy/OB -9E4Ztw3fXlA4PjrjMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU4LmNsdXN0ZXI2 -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQBJ5uWB -ctpUMiTKasyyio9jZMMsezuZGNnpOb6ZjwSfX2xbyC3KvNS2Dcb5ajlLaNuRLdqd -dz/IGzI69cl808fUqsFa9rGJBQvyQQBbtNGE4knQ5pdw3cqJEayMfaliViY0CYya -t3CmJIB1UZ3/zvbMDhrb8RG+/xYvcagCRpchnQ== +MIIDTjCCAjagAwIBAgIVAL4ywdFEQ+TioL+txBKJkXuWgMk1MA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW44LmM2MB4XDTIyMTEyNjIxMjMwMVoXDTQyMTEyMTIx +MjMwMVowEDEOMAwGA1UEAxMFbjguYzYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDML8+2lJviVFFKUv+xjzZVxxZMvpUOgA7OboKqs/8mKh3w93eHiSQy +MJm6TY45xvS7nXByIKFPVU1PjpaRkMMO+h6HaAUlixjHEUaXuwOmmpVHbLg40oWM +RDBOSCc2nGY0nVSeNQdQMMNJ/8uvWR1NI1pmhEz1ErvYEITYd6pPs52xQsCfYyYV +3tTtJXh9aOjZxKwfl+H2cd4x8R7IrDp5j8vJ2akfJJg730SvIGIuOPMYnsYZycy9 +jKP+ZifYu5VwcIiCVC97TmyAcB5rJ1pewJADJyRTKJpB4btujdJJObyJXafV7nUn +T+7OSMvPefUGbWJFEAr5GnRqqzsn/bn9AgMBAAGjgZ4wgZswHQYDVR0OBBYEFHGJ +dMu/OnZO/kgD5skdYCMn0A7pMB8GA1UdIwQYMBaAFHGJdMu/OnZO/kgD5skdYCMn +0A7pME4GA1UdEQRHMEWCHG5vZGU4LmNsdXN0ZXI2LmVsYXN0aWNzZWFyY2igJQYD +VQQDoB4MHG5vZGU4LmNsdXN0ZXI2LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEASW6tzJ2tsGlVz7Y64x1BI3JrW89sBN5eZtYQD16K +X+j2fkzR/4KP7Szs6+0/fyyvvnamuAujlliZ3SddTc30uNUYJ7sfDz/W475HXaQe +MqMT78oIO+Osmc8eHEdi8Nv8uR1eF/aqkUfBMFRhcnT3I5hNeBWQGc+vV1AIwB7v +HKGfOjAUyWdQ/s1roB0g7HZFpwtWnfTw58U9zDkqjsRWL3PfUcboJHOdx6h1bbY7 +AwqEZPcM6XZ0u9/cBghM8n9A4Skv9RQuU/uLd2XtI9fHbPi4DUj7Qi+SxT5zHCw3 +amhvhLk4il8tT8/2Q586JvOpClWqVKiiZMKpQzVGvASBFA== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c6.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c6.key new file mode 100644 index 000000000000..906b0b5ef9d8 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c6.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAzC/PtpSb4lRRSlL/sY82VccWTL6VDoAOzm6CqrP/Jiod8Pd3 +h4kkMjCZuk2OOcb0u51wciChT1VNT46WkZDDDvoeh2gFJYsYxxFGl7sDppqVR2y4 +ONKFjEQwTkgnNpxmNJ1UnjUHUDDDSf/Lr1kdTSNaZoRM9RK72BCE2HeqT7OdsULA +n2MmFd7U7SV4fWjo2cSsH5fh9nHeMfEeyKw6eY/LydmpHySYO99EryBiLjjzGJ7G +GcnMvYyj/mYn2LuVcHCIglQve05sgHAeaydaXsCQAyckUyiaQeG7bo3SSTm8iV2n +1e51J0/uzkjLz3n1Bm1iRRAK+Rp0aqs7J/25/QIDAQABAoIBABWqDv5exylc6+EY +bFPBD7FLNdT/tZ37vVZuyENnZK9+cX3TK+YACYyftnfbabc8olPTsQ4S4zqhINpL +lTMtqlyI8jtIsJ8aoo7giN5pc8o2BZe189fnkk7fH0b3Bd4X+wojFUrR8owyGE5e +KxTYPjEkip9s8yNfkPmsSwQSsknRYt3BfEoqwp/Blqv8Crf8QQTyQdA+H2yUgZQ8 ++MvAYg8pFv/uoO2jSl/Q4QwfcYKzG4qBD1Jf6oYSaH7FeD3nWhXcEq4h7zlFaajm ++fGyJD5/NXQ2TRy7EY7f4F4j3lVRFfOdsqfT0+eY+Gdfriff0jKqKrXDheYwrVBp +kEBuetECgYEA7xnc1UDC/ouXUwbZDqk4HQF1+1ZF8cVscfPu/tQpSMX/RjpSLdA1 +6H7xmCGvEyRw/6L1UkEOjpiw76LPLhpHOdH1ktDJ/bQyHGmVoS83F5r/oF5LHahX +yqs0Y/i4sBL1E9SFyx5w1Q5/l95xztdPHvpiI84GhMAsmNyVaglrALkCgYEA2p47 +onMvfJ8N2zGwlQzCZ0Gb/p2MVzaszQXAaDLCwNObfKs1ysjET5IaImaCkg5zzJii +JqVYj6xmF+bPivgkSJPQ9d66K8TqPuJ1lQw4yl8LlRRdn/MPdgkoqpJf6Zm/fFXx +yf6xhwJz5QP5nM3UbSPxRmILI2HKxZdLMatneWUCgYBeFzFeAjQ58zjDhYhZSqI4 +V+DT3Wl88RHx7ep5AYiViMUiH8VDHhJ4wavVEDxTLh4Bm2Eq00slNlU6mDkrjU/G +IyeedOdoHUczAyHM4kWt1mMN+s1YiGyz5pehV7lmIK+s9A8KlZoVXbU8UF885GHQ +cPu511OdXfqu1O8/YSFokQKBgQDXBM5Ohs/r2Fw+fG7bCkOiXytYkAhaNxJaTA/j +7BkUoovJYHN10WUThfViZESD9ub6Eh1ZuUKTfNKlK4RxgIHfSFM7oCanqfNiikVv +pSeuEzFHHVLAYMWSKIvwyCrBjVCCr90x+OkOiuEGHJTDHsaR2coSmZeu4x4WnSl9 +3sHU6QKBgAywmj7z0V0Jbb8Vuo5XzjCyA/ICK9F/TrMSgeq0/Vwf8Ya9pVy0eUz4 +ZQKFgO7TVjLMhbbFZ0KEvXeYr7GDgNrBbl2JdnHBoH/Le5/iDlw7iue3zrtH187x +mblWcwwAQ2K2gB+CC5DRQrAgvsUwZi2von2AjD7G0XeWy4zNJdlR +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c7.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c7.crt index 6bdf74aade91..e854f4056d8b 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c7.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c7.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZKgAwIBAgIVAITsoW3WKpWQHwqyFSUAIXdectrZMA0GCSqGSIb3DQEB -CwUAMBAxDjAMBgNVBAMTBW44LmM3MB4XDTE4MDQxOTEzMjIxNVoXDTQ1MDkwNDEz -MjIxNVowEDEOMAwGA1UEAxMFbjguYzcwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBALgPqDe2FNs+O8NC9tcUqFEHNaey5ax022GQ5qAiykKoNCFD+YLxDQW8xNPq -2/nJIaQhzymUi/1KdPs14gDw6HXmSUXO9juLv413PQ3B4aVfsOKG09JmUAxLbcoA -Q2PrdFichWWaYEtudF4RzAOtMNGbj+n4QL7JkEAxg0Zl+9NlAgMBAAGjfzB9MB0G -A1UdDgQWBBR2lHOcDOVXtV6xMd2mHu91ksElnjAfBgNVHSMEGDAWgBR2lHOcDOVX -tV6xMd2mHu91ksElnjAwBgNVHREEKTAnoCUGA1UEA6AeDBxub2RlOC5jbHVzdGVy -Ny5lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADgYEAC8Ta -BWlhJWv75fq+NPy5glqnSgfAGCvNpjRYcxFbkxSUTZkFq5mTtu0J+Ckk4fj2IzgP -lF+NeNxaWmzoCfsYOLKs2+pChor3eaFEckVJThDdkg17rYMa5Ym/T/Ph+hXWoI95 -3xm1ZZbCVJhdaZzFcCnSghji/g4aeXlE6hLwBmk= +MIIDTTCCAjWgAwIBAgIUAcdRKrzoeK5IlqNcp3OX2Me5uswwDQYJKoZIhvcNAQEL +BQAwEDEOMAwGA1UEAxMFbjguYzcwHhcNMjIxMTI2MjEyMzAxWhcNNDIxMTIxMjEy +MzAxWjAQMQ4wDAYDVQQDEwVuOC5jNzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALeGCDYi+SHv46zoXEVNRijcQFPsv+LLnenMBOrSOF+0s+gIUXMQ8ohG +TNdOgUXt5opDi4XPDp/00Q51+nE8j9t7oLBG4XLMNXY00lFSgRUulblJ2ZJPnEnx +jvjtZinBd46CeD+iZSNjEvBLgqvNVSiINx892VX4RARnsFTfrnlFv6rzpHgdkBAi +9hOWvWXRhe4/r3wMu3U/0fDn1SadFEQlNzjm/skQeDLkhMHXWH2ELppJCe72Q4zf +bBBIVvfi8mmE0ThqAk3Tjq/d3keMrOmAcF1IrcMlQ+bM6u3VQ+Rk/1gxaRyIbkUr +Fi42PgqCN0AzPygyA6ve/h3JT9lfthcCAwEAAaOBnjCBmzAdBgNVHQ4EFgQU5MLK +D9bKWe6x1NxJlhtvHcwC4mkwHwYDVR0jBBgwFoAU5MLKD9bKWe6x1NxJlhtvHcwC +4mkwTgYDVR0RBEcwRYIcbm9kZTguY2x1c3RlcjcuZWxhc3RpY3NlYXJjaKAlBgNV +BAOgHgwcbm9kZTguY2x1c3RlcjcuZWxhc3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0G +CSqGSIb3DQEBCwUAA4IBAQChQWJ/f1A+QDqAKvhcRCHgyhuhSONo4e8qwJuRmeHn +F8KnBnOhirw++eTEuAhRf2H8k9R/gmsPB/3pDs5IjvM9/TUOIcGqHfnZCd3P9SDf +nkZvrDf7uXt3+ni2whGbfcEZZMzFSNCtEZOAjxe8xfT8BuKpxAiJd3PzVE221XEZ +DO/Wq8hETKWavUqKaHSR2fkhIeRZXG/hzHrB12g/X5tKLxjN5fezC4KsI27sJc+y +V0TVku8jYhccqODC1EpiZL3E3XmKNFkG5dQAmbyh15uBxZj1vgZ14JAjBaKLi4sw +s4+EGZMilaMhfrqhTDkCQ4W8N1GH0STZIywTYrD/CsGE -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c7.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c7.key new file mode 100644 index 000000000000..c3e48622415b --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c7.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAt4YINiL5Ie/jrOhcRU1GKNxAU+y/4sud6cwE6tI4X7Sz6AhR +cxDyiEZM106BRe3mikOLhc8On/TRDnX6cTyP23ugsEbhcsw1djTSUVKBFS6VuUnZ +kk+cSfGO+O1mKcF3joJ4P6JlI2MS8EuCq81VKIg3Hz3ZVfhEBGewVN+ueUW/qvOk +eB2QECL2E5a9ZdGF7j+vfAy7dT/R8OfVJp0URCU3OOb+yRB4MuSEwddYfYQumkkJ +7vZDjN9sEEhW9+LyaYTROGoCTdOOr93eR4ys6YBwXUitwyVD5szq7dVD5GT/WDFp +HIhuRSsWLjY+CoI3QDM/KDIDq97+HclP2V+2FwIDAQABAoIBAEPICA5eml8TZ6Z7 +52BdKvV6a3PTtAdCpt8r0AEehV2rsuqW7+YsszbrqAO5Zft64zPbwJM6Jy+izvel +UUszT/qDTZTtiOTKUVCIL8mDTFOTcg8OHIJyTsnsop6Dp8DySILk4x61jt8j/2Gq +sedcHgCrVph+Ul/zIsPxu+mM97aoEEkMkkNXed3A+bxiK374ibN8q0Mo97CHRIS9 +jEbrpSU0wUFjeR1tueoy9EYJY+zsFOctnP98LYVSiN0mde/ykVWcJpqsBiMDcSBq +2RAEclqMmQplhT9eQONUgK1kUXCb8v8V4qXtFVMM8JG6wgPSCbjOMAcmSD/DBLpG +p0oaHgECgYEAxN9nGNRHOyiK+PcYk416Rk+vLGrDhvsad8uVJPhThRQWrG/6Gr9b +MjfYoLxH5Af/JtUqHTbe8paOlGCZXDXo4vb38aN0dAtoX/rhQn54fYoAh5NdFu85 +V3lb+Ju4myuMzjLcMfE0tQY4UwVb8k1aXQMGrTGWsoAvMfKcXxWMAQECgYEA7qRH +1p3ScqyyINLh8RoCRWzvu7oKFkl8E9hIyeBXM7xdCOU9tbGI8cBm7/Own2f+EVQI +gvcifBu5QrjW1IfSo67yUbk1TR6UeJAeMaHd9M/c8WtJtjAUEY4IhsK32OiXrjia +rE3XJyoL8tdWZrPQ85+xjE/eHOmdEn+EicksnxcCgYBHaGzBscWAbK0zrpSIeImA +VUjJiJQRMqUgDIS1WykYHPg4ZYlHCF9wsJbvfjtpN4lRsYHMBOoK/ZcLtJoLuzYU +lNXMHKiRqs2Z+qqX7r2Pc23LvyTFu9GFq+wzGXB1P++bnxEFQGMIwg/N9SSrl2kO +UZmrWlA5wIrdyAz15B/bAQKBgCFL2pofOxW08sWRpuS09Ewrfud6dZ3WcvFx4/YG +zyPwzCpJkeqf8rOIXNRt897J67EPQIDFapJWa+rbT14n9UPRTQ4KAV3ZPm3z7cKP +CyD1P6aPiaFZ4/LvFlF3KZ+iFEfCrhIMalCUlUY0tWFvBWB4j5U8L4lpLvk9wYhY +AVnRAoGAfSJeiGIcZ33paTjlyZanTrc/0NDA9ZihatBoka4u+HuRfUxs+xScNyxf +j1LcImZpM/raVLbTLIF/ao1CXUwOHw9lKBIReDIs1zTFd0Ir6E7lRd+N/DNKWSux +q5xg4jEnemiWPJd6jOtM4hC11tLmSvYV9Tw9kn+d0fgu0vBEqv8= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c8.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c8.crt index 80beac386fcd..44ad4dfb481d 100644 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c8.crt +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c8.crt @@ -1,14 +1,20 @@ -----BEGIN CERTIFICATE----- -MIICKDCCAZGgAwIBAgIUWHiDGtjFMmyQe5o/E90cc8TTAJQwDQYJKoZIhvcNAQEL -BQAwEDEOMAwGA1UEAxMFbjguYzgwHhcNMTgwNDE5MTMyMjE1WhcNNDUwOTA0MTMy -MjE1WjAQMQ4wDAYDVQQDEwVuOC5jODCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC -gYEAwvRhMUXpnmopSSajgAAcImV5vnRLUB88duRl/j6oM0Oq4xxPoKg1HfSjTnJu -Ld3mrMuTQPU67KodJoZ1keFoeKQGH1S6pDT+GBI+8IcZMZRJXyeourDB5AklqRVK -PiciepoVCHuePYZKVlsUnWbH1Vg9yMcYW6Jx6zbnS/meIC0CAwEAAaN/MH0wHQYD -VR0OBBYEFO5ttLIFOaMQELSrMcKok9nD4PqyMB8GA1UdIwQYMBaAFO5ttLIFOaMQ -ELSrMcKok9nD4PqyMDAGA1UdEQQpMCegJQYDVQQDoB4MHG5vZGU4LmNsdXN0ZXI4 -LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOBgQCoZw5U -6LVuXhAIKtncNCTbxfjmeL5JG4GsKb1lS1Xwb5WedO9uAusIXQS2SuLhKvc35EW3 -TJgkDlLfSqtRCx53fwm1cw+HmRRzpmMaiKaJ/K1tdhv7r3bvH+tRVzFHxye2Sai9 -U8gIueP6v/5/T/D6MobHsyXDL9l/flHHdQtsFg== +MIIDTjCCAjagAwIBAgIVAPVxpd2FAVkJ2f31em0UKJkPQL/0MA0GCSqGSIb3DQEB +CwUAMBAxDjAMBgNVBAMTBW44LmM4MB4XDTIyMTEyNjIxMjMwMVoXDTQyMTEyMTIx +MjMwMVowEDEOMAwGA1UEAxMFbjguYzgwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQC14yzjbwwpjWZ5LC+ch4pq2t+UTfzuxObdmHVLXL+a5WoDnid1HCUq +FitVU2AUs9aaoo/h+eODtAY8GgL+r4E8g4OtQLD8ZXDm7KSXfWDXuUrNOCEHPqbJ +6BFPe7xBKf/zho4om4/32EP6uWfzNUKTNCxyirOm9EnKKIMorfQyhQ4JmWxxEgqH +cxdRWF/hBF1mHadGuAfhso2LptskKumXktSX5EzF80efx7P+OaPYu1mBlO/+LEoh +CktkdqRR1En1R8//9oYd7yUOD0pr1KhTa+6m41NIUmIu0sI3JeHwlpotzdU33vTO +VCJcd9o9L6yoWqSpU7LOvVnbsby5omMRAgMBAAGjgZ4wgZswHQYDVR0OBBYEFDPd +ku4cLj+JnrvGDuMqfGONuJUsMB8GA1UdIwQYMBaAFDPdku4cLj+JnrvGDuMqfGON +uJUsME4GA1UdEQRHMEWgJQYDVQQDoB4MHG5vZGU4LmNsdXN0ZXI4LmVsYXN0aWNz +ZWFyY2iCHG5vZGU4LmNsdXN0ZXI4LmVsYXN0aWNzZWFyY2gwCQYDVR0TBAIwADAN +BgkqhkiG9w0BAQsFAAOCAQEAtEMjPqElf8dXxypYMv5PksEYtsE2Md8VoREFjieU +wpW1jnI5WxBo/Tw4CjJwqnv0Uy8XTL7JLTQbbIR3n45ZGu4JW8PGlnTHDPBBLkEQ +uFDOs7rWpXgKW9PuD6dH4yRXilnOi9P8MRtk+9mT225s8cu5fLxwsT4A7Wpx0F1m +XV1mxv971t0WOKIK45rEDnnZWfsivWvU7WqqCSnl2VULKm07qO/omoUayrVM4YzB +z3GP0DVTwEKzwrmIZX742SEnZF1oZ82lwuoB2kBESMM5Nr4sBwk4WBACSqKvPtRy +DDyQDV0GZBeENCe+ivTJPej3vAPVPvPN2I6KTnh/43ytAw== -----END CERTIFICATE----- diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c8.key b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c8.key new file mode 100644 index 000000000000..463f3fbdd303 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/self-signed/n8.c8.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAteMs428MKY1meSwvnIeKatrflE387sTm3Zh1S1y/muVqA54n +dRwlKhYrVVNgFLPWmqKP4fnjg7QGPBoC/q+BPIODrUCw/GVw5uykl31g17lKzTgh +Bz6myegRT3u8QSn/84aOKJuP99hD+rln8zVCkzQscoqzpvRJyiiDKK30MoUOCZls +cRIKh3MXUVhf4QRdZh2nRrgH4bKNi6bbJCrpl5LUl+RMxfNHn8ez/jmj2LtZgZTv +/ixKIQpLZHakUdRJ9UfP//aGHe8lDg9Ka9SoU2vupuNTSFJiLtLCNyXh8JaaLc3V +N970zlQiXHfaPS+sqFqkqVOyzr1Z27G8uaJjEQIDAQABAoIBACTlY7hyERNHw/r+ +F91mkFFLO3Hj3RRZXGSktdx3AMYs1fPMbAAhWLegHiO1oUap0XV2Vv4V2Q1hIiek +dimQVATAv+AQAKb+c+fgJqY1UIUKKQ6WnXqQ/rfz67kGJmLIoItXpvQ9VDfIRsfw +PtIy4mGmgoKsuLM8GFDaJXsFyITLqaMOBtX4f2czK2GNuZ9zTQE7kPXPmt+kFjos +DJdDCQaNYLpzVgAXixkHdOonwGtQSB/vwVxpvhAgPAac0K18nCgWeR5SK2kQlRAR +whN4UggocoSwuH/xSNYK/iJUaXsdnXxOH/hZjHVnQLC4r/6k4rtJMMZX1jKvVpuN +ZW2TgR0CgYEA5K9rmakCOQTOP8SpqL9AvsEeWgWjVYURyjiZoUBjMprG3Hospjs9 +x0JD5UDMcJqsC03riEkZ05EoBj1KzG6mIA1q1mofdLqTDp46rn3mlTbaoLncQ/t+ +Wt16qKVa3M8Y0Y0h9ZXwaIjYTtyQR2+AcGWmzR9MWdxw0XpiYrRPl+0CgYEAy5zN +mxeG1xEbRH+K8DRhvDjGUXgMD7yFYgMFeGjSC49jZ/c0CKA8ontD2S81rrG5fYUz +XfNf1gOTX6D2Muqc0qpXVgej4TzY0tk+ZXo4UV47g3c9rVC7WJkD3mhRjxBwGtg0 +cJX/TgLt6ftmBYRwqzVEkAZgA1FFY0UuWLKLyzUCgYBYkXAU9RX9S1Ut8VbcGiZT +OqwspV5a8syvCzjE8RD6OmOmZFMClpMYpxtwoKYNPno/bpA4Gke8Xs2njPl5L6l7 +ZHbqT/Oa29+0m6eSiQRS6gnGkfUzByYqvviQdGo6dqLgFc7/NHEFq2pLMLVYVWDj +Jxtw+hwx+WRgXQvZuofRCQKBgAfo9Yva5CAix/5nPgyK3Gklg37SvRU3a68dEu9U +kyRj06bMUbcsEm71fvNHBDzfJqebrx0tQUHdLbJ/XFEqECGXAIGVFma/qGS4j9X9 +Y4a1tHYtAbZj9oNAnofbYZ+/Mbu5PoBJM3viXT/DxnjcZkwga6N0u/IlDB8JJv3H +IespAoGAP5bDdT9WASVMRMKeNlSB90JgN5oDtLK7ccvUEBUB0UCKWUNZUkhWAKp9 +3iLJqMfr8LfDN8+W592tUBZ5V7UXbO+Jojlgjp4CQa1yf3tla0BNXPyeyHbJRhN2 +ZmGlYO03zGne46suhP2MO+EMc1w8dUyMdV6JtfLhLldF3UrEf0g= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java index c8a5ff380df5..72aa8c3e0125 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java @@ -340,6 +340,8 @@ public void testFilteredSettings() throws Exception { "truststore.password", "truststore.path", "truststore.algorithm", + "trust_restrictions.path", + "trust_restrictions.x509_fields", "keystore.key_password" ).forEach(suffix -> { diff --git a/x-pack/qa/multi-cluster-search-security/legacy-with-restricted-trust/build.gradle b/x-pack/qa/multi-cluster-search-security/legacy-with-restricted-trust/build.gradle new file mode 100644 index 000000000000..810895abe1e8 --- /dev/null +++ b/x-pack/qa/multi-cluster-search-security/legacy-with-restricted-trust/build.gradle @@ -0,0 +1,94 @@ +import org.elasticsearch.gradle.internal.info.BuildParams +import org.elasticsearch.gradle.internal.test.RestIntegTestTask + +apply plugin: 'elasticsearch.standalone-rest-test' +apply plugin: 'elasticsearch.rest-resources' + +configurations { + signedCerts + rootCert +} + +dependencies { + signedCerts project(path: ':x-pack:plugin:core', configuration: 'signedCerts') + rootCert project(path: ':x-pack:plugin:core', configuration: 'rootCert') +} + +tasks.register("copyCerts", Sync) { + dependsOn configurations.signedCerts + from(configurations.signedCerts) + from(configurations.rootCert) + into "${buildDir}/certs" +} + +// randomise between sniff and proxy modes +boolean proxyMode = BuildParams.random.nextBoolean() + +def fulfillingCluster = testClusters.register('fulfilling-cluster') { + setting 'xpack.security.enabled', 'true' + setting 'xpack.license.self_generated.type', 'basic' + setting 'xpack.security.transport.ssl.enabled', 'true' + setting 'xpack.security.transport.ssl.client_authentication', 'required' + extraConfigFile 'transport.key', file("${buildDir}/certs/n1.c1.key") + extraConfigFile 'transport.cert', file("${buildDir}/certs/n1.c1.crt") + extraConfigFile 'transport.ca', file("${buildDir}/certs/ca.crt") + extraConfigFile 'trust.yml', file("${buildDir}/resources/test/trust.yml") + setting 'xpack.security.transport.ssl.key', 'transport.key' + setting 'xpack.security.transport.ssl.certificate', 'transport.cert' + setting 'xpack.security.transport.ssl.certificate_authorities', 'transport.ca' + setting 'xpack.security.transport.ssl.verification_mode', 'certificate' + setting 'xpack.security.transport.ssl.trust_restrictions.path', 'trust.yml' + user username: "test_user", password: "x-pack-test-password" +} + +def queryingCluster = testClusters.register('querying-cluster') { + setting 'xpack.security.enabled', 'true' + setting 'xpack.license.self_generated.type', 'basic' + setting 'xpack.security.transport.ssl.enabled', 'true' + setting 'xpack.security.transport.ssl.client_authentication', 'required' + extraConfigFile 'transport.key', file("${buildDir}/certs/n1.c2.key") + extraConfigFile 'transport.cert', file("${buildDir}/certs/n1.c2.crt") + extraConfigFile 'transport.ca', file("${buildDir}/certs/ca.crt") + extraConfigFile 'trust.yml', file("${buildDir}/resources/test/trust.yml") + setting 'xpack.security.transport.ssl.key', 'transport.key' + setting 'xpack.security.transport.ssl.certificate', 'transport.cert' + setting 'xpack.security.transport.ssl.certificate_authorities', 'transport.ca' + setting 'xpack.security.transport.ssl.verification_mode', 'certificate' + setting 'xpack.security.transport.ssl.trust_restrictions.path', 'trust.yml' + setting 'xpack.security.transport.ssl.trust_restrictions.x509_fields', 'subjectAltName.dnsName' + setting 'cluster.remote.connections_per_cluster', "1" + user username: "test_user", password: "x-pack-test-password" + + if (proxyMode) { + setting 'cluster.remote.my_remote_cluster.mode', 'proxy' + setting 'cluster.remote.my_remote_cluster.proxy_address', { + "\"${fulfillingCluster.get().getAllTransportPortURI().get(0)}\"" + } + } else { + setting 'cluster.remote.my_remote_cluster.seeds', { + fulfillingCluster.get().getAllTransportPortURI().collect { "\"$it\"" }.toString() + } + } +} + +tasks.register('fulfilling-cluster', RestIntegTestTask) { + dependsOn 'copyCerts' + useCluster fulfillingCluster + systemProperty 'tests.rest.suite', 'fulfilling_cluster' +} + +tasks.register('querying-cluster', RestIntegTestTask) { + dependsOn 'copyCerts' + dependsOn 'fulfilling-cluster' + useCluster queryingCluster + useCluster fulfillingCluster + systemProperty 'tests.rest.suite', 'querying_cluster' +} + +// runs the fulfilling-cluster cluster tests then the querying-cluster tests +tasks.register("integTest") { + dependsOn 'copyCerts' + dependsOn 'querying-cluster' +} + +tasks.named("check").configure { dependsOn("integTest") } diff --git a/x-pack/qa/multi-cluster-search-security/legacy-with-restricted-trust/src/test/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecuritySmokeIT.java b/x-pack/qa/multi-cluster-search-security/legacy-with-restricted-trust/src/test/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecuritySmokeIT.java new file mode 100644 index 000000000000..c29442f1bb0a --- /dev/null +++ b/x-pack/qa/multi-cluster-search-security/legacy-with-restricted-trust/src/test/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecuritySmokeIT.java @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.remotecluster; + +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.elasticsearch.test.rest.ObjectPath; + +import static org.hamcrest.Matchers.equalTo; + +/** + * This test suite will be run twice: Once against the fulfilling cluster, then again against the querying cluster. + */ +public class RemoteClusterSecuritySmokeIT extends ESRestTestCase { + + private static final String USER = "test_user"; + private static final SecureString PASS = new SecureString("x-pack-test-password".toCharArray()); + + @Override + protected boolean preserveIndicesUponCompletion() { + return true; + } + + @Override + protected boolean preserveDataStreamsUponCompletion() { + return true; + } + + @Override + protected Settings restClientSettings() { + String token = basicAuthHeaderValue(USER, PASS); + return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build(); + } + + private boolean isFulfillingCluster() { + return "fulfilling_cluster".equals(System.getProperty("tests.rest.suite")); + } + + /** + * This test really depends on the local build.gradle, which configures cross-cluster search using the `remote_cluster.*` settings. + */ + public void testRemoteAccessPortFunctions() throws Exception { + if (isFulfillingCluster()) { + // Index some documents, so we can search them from the querying cluster + Request indexDocRequest = new Request("POST", "/test_idx/_doc"); + indexDocRequest.setJsonEntity("{\"foo\": \"bar\"}"); + Response response = client().performRequest(indexDocRequest); + assertOK(response); + + } else { + // Check that we can search the fulfilling cluster from the querying cluster + Request searchRequest = new Request("GET", "/my_remote_cluster:test_idx/_search"); + Response response = client().performRequest(searchRequest); + assertOK(response); + ObjectPath responseObj = ObjectPath.createFromResponse(response); + int totalHits = responseObj.evaluate("hits.total.value"); + assertThat(totalHits, equalTo(1)); + } + } +} diff --git a/x-pack/qa/multi-cluster-search-security/legacy-with-restricted-trust/src/test/resources/trust.yml b/x-pack/qa/multi-cluster-search-security/legacy-with-restricted-trust/src/test/resources/trust.yml new file mode 100644 index 000000000000..64b16a00e353 --- /dev/null +++ b/x-pack/qa/multi-cluster-search-security/legacy-with-restricted-trust/src/test/resources/trust.yml @@ -0,0 +1,2 @@ +trust.subject_name: + - "*.elasticsearch" From 51c358ec1010779c370df84eade2cb173e704fd6 Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Mon, 28 Nov 2022 08:51:26 +0100 Subject: [PATCH 089/919] Remove debug logging as flaky test is fixed (#91740) --- .../xpack/autoscaling/storage/ReactiveStorageIT.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugin/autoscaling/src/internalClusterTest/java/org/elasticsearch/xpack/autoscaling/storage/ReactiveStorageIT.java b/x-pack/plugin/autoscaling/src/internalClusterTest/java/org/elasticsearch/xpack/autoscaling/storage/ReactiveStorageIT.java index 6dc2a8d8be88..236c798851d9 100644 --- a/x-pack/plugin/autoscaling/src/internalClusterTest/java/org/elasticsearch/xpack/autoscaling/storage/ReactiveStorageIT.java +++ b/x-pack/plugin/autoscaling/src/internalClusterTest/java/org/elasticsearch/xpack/autoscaling/storage/ReactiveStorageIT.java @@ -23,7 +23,6 @@ import org.elasticsearch.node.Node; import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.test.NodeRoles; -import org.elasticsearch.test.junit.annotations.TestLogging; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.autoscaling.action.GetAutoscalingCapacityAction; import org.elasticsearch.xpack.autoscaling.action.PutAutoscalingPolicyAction; @@ -231,7 +230,6 @@ public void testScaleFromEmptyLegacy() { ); } - @TestLogging(value = "org.elasticsearch.monitor.fs.FsService:DEBUG", reason = "https://github.com/elastic/elasticsearch/issues/91083") public void testScaleWhileShrinking() throws Exception { internalCluster().startMasterOnlyNode(); final String dataNode1Name = internalCluster().startDataOnlyNode(); From 6f64af83b6c2ce4592f0ba393549d088b001fb5f Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Mon, 28 Nov 2022 09:37:22 +0100 Subject: [PATCH 090/919] Remove unused params in ClusterModule (#91913) --- .../elasticsearch/cluster/ClusterModule.java | 8 ++--- .../java/org/elasticsearch/node/Node.java | 1 - .../cluster/ClusterModuleTests.java | 35 ++----------------- 3 files changed, 5 insertions(+), 39 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/ClusterModule.java b/server/src/main/java/org/elasticsearch/cluster/ClusterModule.java index ffe8000480a2..b8c15dae0be2 100644 --- a/server/src/main/java/org/elasticsearch/cluster/ClusterModule.java +++ b/server/src/main/java/org/elasticsearch/cluster/ClusterModule.java @@ -25,7 +25,6 @@ import org.elasticsearch.cluster.metadata.NodesShutdownMetadata; import org.elasticsearch.cluster.metadata.RepositoriesMetadata; import org.elasticsearch.cluster.routing.DelayedAllocationService; -import org.elasticsearch.cluster.routing.RerouteService; import org.elasticsearch.cluster.routing.allocation.AllocationService; import org.elasticsearch.cluster.routing.allocation.ExistingShardsAllocator; import org.elasticsearch.cluster.routing.allocation.RoutingAllocation; @@ -123,7 +122,6 @@ public ClusterModule( SnapshotsInfoService snapshotsInfoService, ThreadPool threadPool, SystemIndices systemIndices, - Supplier rerouteServiceSupplier, WriteLoadForecaster writeLoadForecaster ) { this.clusterPlugins = clusterPlugins; @@ -136,8 +134,7 @@ public ClusterModule( clusterPlugins, clusterService, this::reconcile, - writeLoadForecaster, - clusterInfoService + writeLoadForecaster ); this.clusterService = clusterService; this.indexNameExpressionResolver = new IndexNameExpressionResolver(threadPool.getThreadContext(), systemIndices); @@ -349,8 +346,7 @@ private static ShardsAllocator createShardsAllocator( List clusterPlugins, ClusterService clusterService, DesiredBalanceReconcilerAction reconciler, - WriteLoadForecaster writeLoadForecaster, - ClusterInfoService clusterInfoService + WriteLoadForecaster writeLoadForecaster ) { Map> allocators = new HashMap<>(); allocators.put(BALANCED_ALLOCATOR, () -> new BalancedShardsAllocator(settings, clusterSettings, writeLoadForecaster)); diff --git a/server/src/main/java/org/elasticsearch/node/Node.java b/server/src/main/java/org/elasticsearch/node/Node.java index 566204bb8530..58b089940b21 100644 --- a/server/src/main/java/org/elasticsearch/node/Node.java +++ b/server/src/main/java/org/elasticsearch/node/Node.java @@ -581,7 +581,6 @@ protected Node( snapshotsInfoService, threadPool, systemIndices, - rerouteServiceReference::get, writeLoadForecaster ); modules.add(clusterModule); diff --git a/server/src/test/java/org/elasticsearch/cluster/ClusterModuleTests.java b/server/src/test/java/org/elasticsearch/cluster/ClusterModuleTests.java index fe4147d5f8cb..45bd2a86d6f7 100644 --- a/server/src/test/java/org/elasticsearch/cluster/ClusterModuleTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/ClusterModuleTests.java @@ -8,7 +8,6 @@ package org.elasticsearch.cluster; -import org.elasticsearch.cluster.routing.RerouteService; import org.elasticsearch.cluster.routing.ShardRouting; import org.elasticsearch.cluster.routing.allocation.ExistingShardsAllocator; import org.elasticsearch.cluster.routing.allocation.RoutingAllocation; @@ -156,14 +155,7 @@ public void testRegisterAllocationDeciderDuplicate() { public Collection createAllocationDeciders(Settings settings, ClusterSettings clusterSettings) { return Collections.singletonList(new EnableAllocationDecider(settings, clusterSettings)); } - }), - clusterInfoService, - null, - threadPool, - EmptySystemIndices.INSTANCE, - ClusterModuleTests::getFakeRerouteService, - WriteLoadForecaster.DEFAULT - ) + }), clusterInfoService, null, threadPool, EmptySystemIndices.INSTANCE, WriteLoadForecaster.DEFAULT) ); assertEquals(e.getMessage(), "Cannot specify allocation decider [" + EnableAllocationDecider.class.getName() + "] twice"); } @@ -174,14 +166,7 @@ public void testRegisterAllocationDecider() { public Collection createAllocationDeciders(Settings settings, ClusterSettings clusterSettings) { return Collections.singletonList(new FakeAllocationDecider()); } - }), - clusterInfoService, - null, - threadPool, - EmptySystemIndices.INSTANCE, - ClusterModuleTests::getFakeRerouteService, - WriteLoadForecaster.DEFAULT - ); + }), clusterInfoService, null, threadPool, EmptySystemIndices.INSTANCE, WriteLoadForecaster.DEFAULT); assertTrue(module.deciderList.stream().anyMatch(d -> d.getClass().equals(FakeAllocationDecider.class))); } @@ -191,14 +176,7 @@ private ClusterModule newClusterModuleWithShardsAllocator(Settings settings, Str public Map> getShardsAllocators(Settings settings, ClusterSettings clusterSettings) { return Collections.singletonMap(name, supplier); } - }), - clusterInfoService, - null, - threadPool, - EmptySystemIndices.INSTANCE, - ClusterModuleTests::getFakeRerouteService, - WriteLoadForecaster.DEFAULT - ); + }), clusterInfoService, null, threadPool, EmptySystemIndices.INSTANCE, WriteLoadForecaster.DEFAULT); } public void testRegisterShardsAllocator() { @@ -227,7 +205,6 @@ public void testUnknownShardsAllocator() { null, threadPool, EmptySystemIndices.INSTANCE, - ClusterModuleTests::getFakeRerouteService, WriteLoadForecaster.DEFAULT ) ); @@ -286,7 +263,6 @@ public void testRejectsReservedExistingShardsAllocatorName() { null, threadPool, EmptySystemIndices.INSTANCE, - ClusterModuleTests::getFakeRerouteService, WriteLoadForecaster.DEFAULT ); expectThrows(IllegalArgumentException.class, () -> clusterModule.setExistingShardsAllocators(new TestGatewayAllocator())); @@ -301,7 +277,6 @@ public void testRejectsDuplicateExistingShardsAllocatorName() { null, threadPool, EmptySystemIndices.INSTANCE, - ClusterModuleTests::getFakeRerouteService, WriteLoadForecaster.DEFAULT ); expectThrows(IllegalArgumentException.class, () -> clusterModule.setExistingShardsAllocators(new TestGatewayAllocator())); @@ -315,8 +290,4 @@ public Map getExistingShardsAllocators() { } }; } - - private static RerouteService getFakeRerouteService() { - return (s, p, r) -> { throw new AssertionError("should not be called"); }; - } } From c1a4cdee66b18a40212920eb4b7cd3a268af3d8b Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Mon, 28 Nov 2022 09:49:01 +0100 Subject: [PATCH 091/919] Simplify services set up in disk threshold decider tests (#91879) --- .../decider/DiskThresholdDeciderTests.java | 493 +++++------------- 1 file changed, 130 insertions(+), 363 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderTests.java index a81ea558a49a..98578e44cc08 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderTests.java @@ -55,18 +55,17 @@ import org.elasticsearch.snapshots.Snapshot; import org.elasticsearch.snapshots.SnapshotId; import org.elasticsearch.snapshots.SnapshotShardSizeInfo; +import org.elasticsearch.snapshots.SnapshotsInfoService; import org.elasticsearch.test.gateway.TestGatewayAllocator; -import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; -import static java.util.Collections.emptyMap; -import static java.util.Collections.singleton; import static org.elasticsearch.cluster.routing.RoutingNodesHelper.shardsWithState; import static org.elasticsearch.cluster.routing.ShardRoutingState.INITIALIZING; import static org.elasticsearch.cluster.routing.ShardRoutingState.RELOCATING; @@ -81,10 +80,6 @@ public class DiskThresholdDeciderTests extends ESAllocationTestCase { - DiskThresholdDecider makeDecider(Settings settings) { - return new DiskThresholdDecider(settings, new ClusterSettings(settings, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS)); - } - private void doTestDiskThreshold(boolean testMaxHeadroom) { Settings.Builder diskSettings = Settings.builder() .put(DiskThresholdSettings.CLUSTER_ROUTING_ALLOCATION_DISK_THRESHOLD_ENABLED_SETTING.getKey(), true) @@ -123,32 +118,12 @@ private void doTestDiskThreshold(boolean testMaxHeadroom) { shardSizes.put("[test][0][r]", exactFreeSpaceForHighWatermark); final ClusterInfo clusterInfo = new DevNullClusterInfo(usages, usages, shardSizes); - ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); - AllocationDeciders deciders = new AllocationDeciders( - new HashSet<>(Arrays.asList(new SameShardAllocationDecider(Settings.EMPTY, clusterSettings), makeDecider(diskSettings.build()))) - ); - - ClusterInfoService cis = () -> { - logger.info("--> calling fake getClusterInfo"); - return clusterInfo; - }; - AllocationService strategy = new AllocationService( - deciders, - new TestGatewayAllocator(), - new BalancedShardsAllocator(Settings.EMPTY), - cis, - EmptySnapshotsInfoService.INSTANCE - ); - - Metadata metadata = Metadata.builder() - .put(IndexMetadata.builder("test").settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(1)) - .build(); - - final RoutingTable initialRoutingTable = RoutingTable.builder().addAsNew(metadata.index("test")).build(); + AllocationService strategy = createAllocationService(clusterInfo, createDiskThresholdDecider(diskSettings.build())); + var indexMetadata = IndexMetadata.builder("test").settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(1).build(); ClusterState clusterState = ClusterState.builder(ClusterName.CLUSTER_NAME_SETTING.getDefault(Settings.EMPTY)) - .metadata(metadata) - .routingTable(initialRoutingTable) + .metadata(Metadata.builder().put(indexMetadata, false).build()) + .routingTable(RoutingTable.builder().addAsNew(indexMetadata).build()) .build(); logger.info("--> adding two nodes"); @@ -216,18 +191,7 @@ private void doTestDiskThreshold(boolean testMaxHeadroom) { .put(DiskThresholdSettings.CLUSTER_ROUTING_ALLOCATION_LOW_DISK_WATERMARK_SETTING.getKey(), "60%") .put(DiskThresholdSettings.CLUSTER_ROUTING_ALLOCATION_HIGH_DISK_WATERMARK_SETTING.getKey(), 0.7); } - - deciders = new AllocationDeciders( - new HashSet<>(Arrays.asList(new SameShardAllocationDecider(Settings.EMPTY, clusterSettings), makeDecider(diskSettings.build()))) - ); - - strategy = new AllocationService( - deciders, - new TestGatewayAllocator(), - new BalancedShardsAllocator(Settings.EMPTY), - cis, - EmptySnapshotsInfoService.INSTANCE - ); + strategy = createAllocationService(clusterInfo, createDiskThresholdDecider(diskSettings.build())); clusterState = strategy.reroute(clusterState, "reroute", ActionListener.noop()); logShardStates(clusterState); @@ -259,18 +223,7 @@ private void doTestDiskThreshold(boolean testMaxHeadroom) { .put(DiskThresholdSettings.CLUSTER_ROUTING_ALLOCATION_LOW_DISK_WATERMARK_SETTING.getKey(), 0.5) .put(DiskThresholdSettings.CLUSTER_ROUTING_ALLOCATION_HIGH_DISK_WATERMARK_SETTING.getKey(), 0.6); } - - deciders = new AllocationDeciders( - new HashSet<>(Arrays.asList(new SameShardAllocationDecider(Settings.EMPTY, clusterSettings), makeDecider(diskSettings.build()))) - ); - - strategy = new AllocationService( - deciders, - new TestGatewayAllocator(), - new BalancedShardsAllocator(Settings.EMPTY), - cis, - EmptySnapshotsInfoService.INSTANCE - ); + strategy = createAllocationService(clusterInfo, createDiskThresholdDecider(diskSettings.build())); clusterState = strategy.reroute(clusterState, "reroute", ActionListener.noop()); @@ -331,33 +284,12 @@ public void testDiskThresholdWithAbsoluteSizes() { shardSizes.put("[test][0][r]", 10L); final ClusterInfo clusterInfo = new DevNullClusterInfo(usages, usages, shardSizes); - ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); - AllocationDeciders deciders = new AllocationDeciders( - new HashSet<>(Arrays.asList(new SameShardAllocationDecider(Settings.EMPTY, clusterSettings), makeDecider(diskSettings))) - ); - - ClusterInfoService cis = () -> { - logger.info("--> calling fake getClusterInfo"); - return clusterInfo; - }; - - AllocationService strategy = new AllocationService( - deciders, - new TestGatewayAllocator(), - new BalancedShardsAllocator(Settings.EMPTY), - cis, - EmptySnapshotsInfoService.INSTANCE - ); - - Metadata metadata = Metadata.builder() - .put(IndexMetadata.builder("test").settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(2)) - .build(); - - RoutingTable initialRoutingTable = RoutingTable.builder().addAsNew(metadata.index("test")).build(); + AllocationService strategy = createAllocationService(clusterInfo, createDiskThresholdDecider(diskSettings)); + var indexMetadata = IndexMetadata.builder("test").settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(2).build(); ClusterState clusterState = ClusterState.builder(ClusterName.CLUSTER_NAME_SETTING.getDefault(Settings.EMPTY)) - .metadata(metadata) - .routingTable(initialRoutingTable) + .metadata(Metadata.builder().put(indexMetadata, false).build()) + .routingTable(RoutingTable.builder().addAsNew(indexMetadata).build()) .build(); logger.info("--> adding node1 and node2 node"); @@ -386,17 +318,8 @@ public void testDiskThresholdWithAbsoluteSizes() { usages = new HashMap<>(usages); usages.put(nodeWithoutPrimary, new DiskUsage(nodeWithoutPrimary, "", "/dev/null", 100, 35)); // 65% used final ClusterInfo clusterInfo2 = new DevNullClusterInfo(usages, usages, shardSizes); - cis = () -> { - logger.info("--> calling fake getClusterInfo"); - return clusterInfo2; - }; - strategy = new AllocationService( - deciders, - new TestGatewayAllocator(), - new BalancedShardsAllocator(Settings.EMPTY), - cis, - EmptySnapshotsInfoService.INSTANCE - ); + + strategy = createAllocationService(clusterInfo2, createDiskThresholdDecider(diskSettings)); clusterState = strategy.reroute(clusterState, "reroute", ActionListener.noop()); logShardStates(clusterState); @@ -449,18 +372,7 @@ public void testDiskThresholdWithAbsoluteSizes() { .put(DiskThresholdSettings.CLUSTER_ROUTING_ALLOCATION_HIGH_DISK_WATERMARK_SETTING.getKey(), "30b") .put(DiskThresholdSettings.CLUSTER_ROUTING_ALLOCATION_DISK_FLOOD_STAGE_WATERMARK_SETTING.getKey(), "20b") .build(); - - deciders = new AllocationDeciders( - new HashSet<>(Arrays.asList(new SameShardAllocationDecider(Settings.EMPTY, clusterSettings), makeDecider(diskSettings))) - ); - - strategy = new AllocationService( - deciders, - new TestGatewayAllocator(), - new BalancedShardsAllocator(Settings.EMPTY), - cis, - EmptySnapshotsInfoService.INSTANCE - ); + strategy = createAllocationService(clusterInfo2, createDiskThresholdDecider(diskSettings)); clusterState = strategy.reroute(clusterState, "reroute", ActionListener.noop()); logShardStates(clusterState); @@ -482,18 +394,7 @@ public void testDiskThresholdWithAbsoluteSizes() { .put(DiskThresholdSettings.CLUSTER_ROUTING_ALLOCATION_HIGH_DISK_WATERMARK_SETTING.getKey(), "40b") .put(DiskThresholdSettings.CLUSTER_ROUTING_ALLOCATION_DISK_FLOOD_STAGE_WATERMARK_SETTING.getKey(), "30b") .build(); - - deciders = new AllocationDeciders( - new HashSet<>(Arrays.asList(new SameShardAllocationDecider(Settings.EMPTY, clusterSettings), makeDecider(diskSettings))) - ); - - strategy = new AllocationService( - deciders, - new TestGatewayAllocator(), - new BalancedShardsAllocator(Settings.EMPTY), - cis, - EmptySnapshotsInfoService.INSTANCE - ); + strategy = createAllocationService(clusterInfo2, createDiskThresholdDecider(diskSettings)); clusterState = strategy.reroute(clusterState, "reroute", ActionListener.noop()); @@ -587,51 +488,21 @@ private void doTestDiskThresholdWithShardSizes(boolean testMaxHeadroom) { Map.of("[test][0][p]", testMaxHeadroom ? ByteSizeValue.ofGb(10).getBytes() : 10L) ); - AllocationDeciders deciders = new AllocationDeciders( - new HashSet<>( - Arrays.asList( - new SameShardAllocationDecider( - Settings.EMPTY, - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS) - ), - makeDecider(diskSettings.build()) - ) - ) - ); - - ClusterInfoService cis = () -> { - logger.info("--> calling fake getClusterInfo"); - return clusterInfo; - }; - - AllocationService strategy = new AllocationService( - deciders, - new TestGatewayAllocator(), - new BalancedShardsAllocator(Settings.EMPTY), - cis, - EmptySnapshotsInfoService.INSTANCE - ); - - Metadata metadata = Metadata.builder() - .put(IndexMetadata.builder("test").settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(0)) - .build(); - - RoutingTable routingTable = RoutingTable.builder().addAsNew(metadata.index("test")).build(); + AllocationService strategy = createAllocationService(clusterInfo, createDiskThresholdDecider(diskSettings.build())); + var indexMetadata = IndexMetadata.builder("test").settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(0).build(); ClusterState clusterState = ClusterState.builder(ClusterName.CLUSTER_NAME_SETTING.getDefault(Settings.EMPTY)) - .metadata(metadata) - .routingTable(routingTable) + .metadata(Metadata.builder().put(indexMetadata, false).build()) + .routingTable(RoutingTable.builder().addAsNew(indexMetadata).build()) .build(); logger.info("--> adding node1"); // node2 is added because DiskThresholdDecider automatically ignore single-node clusters clusterState = ClusterState.builder(clusterState) .nodes(DiscoveryNodes.builder().add(newNode("node1")).add(newNode("node2"))) .build(); - routingTable = strategy.reroute(clusterState, "reroute", ActionListener.noop()).routingTable(); - clusterState = ClusterState.builder(clusterState).routingTable(routingTable).build(); + clusterState = strategy.reroute(clusterState, "reroute", ActionListener.noop()); logger.info("--> start the shards (primaries)"); - routingTable = startInitializingShardsAndReroute(strategy, clusterState).routingTable(); - clusterState = ClusterState.builder(clusterState).routingTable(routingTable).build(); + clusterState = startInitializingShardsAndReroute(strategy, clusterState); logShardStates(clusterState); // Shard can't be allocated to node1 (or node2) because it would cause too much usage @@ -664,40 +535,12 @@ public void testUnknownDiskUsage() { shardSizes.put("[test][0][r]", 10L); // 10 bytes final ClusterInfo clusterInfo = new DevNullClusterInfo(usages, usages, shardSizes); - AllocationDeciders deciders = new AllocationDeciders( - new HashSet<>( - Arrays.asList( - new SameShardAllocationDecider( - Settings.EMPTY, - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS) - ), - makeDecider(diskSettings) - ) - ) - ); - - ClusterInfoService cis = () -> { - logger.info("--> calling fake getClusterInfo"); - return clusterInfo; - }; - - AllocationService strategy = new AllocationService( - deciders, - new TestGatewayAllocator(), - new BalancedShardsAllocator(Settings.EMPTY), - cis, - EmptySnapshotsInfoService.INSTANCE - ); - - Metadata metadata = Metadata.builder() - .put(IndexMetadata.builder("test").settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(0)) - .build(); - - RoutingTable routingTable = RoutingTable.builder().addAsNew(metadata.index("test")).build(); + AllocationService strategy = createAllocationService(clusterInfo, createDiskThresholdDecider(diskSettings)); + var indexMetadata = IndexMetadata.builder("test").settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(0).build(); ClusterState clusterState = ClusterState.builder(ClusterName.CLUSTER_NAME_SETTING.getDefault(Settings.EMPTY)) - .metadata(metadata) - .routingTable(routingTable) + .metadata(Metadata.builder().put(indexMetadata, false).build()) + .routingTable(RoutingTable.builder().addAsNew(indexMetadata).build()) .build(); logger.info("--> adding node1"); clusterState = ClusterState.builder(clusterState) @@ -706,16 +549,14 @@ public void testUnknownDiskUsage() { // automatically ignore single-node clusters ) .build(); - routingTable = strategy.reroute(clusterState, "reroute", ActionListener.noop()).routingTable(); - clusterState = ClusterState.builder(clusterState).routingTable(routingTable).build(); + clusterState = strategy.reroute(clusterState, "reroute", ActionListener.noop()); // Shard can be allocated to node1, even though it only has 25% free, // because it's a primary that's never been allocated before assertThat(shardsWithState(clusterState.getRoutingNodes(), INITIALIZING).size(), equalTo(1)); logger.info("--> start the shards (primaries)"); - routingTable = startInitializingShardsAndReroute(strategy, clusterState).routingTable(); - clusterState = ClusterState.builder(clusterState).routingTable(routingTable).build(); + clusterState = startInitializingShardsAndReroute(strategy, clusterState); logShardStates(clusterState); // A single shard is started on node1, even though it normally would not @@ -727,7 +568,6 @@ public void testUnknownDiskUsage() { public void testAverageUsage() { RoutingNode rn = RoutingNodesHelper.routingNode("node1", newNode("node1")); - DiskThresholdDecider decider = makeDecider(Settings.EMPTY); Map usages = new HashMap<>(); usages.put("node2", new DiskUsage("node2", "n2", "/dev/null", 100, 50)); // 50% used @@ -776,45 +616,28 @@ private void doTestShardRelocationsTakenIntoAccount(boolean testMaxHeadroom) { shardSizes.put("[test2][0][r]", testMaxHeadroom ? ByteSizeValue.ofGb(1).getBytes() : 1L); final ClusterInfo clusterInfo = new DevNullClusterInfo(usages, usages, shardSizes); - DiskThresholdDecider decider = makeDecider(diskSettings.build()); - final ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); - AllocationDeciders deciders = new AllocationDeciders( - new HashSet<>( - Arrays.asList( - new SameShardAllocationDecider(Settings.EMPTY, clusterSettings), - new EnableAllocationDecider( - Settings.builder().put(CLUSTER_ROUTING_REBALANCE_ENABLE_SETTING.getKey(), "none").build(), - clusterSettings - ), - decider - ) - ) - ); - final AtomicReference clusterInfoReference = new AtomicReference<>(clusterInfo); - final ClusterInfoService cis = clusterInfoReference::get; - AllocationService strategy = new AllocationService( - deciders, - new TestGatewayAllocator(), - new BalancedShardsAllocator(Settings.EMPTY), - cis, - EmptySnapshotsInfoService.INSTANCE + AllocationService strategy = createAllocationService( + clusterInfoReference::get, + EmptySnapshotsInfoService.INSTANCE, + createEnableAllocationDecider(Settings.builder().put(CLUSTER_ROUTING_REBALANCE_ENABLE_SETTING.getKey(), "none").build()), + createDiskThresholdDecider(diskSettings.build()) ); - Metadata metadata = Metadata.builder() - .put(IndexMetadata.builder("test").settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(1)) - .put(IndexMetadata.builder("test2").settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(1)) + var indexMetadata1 = IndexMetadata.builder("test") + .settings(settings(Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(1) .build(); - - RoutingTable initialRoutingTable = RoutingTable.builder() - .addAsNew(metadata.index("test")) - .addAsNew(metadata.index("test2")) + var indexMetadata2 = IndexMetadata.builder("test2") + .settings(settings(Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(1) .build(); - ClusterState clusterState = ClusterState.builder(ClusterName.CLUSTER_NAME_SETTING.getDefault(Settings.EMPTY)) - .metadata(metadata) - .routingTable(initialRoutingTable) + .metadata(Metadata.builder().put(indexMetadata1, false).put(indexMetadata2, false).build()) + .routingTable(RoutingTable.builder().addAsNew(indexMetadata1).addAsNew(indexMetadata2).build()) .build(); logger.info("--> adding two nodes"); @@ -989,34 +812,17 @@ private void doTestCanRemainWithShardRelocatingAway(boolean testMaxHeadroom) { final ClusterInfo clusterInfo = new DevNullClusterInfo(usages, usages, shardSizes); - DiskThresholdDecider diskThresholdDecider = makeDecider(diskSettings.build()); - Metadata metadata = Metadata.builder() - .put(IndexMetadata.builder("test").settings(settings(Version.CURRENT)).numberOfShards(2).numberOfReplicas(0)) - .put(IndexMetadata.builder("foo").settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(0)) - .build(); - - RoutingTable initialRoutingTable = RoutingTable.builder().addAsNew(metadata.index("test")).addAsNew(metadata.index("foo")).build(); + DiskThresholdDecider diskThresholdDecider = createDiskThresholdDecider(diskSettings.build()); - DiscoveryNode discoveryNode1 = new DiscoveryNode( - "node1", - buildNewFakeTransportAddress(), - emptyMap(), - MASTER_DATA_ROLES, - Version.CURRENT - ); - DiscoveryNode discoveryNode2 = new DiscoveryNode( - "node2", - buildNewFakeTransportAddress(), - emptyMap(), - MASTER_DATA_ROLES, - Version.CURRENT - ); - DiscoveryNodes discoveryNodes = DiscoveryNodes.builder().add(discoveryNode1).add(discoveryNode2).build(); + DiscoveryNode discoveryNode1 = newNode("node1"); + DiscoveryNode discoveryNode2 = newNode("node2"); + var testMetadata = IndexMetadata.builder("test").settings(settings(Version.CURRENT)).numberOfShards(2).numberOfReplicas(0).build(); + var fooMetadata = IndexMetadata.builder("foo").settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(0).build(); ClusterState baseClusterState = ClusterState.builder(ClusterName.CLUSTER_NAME_SETTING.getDefault(Settings.EMPTY)) - .metadata(metadata) - .routingTable(initialRoutingTable) - .nodes(discoveryNodes) + .metadata(Metadata.builder().put(testMetadata, false).put(fooMetadata, false).build()) + .routingTable(RoutingTable.builder().addAsNew(testMetadata).addAsNew(fooMetadata).build()) + .nodes(DiscoveryNodes.builder().add(discoveryNode1).add(discoveryNode2).build()) .build(); // Two shards consuming each 80% of disk space while 70% is allowed, so shard 0 isn't allowed here @@ -1091,13 +897,13 @@ private void doTestCanRemainWithShardRelocatingAway(boolean testMaxHeadroom) { testMaxHeadroom ? "there is enough disk on this node for the shard to remain, free: [4.9tb]" : "there is enough disk on this node for the shard to remain, free: [60b]", - ((Decision.Single) decision).getExplanation() + decision.getExplanation() ); decision = diskThresholdDecider.canAllocate(fooRouting, firstRoutingNode, routingAllocation); assertThat(decision.type(), equalTo(Decision.Type.NO)); if (fooRouting.recoverySource().getType() == RecoverySource.Type.EMPTY_STORE) { assertThat( - ((Decision.Single) decision).getExplanation(), + decision.getExplanation(), containsString( testMaxHeadroom ? "the node is above the high watermark cluster setting [cluster.routing.allocation.disk.watermark" @@ -1109,7 +915,7 @@ private void doTestCanRemainWithShardRelocatingAway(boolean testMaxHeadroom) { ); } else { assertThat( - ((Decision.Single) decision).getExplanation(), + decision.getExplanation(), containsString( testMaxHeadroom ? "the node is above the low watermark cluster setting [cluster.routing.allocation.disk.watermark.low" @@ -1122,28 +928,7 @@ private void doTestCanRemainWithShardRelocatingAway(boolean testMaxHeadroom) { } // Creating AllocationService instance and the services it depends on... - ClusterInfoService cis = () -> { - logger.info("--> calling fake getClusterInfo"); - return clusterInfo; - }; - AllocationDeciders deciders = new AllocationDeciders( - new HashSet<>( - Arrays.asList( - new SameShardAllocationDecider( - Settings.EMPTY, - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS) - ), - diskThresholdDecider - ) - ) - ); - AllocationService strategy = new AllocationService( - deciders, - new TestGatewayAllocator(), - new BalancedShardsAllocator(Settings.EMPTY), - cis, - EmptySnapshotsInfoService.INSTANCE - ); + AllocationService strategy = createAllocationService(clusterInfo, diskThresholdDecider); // Ensure that the reroute call doesn't alter the routing table, since the first primary is relocating away // and therefore we will have sufficient disk space on node1. ClusterState result = strategy.reroute(clusterState, "reroute", ActionListener.noop()); @@ -1194,60 +979,22 @@ private void doTestWatermarksEnabledForSingleDataNode(boolean testMaxHeadroom) { Map shardSizes = Map.of("[test][0][p]", testMaxHeadroom ? ByteSizeValue.ofGb(60).getBytes() : 40L); final ClusterInfo clusterInfo = new DevNullClusterInfo(usages, usages, shardSizes); - DiskThresholdDecider diskThresholdDecider = makeDecider(diskSettings); - Metadata metadata = Metadata.builder() - .put(IndexMetadata.builder("test").settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(0)) - .build(); - RoutingTable initialRoutingTable = RoutingTable.builder().addAsNew(metadata.index("test")).build(); - - DiscoveryNode masterNode = new DiscoveryNode( - "master", - "master", - buildNewFakeTransportAddress(), - emptyMap(), - singleton(DiscoveryNodeRole.MASTER_ROLE), - Version.CURRENT - ); - DiscoveryNode dataNode = new DiscoveryNode( - "data", - "data", - buildNewFakeTransportAddress(), - emptyMap(), - singleton(DiscoveryNodeRole.DATA_ROLE), - Version.CURRENT - ); - DiscoveryNodes.Builder discoveryNodesBuilder = DiscoveryNodes.builder().add(dataNode); + DiskThresholdDecider diskThresholdDecider = createDiskThresholdDecider(diskSettings); + + var discoveryNodesBuilder = DiscoveryNodes.builder().add(newNode("data", "data", Set.of(DiscoveryNodeRole.DATA_ROLE))); if (randomBoolean()) { - discoveryNodesBuilder.add(masterNode); + discoveryNodesBuilder.add(newNode("master", "master", Set.of(DiscoveryNodeRole.MASTER_ROLE))); } - DiscoveryNodes discoveryNodes = discoveryNodesBuilder.build(); + var testMetadata = IndexMetadata.builder("test").settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(0).build(); ClusterState clusterState = ClusterState.builder(new ClusterName("test")) - .nodes(discoveryNodes) - .metadata(metadata) - .routingTable(initialRoutingTable) + .nodes(discoveryNodesBuilder.build()) + .metadata(Metadata.builder().put(testMetadata, false).build()) + .routingTable(RoutingTable.builder().addAsNew(testMetadata).build()) .build(); // validate that the shard cannot be allocated - ClusterInfoService cis = () -> clusterInfo; - AllocationDeciders deciders = new AllocationDeciders( - new HashSet<>( - Arrays.asList( - new SameShardAllocationDecider( - Settings.EMPTY, - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS) - ), - diskThresholdDecider - ) - ) - ); - AllocationService strategy = new AllocationService( - deciders, - new TestGatewayAllocator(), - new BalancedShardsAllocator(Settings.EMPTY), - cis, - EmptySnapshotsInfoService.INSTANCE - ); + AllocationService strategy = createAllocationService(clusterInfo, diskThresholdDecider); ClusterState result = strategy.reroute(clusterState, "reroute", ActionListener.noop()); ShardRouting shardRouting = result.routingTable().index("test").shard(0).primaryShard(); @@ -1355,53 +1102,41 @@ private void doTestDiskThresholdWithSnapshotShardSizes(boolean testMaxHeadroom) new DiskUsage("node1", "n1", "/dev/null", totalBytes, testMaxHeadroom ? ByteSizeValue.ofGb(210).getBytes() : 21) ); usages.put("node2", new DiskUsage("node2", "n2", "/dev/null", totalBytes, testMaxHeadroom ? ByteSizeValue.ofGb(1).getBytes() : 1)); - final ClusterInfoService clusterInfoService = () -> new DevNullClusterInfo(usages, usages, Map.of()); - - final AllocationDeciders deciders = new AllocationDeciders( - new HashSet<>( - Arrays.asList( - new RestoreInProgressAllocationDecider(), - new SameShardAllocationDecider( - Settings.EMPTY, - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS) - ), - makeDecider(diskSettings.build()) - ) - ) - ); final Snapshot snapshot = new Snapshot("_repository", new SnapshotId("_snapshot_name", UUIDs.randomBase64UUID(random()))); final IndexId indexId = new IndexId("_indexid_name", UUIDs.randomBase64UUID(random())); final ShardId shardId = new ShardId(new Index("test", IndexMetadata.INDEX_UUID_NA_VALUE), 0); - final Metadata metadata = Metadata.builder() - .put( - IndexMetadata.builder("test") - .settings(settings(Version.CURRENT)) - .numberOfShards(1) - .numberOfReplicas(0) - .putInSyncAllocationIds(0, Set.of(AllocationId.newInitializing().getId())) - ) + var indexMetadata = IndexMetadata.builder("test") + .settings(settings(Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(0) + .putInSyncAllocationIds(0, Set.of(AllocationId.newInitializing().getId())) .build(); - - final RoutingTable routingTable = RoutingTable.builder() - .addAsNewRestore( - metadata.index("test"), - new RecoverySource.SnapshotRecoverySource("_restore_uuid", snapshot, Version.CURRENT, indexId), - new HashSet<>() - ) - .build(); - - Map shards = Map.of(shardId, new RestoreInProgress.ShardRestoreStatus("node1")); - - final RestoreInProgress.Builder restores = new RestoreInProgress.Builder().add( - new RestoreInProgress.Entry("_restore_uuid", snapshot, RestoreInProgress.State.INIT, false, List.of("test"), shards) - ); - ClusterState clusterState = ClusterState.builder(new ClusterName(getTestName())) - .metadata(metadata) - .routingTable(routingTable) - .putCustom(RestoreInProgress.TYPE, restores.build()) + .metadata(Metadata.builder().put(indexMetadata, false).build()) + .routingTable( + RoutingTable.builder() + .addAsNewRestore( + indexMetadata, + new RecoverySource.SnapshotRecoverySource("_restore_uuid", snapshot, Version.CURRENT, indexId), + new HashSet<>() + ) + .build() + ) + .putCustom( + RestoreInProgress.TYPE, + new RestoreInProgress.Builder().add( + new RestoreInProgress.Entry( + "_restore_uuid", + snapshot, + RestoreInProgress.State.INIT, + false, + List.of("test"), + Map.of(shardId, new RestoreInProgress.ShardRestoreStatus("node1")) + ) + ).build() + ) .nodes(DiscoveryNodes.builder().add(newNode("node1")).add(newNode("node2")) // node2 is added because DiskThresholdDecider // automatically ignore single-node clusters ) @@ -1422,12 +1157,12 @@ private void doTestDiskThresholdWithSnapshotShardSizes(boolean testMaxHeadroom) assertThat(shardsWithState(clusterState.getRoutingNodes(), UNASSIGNED).size(), equalTo(1)); final AtomicReference snapshotShardSizeInfoRef = new AtomicReference<>(SnapshotShardSizeInfo.EMPTY); - final AllocationService strategy = new AllocationService( - deciders, - new TestGatewayAllocator(), - new BalancedShardsAllocator(Settings.EMPTY), - clusterInfoService, - snapshotShardSizeInfoRef::get + + final AllocationService strategy = createAllocationService( + () -> new DevNullClusterInfo(usages, usages, Map.of()), + snapshotShardSizeInfoRef::get, + new RestoreInProgressAllocationDecider(), + createDiskThresholdDecider(diskSettings.build()) ); // reroute triggers snapshot shard size fetching @@ -1495,6 +1230,38 @@ public void logShardStates(ClusterState state) { ); } + private AllocationService createAllocationService(ClusterInfo clusterInfo, AllocationDecider... allocationDeciders) { + return createAllocationService(() -> clusterInfo, EmptySnapshotsInfoService.INSTANCE, allocationDeciders); + } + + private AllocationService createAllocationService( + ClusterInfoService clusterInfoService, + SnapshotsInfoService snapshotShardSizeInfoService, + AllocationDecider... allocationDeciders + ) { + return new AllocationService( + new AllocationDeciders( + Stream.concat(Stream.of(createSameShardAllocationDecider(Settings.EMPTY)), Stream.of(allocationDeciders)).toList() + ), + new TestGatewayAllocator(), + new BalancedShardsAllocator(Settings.EMPTY), + clusterInfoService, + snapshotShardSizeInfoService + ); + } + + private DiskThresholdDecider createDiskThresholdDecider(Settings settings) { + return new DiskThresholdDecider(settings, new ClusterSettings(settings, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS)); + } + + private SameShardAllocationDecider createSameShardAllocationDecider(Settings settings) { + return new SameShardAllocationDecider(settings, new ClusterSettings(settings, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS)); + } + + private EnableAllocationDecider createEnableAllocationDecider(Settings settings) { + return new EnableAllocationDecider(settings, new ClusterSettings(settings, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS)); + } + /** * ClusterInfo that always reports /dev/null for the shards' data paths. */ From 4095d658e53bd414ceff4a2d85b9b6d08133d040 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 28 Nov 2022 09:23:16 +0000 Subject: [PATCH 092/919] Chunked encoding for tasks APIs (#91935) This response can reach many MiB in size in a large and busy cluster, let's use chunking here. Relates #89838 --- .../tasks/cancel/CancelTasksResponse.java | 12 -- .../node/tasks/list/ListTasksResponse.java | 146 ++++++++++-------- .../admin/cluster/RestListTasksAction.java | 42 ++--- .../node/tasks/TransportTasksActionTests.java | 7 +- .../tasks/CancelTasksResponseTests.java | 31 ++-- .../tasks/ListTasksResponseTests.java | 70 +++++++-- 6 files changed, 179 insertions(+), 129 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/cancel/CancelTasksResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/cancel/CancelTasksResponse.java index 51ad85f99328..a53ed8dacc36 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/cancel/CancelTasksResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/cancel/CancelTasksResponse.java @@ -11,11 +11,9 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.TaskOperationFailure; import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.tasks.TaskInfo; import org.elasticsearch.xcontent.ConstructingObjectParser; -import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; @@ -43,17 +41,7 @@ public CancelTasksResponse( super(tasks, taskFailures, nodeFailures); } - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - return super.toXContent(builder, params); - } - public static CancelTasksResponse fromXContent(XContentParser parser) { return PARSER.apply(parser, null); } - - @Override - public String toString() { - return Strings.toString(this, true, true); - } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/list/ListTasksResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/list/ListTasksResponse.java index 28df33674ee7..2d65e128a657 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/list/ListTasksResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/list/ListTasksResponse.java @@ -16,30 +16,33 @@ import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.common.Strings; import org.elasticsearch.common.TriFunction; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.tasks.TaskId; import org.elasticsearch.tasks.TaskInfo; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.ToXContentObject; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; /** * Returns the list of tasks currently running on the nodes */ -public class ListTasksResponse extends BaseTasksResponse implements ToXContentObject { +public class ListTasksResponse extends BaseTasksResponse { private static final String TASKS = "tasks"; private final List tasks; @@ -142,7 +145,7 @@ private void buildTaskGroups() { topLevelTasks.add(taskGroup); } } - this.groups = Collections.unmodifiableList(topLevelTasks.stream().map(TaskGroup.Builder::build).toList()); + this.groups = topLevelTasks.stream().map(TaskGroup.Builder::build).toList(); } /** @@ -155,82 +158,98 @@ public List getTasks() { /** * Convert this task response to XContent grouping by executing nodes. */ - public XContentBuilder toXContentGroupedByNode(XContentBuilder builder, Params params, DiscoveryNodes discoveryNodes) - throws IOException { - toXContentCommon(builder, params); - builder.startObject("nodes"); - for (Map.Entry> entry : getPerNodeTasks().entrySet()) { - DiscoveryNode node = discoveryNodes.get(entry.getKey()); - builder.startObject(entry.getKey()); - if (node != null) { - // If the node is no longer part of the cluster, oh well, we'll just skip it's useful information. - builder.field("name", node.getName()); - builder.field("transport_address", node.getAddress().toString()); - builder.field("host", node.getHostName()); - builder.field("ip", node.getAddress()); - - builder.startArray("roles"); - for (DiscoveryNodeRole role : node.getRoles()) { - builder.value(role.roleName()); - } - builder.endArray(); + public ChunkedToXContent groupedByNode(Supplier nodesInCluster) { + return ignored -> { + final var discoveryNodes = nodesInCluster.get(); + return Iterators.concat(Iterators.single((builder, params) -> { + builder.startObject(); + toXContentCommon(builder, params); + builder.startObject("nodes"); + return builder; + }), getPerNodeTasks().entrySet().stream().flatMap(entry -> { + DiscoveryNode node = discoveryNodes.get(entry.getKey()); + return Stream.>of(Stream.of((builder, params) -> { + builder.startObject(entry.getKey()); + if (node != null) { + // If the node is no longer part of the cluster, oh well, we'll just skip its useful information. + builder.field("name", node.getName()); + builder.field("transport_address", node.getAddress().toString()); + builder.field("host", node.getHostName()); + builder.field("ip", node.getAddress()); + + builder.startArray("roles"); + for (DiscoveryNodeRole role : node.getRoles()) { + builder.value(role.roleName()); + } + builder.endArray(); - if (node.getAttributes().isEmpty() == false) { - builder.startObject("attributes"); - for (Map.Entry attrEntry : node.getAttributes().entrySet()) { - builder.field(attrEntry.getKey(), attrEntry.getValue()); + if (node.getAttributes().isEmpty() == false) { + builder.startObject("attributes"); + for (Map.Entry attrEntry : node.getAttributes().entrySet()) { + builder.field(attrEntry.getKey(), attrEntry.getValue()); + } + builder.endObject(); + } } + builder.startObject(TASKS); + return builder; + }), entry.getValue().stream().map(task -> (builder, params) -> { + builder.startObject(task.taskId().toString()); + task.toXContent(builder, params); builder.endObject(); - } - } - builder.startObject(TASKS); - for (TaskInfo task : entry.getValue()) { - builder.startObject(task.taskId().toString()); - task.toXContent(builder, params); + return builder; + }), Stream.of((builder, params) -> { + builder.endObject(); + builder.endObject(); + return builder; + })).flatMap(Function.identity()); + }).iterator(), Iterators.single((builder, params) -> { builder.endObject(); - } - builder.endObject(); - builder.endObject(); - } - builder.endObject(); - return builder; + builder.endObject(); + return builder; + })); + }; } /** * Convert this response to XContent grouping by parent tasks. */ - public XContentBuilder toXContentGroupedByParents(XContentBuilder builder, Params params) throws IOException { - toXContentCommon(builder, params); - builder.startObject(TASKS); - for (TaskGroup group : getTaskGroups()) { + public ChunkedToXContent groupedByParent() { + return ignored -> Iterators.concat(Iterators.single((builder, params) -> { + builder.startObject(); + toXContentCommon(builder, params); + builder.startObject(TASKS); + return builder; + }), getTaskGroups().stream().map(group -> (builder, params) -> { builder.field(group.taskInfo().taskId().toString()); group.toXContent(builder, params); - } - builder.endObject(); - return builder; + return builder; + }).iterator(), Iterators.single((builder, params) -> { + builder.endObject(); + builder.endObject(); + return builder; + })); } /** * Presents a flat list of tasks */ - public XContentBuilder toXContentGroupedByNone(XContentBuilder builder, Params params) throws IOException { - toXContentCommon(builder, params); - builder.startArray(TASKS); - for (TaskInfo taskInfo : getTasks()) { + public ChunkedToXContent groupedByNone() { + return ignored -> Iterators.concat(Iterators.single((builder, params) -> { + builder.startObject(); + toXContentCommon(builder, params); + builder.startArray(TASKS); + return builder; + }), getTasks().stream().map(taskInfo -> (builder, params) -> { builder.startObject(); taskInfo.toXContent(builder, params); builder.endObject(); - } - builder.endArray(); - return builder; - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - toXContentGroupedByNone(builder, params); - builder.endObject(); - return builder; + return builder; + }).iterator(), Iterators.single((builder, params) -> { + builder.endArray(); + builder.endObject(); + return builder; + })); } public static ListTasksResponse fromXContent(XContentParser parser) { @@ -239,6 +258,7 @@ public static ListTasksResponse fromXContent(XContentParser parser) { @Override public String toString() { - return Strings.toString(this, true, true); + return Strings.toString(ChunkedToXContent.wrapAsXContentObject(groupedByNone()), true, true); } + } diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestListTasksAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestListTasksAction.java index bbf2d86ce07d..99417fbc962b 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestListTasksAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestListTasksAction.java @@ -18,12 +18,8 @@ import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestChannel; import org.elasticsearch.rest.RestRequest; -import org.elasticsearch.rest.RestResponse; -import org.elasticsearch.rest.RestStatus; -import org.elasticsearch.rest.action.RestBuilderListener; -import org.elasticsearch.rest.action.RestToXContentListener; +import org.elasticsearch.rest.action.RestChunkedToXContentListener; import org.elasticsearch.tasks.TaskId; -import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; import java.util.List; @@ -80,33 +76,17 @@ public static ListTasksRequest generateListTasksRequest(RestRequest request) { public static ActionListener listTasksResponseListener( Supplier nodesInCluster, String groupBy, - final RestChannel channel + RestChannel channel ) { - if ("nodes".equals(groupBy)) { - return new RestBuilderListener(channel) { - @Override - public RestResponse buildResponse(T response, XContentBuilder builder) throws Exception { - builder.startObject(); - response.toXContentGroupedByNode(builder, channel.request(), nodesInCluster.get()); - builder.endObject(); - return new RestResponse(RestStatus.OK, builder); - } - }; - } else if ("parents".equals(groupBy)) { - return new RestBuilderListener(channel) { - @Override - public RestResponse buildResponse(T response, XContentBuilder builder) throws Exception { - builder.startObject(); - response.toXContentGroupedByParents(builder, channel.request()); - builder.endObject(); - return new RestResponse(RestStatus.OK, builder); - } - }; - } else if ("none".equals(groupBy)) { - return new RestToXContentListener<>(channel); - } else { - throw new IllegalArgumentException("[group_by] must be one of [nodes], [parents] or [none] but was [" + groupBy + "]"); - } + final var listener = new RestChunkedToXContentListener<>(channel); + return switch (groupBy) { + case "nodes" -> listener.map(response -> response.groupedByNode(nodesInCluster)); + case "parents" -> listener.map(response -> response.groupedByParent()); + case "none" -> listener.map(response -> response.groupedByNone()); + default -> throw new IllegalArgumentException( + "[group_by] must be one of [nodes], [parents] or [none] but was [" + groupBy + "]" + ); + }; } @Override diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TransportTasksActionTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TransportTasksActionTests.java index 87ffd5ea5be9..1b45b7f6811c 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TransportTasksActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TransportTasksActionTests.java @@ -33,6 +33,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.tasks.CancellableTask; import org.elasticsearch.tasks.Task; @@ -889,14 +890,12 @@ public void testTasksToXContentGrouping() throws Exception { private Map serialize(ListTasksResponse response, boolean byParents) throws IOException { XContentBuilder builder = XContentFactory.contentBuilder(XContentType.JSON); - builder.startObject(); if (byParents) { DiscoveryNodes nodes = testNodes[0].clusterService.state().nodes(); - response.toXContentGroupedByNode(builder, ToXContent.EMPTY_PARAMS, nodes); + ChunkedToXContent.wrapAsXContentObject(response.groupedByNode(() -> nodes)).toXContent(builder, ToXContent.EMPTY_PARAMS); } else { - response.toXContentGroupedByParents(builder, ToXContent.EMPTY_PARAMS); + ChunkedToXContent.wrapAsXContentObject(response.groupedByParent()).toXContent(builder, ToXContent.EMPTY_PARAMS); } - builder.endObject(); builder.flush(); logger.info(Strings.toString(builder)); return XContentHelper.convertToMap(BytesReference.bytes(builder), false, builder.contentType()).v2(); diff --git a/server/src/test/java/org/elasticsearch/tasks/CancelTasksResponseTests.java b/server/src/test/java/org/elasticsearch/tasks/CancelTasksResponseTests.java index c44611c0eedf..6e76b822f03a 100644 --- a/server/src/test/java/org/elasticsearch/tasks/CancelTasksResponseTests.java +++ b/server/src/test/java/org/elasticsearch/tasks/CancelTasksResponseTests.java @@ -13,8 +13,11 @@ import org.elasticsearch.action.admin.cluster.node.tasks.cancel.CancelTasksResponse; import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.test.AbstractXContentTestCase; import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xcontent.ToXContentObject; +import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; @@ -27,12 +30,20 @@ import static org.hamcrest.Matchers.equalTo; -public class CancelTasksResponseTests extends AbstractXContentTestCase { +public class CancelTasksResponseTests extends AbstractXContentTestCase { + + // CancelTasksResponse doesn't directly implement ToXContent because it has multiple XContent representations, so we must wrap here + public record CancelTasksResponseWrapper(CancelTasksResponse in) implements ToXContentObject { + @Override + public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { + return ChunkedToXContent.wrapAsXContentObject(in.groupedByNone()).toXContent(builder, params); + } + } @Override - protected CancelTasksResponse createTestInstance() { + protected CancelTasksResponseWrapper createTestInstance() { List randomTasks = randomTasks(); - return new CancelTasksResponse(randomTasks, Collections.emptyList(), Collections.emptyList()); + return new CancelTasksResponseWrapper(new CancelTasksResponse(randomTasks, Collections.emptyList(), Collections.emptyList())); } private static List randomTasks() { @@ -50,7 +61,9 @@ protected Predicate getRandomFieldsExcludeFilter() { } @Override - protected void assertEqualInstances(CancelTasksResponse expectedInstance, CancelTasksResponse newInstance) { + protected void assertEqualInstances(CancelTasksResponseWrapper expectedInstanceWrapper, CancelTasksResponseWrapper newInstanceWrapper) { + final var expectedInstance = expectedInstanceWrapper.in(); + final var newInstance = newInstanceWrapper.in(); assertNotSame(expectedInstance, newInstance); assertThat(newInstance.getTasks(), equalTo(expectedInstance.getTasks())); ListTasksResponseTests.assertOnNodeFailures(newInstance.getNodeFailures(), expectedInstance.getNodeFailures()); @@ -58,8 +71,8 @@ protected void assertEqualInstances(CancelTasksResponse expectedInstance, Cancel } @Override - protected CancelTasksResponse doParseInstance(XContentParser parser) { - return CancelTasksResponse.fromXContent(parser); + protected CancelTasksResponseWrapper doParseInstance(XContentParser parser) { + return new CancelTasksResponseWrapper(CancelTasksResponse.fromXContent(parser)); } @Override @@ -78,7 +91,7 @@ protected boolean assertToXContentEquivalence() { * without failures, and this other test with failures where we disable asserting on xcontent equivalence at the end. */ public void testFromXContentWithFailures() throws IOException { - Supplier instanceSupplier = CancelTasksResponseTests::createTestInstanceWithFailures; + Supplier instanceSupplier = CancelTasksResponseTests::createTestInstanceWithFailures; // with random fields insertion in the inner exceptions, some random stuff may be parsed back as metadata, // but that does not bother our assertions, as we only want to test that we don't break. boolean supportsUnknownFields = true; @@ -98,7 +111,7 @@ public void testFromXContentWithFailures() throws IOException { ); } - private static CancelTasksResponse createTestInstanceWithFailures() { + private static CancelTasksResponseWrapper createTestInstanceWithFailures() { int numNodeFailures = randomIntBetween(0, 3); List nodeFailures = new ArrayList<>(numNodeFailures); for (int i = 0; i < numNodeFailures; i++) { @@ -109,7 +122,7 @@ private static CancelTasksResponse createTestInstanceWithFailures() { for (int i = 0; i < numTaskFailures; i++) { taskFailures.add(new TaskOperationFailure(randomAlphaOfLength(5), randomLong(), new IllegalStateException())); } - return new CancelTasksResponse(randomTasks(), taskFailures, nodeFailures); + return new CancelTasksResponseWrapper(new CancelTasksResponse(randomTasks(), taskFailures, nodeFailures)); } } diff --git a/server/src/test/java/org/elasticsearch/tasks/ListTasksResponseTests.java b/server/src/test/java/org/elasticsearch/tasks/ListTasksResponseTests.java index 49d01f75a4d0..990f6d1a6202 100644 --- a/server/src/test/java/org/elasticsearch/tasks/ListTasksResponseTests.java +++ b/server/src/test/java/org/elasticsearch/tasks/ListTasksResponseTests.java @@ -12,9 +12,13 @@ import org.elasticsearch.action.FailedNodeException; import org.elasticsearch.action.TaskOperationFailure; import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse; +import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.test.AbstractXContentTestCase; import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xcontent.ToXContentObject; +import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; @@ -27,10 +31,20 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; +import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; +import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; -public class ListTasksResponseTests extends AbstractXContentTestCase { +public class ListTasksResponseTests extends AbstractXContentTestCase { + + // ListTasksResponse doesn't directly implement ToXContent because it has multiple XContent representations, so we must wrap here + public record ListTasksResponseWrapper(ListTasksResponse in) implements ToXContentObject { + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return ChunkedToXContent.wrapAsXContentObject(in.groupedByNone()).toXContent(builder, params); + } + } public void testEmptyToString() { assertEquals(""" @@ -79,9 +93,9 @@ public void testNonEmptyToString() { } @Override - protected ListTasksResponse createTestInstance() { + protected ListTasksResponseWrapper createTestInstance() { // failures are tested separately, so we can test xcontent equivalence at least when we have no failures - return new ListTasksResponse(randomTasks(), Collections.emptyList(), Collections.emptyList()); + return new ListTasksResponseWrapper(new ListTasksResponse(randomTasks(), Collections.emptyList(), Collections.emptyList())); } private static List randomTasks() { @@ -93,8 +107,8 @@ private static List randomTasks() { } @Override - protected ListTasksResponse doParseInstance(XContentParser parser) { - return ListTasksResponse.fromXContent(parser); + protected ListTasksResponseWrapper doParseInstance(XContentParser parser) { + return new ListTasksResponseWrapper(ListTasksResponse.fromXContent(parser)); } @Override @@ -109,7 +123,9 @@ protected Predicate getRandomFieldsExcludeFilter() { } @Override - protected void assertEqualInstances(ListTasksResponse expectedInstance, ListTasksResponse newInstance) { + protected void assertEqualInstances(ListTasksResponseWrapper expectedInstanceWrapper, ListTasksResponseWrapper newInstanceWrapper) { + final var expectedInstance = expectedInstanceWrapper.in(); + final var newInstance = newInstanceWrapper.in(); assertNotSame(expectedInstance, newInstance); assertThat(newInstance.getTasks(), equalTo(expectedInstance.getTasks())); assertOnNodeFailures(newInstance.getNodeFailures(), expectedInstance.getNodeFailures()); @@ -149,7 +165,7 @@ protected static void assertOnTaskFailures(List taskFailur * without failures, and this other test with failures where we disable asserting on xcontent equivalence at the end. */ public void testFromXContentWithFailures() throws IOException { - Supplier instanceSupplier = ListTasksResponseTests::createTestInstanceWithFailures; + Supplier instanceSupplier = ListTasksResponseTests::createTestInstanceWithFailures; // with random fields insertion in the inner exceptions, some random stuff may be parsed back as metadata, // but that does not bother our assertions, as we only want to test that we don't break. boolean supportsUnknownFields = true; @@ -165,11 +181,45 @@ public void testFromXContentWithFailures() throws IOException { this::doParseInstance, this::assertEqualInstances, assertToXContentEquivalence, - ToXContent.EMPTY_PARAMS + EMPTY_PARAMS ); } - private static ListTasksResponse createTestInstanceWithFailures() { + public void testChunkedEncoding() throws IOException { + final var response = createTestInstanceWithFailures().in(); + + try (var builder = jsonBuilder()) { + int chunkCount = 0; + final var iterator = response.groupedByNone().toXContentChunked(EMPTY_PARAMS); + while (iterator.hasNext()) { + iterator.next().toXContent(builder, ToXContent.EMPTY_PARAMS); + chunkCount += 1; + } // closing the builder verifies that the XContent is well-formed + assertEquals(response.getTasks().size() + 2, chunkCount); + } + + try (var builder = jsonBuilder()) { + int chunkCount = 0; + final var iterator = response.groupedByParent().toXContentChunked(EMPTY_PARAMS); + while (iterator.hasNext()) { + iterator.next().toXContent(builder, ToXContent.EMPTY_PARAMS); + chunkCount += 1; + } // closing the builder verifies that the XContent is well-formed + assertEquals(response.getTaskGroups().size() + 2, chunkCount); + } + + try (var builder = jsonBuilder()) { + int chunkCount = 0; + final var iterator = response.groupedByNode(() -> DiscoveryNodes.EMPTY_NODES).toXContentChunked(EMPTY_PARAMS); + while (iterator.hasNext()) { + iterator.next().toXContent(builder, ToXContent.EMPTY_PARAMS); + chunkCount += 1; + } // closing the builder verifies that the XContent is well-formed + assertEquals(2 + response.getPerNodeTasks().values().stream().mapToInt(entry -> 2 + entry.size()).sum(), chunkCount); + } + } + + private static ListTasksResponseWrapper createTestInstanceWithFailures() { int numNodeFailures = randomIntBetween(0, 3); List nodeFailures = new ArrayList<>(numNodeFailures); for (int i = 0; i < numNodeFailures; i++) { @@ -180,6 +230,6 @@ private static ListTasksResponse createTestInstanceWithFailures() { for (int i = 0; i < numTaskFailures; i++) { taskFailures.add(new TaskOperationFailure(randomAlphaOfLength(5), randomLong(), new IllegalStateException())); } - return new ListTasksResponse(randomTasks(), taskFailures, nodeFailures); + return new ListTasksResponseWrapper(new ListTasksResponse(randomTasks(), taskFailures, nodeFailures)); } } From 4c15cd8a6af2572e69cee48ab6021e31864197b4 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 28 Nov 2022 09:58:03 +0000 Subject: [PATCH 093/919] Use RestChunkedToXContentListener in RestRecoveryAction (#91954) --- .../action/admin/indices/RestRecoveryAction.java | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestRecoveryAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestRecoveryAction.java index d8a9c9227fa5..a4a5df0674b8 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestRecoveryAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestRecoveryAction.java @@ -9,17 +9,13 @@ package org.elasticsearch.rest.action.admin.indices; import org.elasticsearch.action.admin.indices.recovery.RecoveryRequest; -import org.elasticsearch.action.admin.indices.recovery.RecoveryResponse; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.common.Strings; import org.elasticsearch.rest.BaseRestHandler; -import org.elasticsearch.rest.ChunkedRestResponseBody; import org.elasticsearch.rest.RestRequest; -import org.elasticsearch.rest.RestResponse; -import org.elasticsearch.rest.RestStatus; -import org.elasticsearch.rest.action.RestActionListener; import org.elasticsearch.rest.action.RestCancellableNodeClient; +import org.elasticsearch.rest.action.RestChunkedToXContentListener; import java.io.IOException; import java.util.List; @@ -55,14 +51,6 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC recoveryRequest.indicesOptions(IndicesOptions.fromRequest(request, recoveryRequest.indicesOptions())); return channel -> new RestCancellableNodeClient(client, request.getHttpChannel()).admin() .indices() - .recoveries(recoveryRequest, new RestActionListener<>(channel) { - @Override - protected void processResponse(RecoveryResponse recoveryResponse) throws IOException { - ensureOpen(); - channel.sendResponse( - new RestResponse(RestStatus.OK, ChunkedRestResponseBody.fromXContent(recoveryResponse, request, channel)) - ); - } - }); + .recoveries(recoveryRequest, new RestChunkedToXContentListener<>(channel)); } } From b89f047fe7bd861fdf97ec73c7ffaaad498d47bc Mon Sep 17 00:00:00 2001 From: David Kyle Date: Mon, 28 Nov 2022 10:02:36 +0000 Subject: [PATCH 094/919] [ML] Preserve status code when handling task failures in ml task actions (#91896) Than handling of TaskOperationFailures in ml TransportTasksActions did not use the original status code meaning the error was often reported incorrectly as a 500 internal server error. --- .../xpack/core/ml/utils/ExceptionsHelper.java | 10 +++------- .../xpack/core/ml/utils/ExceptionsHelperTests.java | 10 ++++++++++ .../ml/action/TransportClearDeploymentCacheAction.java | 7 +++---- .../xpack/ml/action/TransportCloseJobAction.java | 4 ++-- .../TransportInferTrainedModelDeploymentAction.java | 4 ++-- .../ml/action/TransportIsolateDatafeedAction.java | 5 +++-- .../xpack/ml/action/TransportJobTaskAction.java | 4 ++-- .../xpack/ml/action/TransportKillProcessAction.java | 4 +--- .../action/TransportStopDataFrameAnalyticsAction.java | 4 ++-- .../xpack/ml/action/TransportStopDatafeedAction.java | 4 ++-- .../TransportStopTrainedModelDeploymentAction.java | 4 ++-- 11 files changed, 32 insertions(+), 28 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/ExceptionsHelper.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/ExceptionsHelper.java index b227a705e2ed..968bbf7fbb22 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/ExceptionsHelper.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/ExceptionsHelper.java @@ -10,6 +10,7 @@ import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.ResourceAlreadyExistsException; import org.elasticsearch.ResourceNotFoundException; +import org.elasticsearch.action.TaskOperationFailure; import org.elasticsearch.action.search.SearchPhaseExecutionException; import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.rest.RestStatus; @@ -85,13 +86,8 @@ public static ElasticsearchStatusException badRequestException(String msg, Objec return new ElasticsearchStatusException(msg, RestStatus.BAD_REQUEST, args); } - public static ElasticsearchStatusException configHasNotBeenMigrated(String verb, String id) { - return new ElasticsearchStatusException( - "cannot {} as the configuration [{}] is temporarily pending migration", - RestStatus.SERVICE_UNAVAILABLE, - verb, - id - ); + public static ElasticsearchStatusException taskOperationFailureToStatusException(TaskOperationFailure failure) { + return new ElasticsearchStatusException(failure.getCause().getMessage(), failure.getStatus(), failure.getCause()); } /** diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/utils/ExceptionsHelperTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/utils/ExceptionsHelperTests.java index e5bd6672ae2a..771a679b9369 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/utils/ExceptionsHelperTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/utils/ExceptionsHelperTests.java @@ -8,9 +8,11 @@ package org.elasticsearch.xpack.core.ml.utils; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.TaskOperationFailure; import org.elasticsearch.action.search.SearchPhaseExecutionException; import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.indices.IndexCreationException; +import org.elasticsearch.rest.RestStatus; import org.elasticsearch.test.ESTestCase; import static org.hamcrest.Matchers.equalTo; @@ -46,4 +48,12 @@ public void testFindSearchExceptionRootCause_GivenWrapperException() { assertThat(rootCauseException.getMessage(), equalTo("cause")); } + + public void testTaskOperationFailureToStatusException() { + var rootCause = new IllegalArgumentException("bah"); + var failure = new TaskOperationFailure("foo", 0L, rootCause); + var convertedException = ExceptionsHelper.taskOperationFailureToStatusException(failure); + assertThat(convertedException.status(), equalTo(RestStatus.BAD_REQUEST)); + assertThat(convertedException.getCause(), sameInstance(rootCause)); + } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportClearDeploymentCacheAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportClearDeploymentCacheAction.java index 735a413b9d3b..79df84d4ab9b 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportClearDeploymentCacheAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportClearDeploymentCacheAction.java @@ -23,14 +23,13 @@ import org.elasticsearch.xpack.core.ml.action.ClearDeploymentCacheAction.Request; import org.elasticsearch.xpack.core.ml.action.ClearDeploymentCacheAction.Response; import org.elasticsearch.xpack.core.ml.inference.assignment.TrainedModelAssignment; +import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import org.elasticsearch.xpack.ml.inference.assignment.TrainedModelAssignmentMetadata; import org.elasticsearch.xpack.ml.inference.deployment.TrainedModelDeploymentTask; import java.util.List; import java.util.Map; -import static org.elasticsearch.ExceptionsHelper.convertToElastic; - public class TransportClearDeploymentCacheAction extends TransportTasksAction { @Inject @@ -59,9 +58,9 @@ protected Response newResponse( List failedNodeExceptions ) { if (taskOperationFailures.isEmpty() == false) { - throw convertToElastic(taskOperationFailures.get(0).getCause()); + throw ExceptionsHelper.taskOperationFailureToStatusException(taskOperationFailures.get(0)); } else if (failedNodeExceptions.isEmpty() == false) { - throw convertToElastic(failedNodeExceptions.get(0)); + throw failedNodeExceptions.get(0); } return new Response(true); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCloseJobAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCloseJobAction.java index 0400418039bf..9e82d3be6c42 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCloseJobAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCloseJobAction.java @@ -479,9 +479,9 @@ protected CloseJobAction.Response newResponse( // otherwise something went wrong if (request.getOpenJobIds().length != tasks.size()) { if (taskOperationFailures.isEmpty() == false) { - throw org.elasticsearch.ExceptionsHelper.convertToElastic(taskOperationFailures.get(0).getCause()); + throw ExceptionsHelper.taskOperationFailureToStatusException(taskOperationFailures.get(0)); } else if (failedNodeExceptions.isEmpty() == false) { - throw org.elasticsearch.ExceptionsHelper.convertToElastic(failedNodeExceptions.get(0)); + throw failedNodeExceptions.get(0); } else { // This can happen when the actual task in the node no longer exists, // which means the job(s) have already been closed. diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportInferTrainedModelDeploymentAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportInferTrainedModelDeploymentAction.java index 8f42e79c4c6c..2191652e1eb8 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportInferTrainedModelDeploymentAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportInferTrainedModelDeploymentAction.java @@ -134,9 +134,9 @@ protected InferTrainedModelDeploymentAction.Response newResponse( List failedNodeExceptions ) { if (taskOperationFailures.isEmpty() == false) { - throw org.elasticsearch.ExceptionsHelper.convertToElastic(taskOperationFailures.get(0).getCause()); + throw ExceptionsHelper.taskOperationFailureToStatusException(taskOperationFailures.get(0)); } else if (failedNodeExceptions.isEmpty() == false) { - throw org.elasticsearch.ExceptionsHelper.convertToElastic(failedNodeExceptions.get(0)); + throw failedNodeExceptions.get(0); } else if (tasks.isEmpty()) { throw new ElasticsearchStatusException( "Unable to find deployment task for model [{}] please stop and start the deployment or try again momentarily", diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportIsolateDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportIsolateDatafeedAction.java index 9679147b5672..ee6a767501d4 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportIsolateDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportIsolateDatafeedAction.java @@ -19,6 +19,7 @@ import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.ml.MlTasks; import org.elasticsearch.xpack.core.ml.action.IsolateDatafeedAction; +import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import org.elasticsearch.xpack.ml.MachineLearning; import java.util.List; @@ -71,9 +72,9 @@ protected IsolateDatafeedAction.Response newResponse( assert taskOperationFailures.size() <= 1 : "more than 1 item in taskOperationFailures: " + taskOperationFailures.size(); assert failedNodeExceptions.size() <= 1 : "more than 1 item in failedNodeExceptions: " + failedNodeExceptions.size(); if (taskOperationFailures.isEmpty() == false) { - throw org.elasticsearch.ExceptionsHelper.convertToElastic(taskOperationFailures.get(0).getCause()); + throw ExceptionsHelper.taskOperationFailureToStatusException(taskOperationFailures.get(0)); } else if (failedNodeExceptions.isEmpty() == false) { - throw org.elasticsearch.ExceptionsHelper.convertToElastic(failedNodeExceptions.get(0)); + throw failedNodeExceptions.get(0); } else if (tasks.isEmpty() == false) { return tasks.get(0); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportJobTaskAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportJobTaskAction.java index 91494d9e11b5..e337a3606926 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportJobTaskAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportJobTaskAction.java @@ -86,9 +86,9 @@ static Response selectFirst( // the actionlistener's onFailure if (tasks.isEmpty()) { if (taskOperationFailures.isEmpty() == false) { - throw org.elasticsearch.ExceptionsHelper.convertToElastic(taskOperationFailures.get(0).getCause()); + throw ExceptionsHelper.taskOperationFailureToStatusException(taskOperationFailures.get(0)); } else if (failedNodeExceptions.isEmpty() == false) { - throw org.elasticsearch.ExceptionsHelper.convertToElastic(failedNodeExceptions.get(0)); + throw failedNodeExceptions.get(0); } else { throw new IllegalStateException("No errors or response"); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportKillProcessAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportKillProcessAction.java index fbe0cb8597ad..6fb9a05ea09c 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportKillProcessAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportKillProcessAction.java @@ -70,9 +70,7 @@ protected KillProcessAction.Response newResponse( List failedNodeExceptions ) { org.elasticsearch.ExceptionsHelper.rethrowAndSuppress( - taskOperationFailures.stream() - .map(t -> org.elasticsearch.ExceptionsHelper.convertToElastic(t.getCause())) - .collect(Collectors.toList()) + taskOperationFailures.stream().map(ExceptionsHelper::taskOperationFailureToStatusException).collect(Collectors.toList()) ); org.elasticsearch.ExceptionsHelper.rethrowAndSuppress(failedNodeExceptions); return new KillProcessAction.Response(true); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDataFrameAnalyticsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDataFrameAnalyticsAction.java index 76fffbd9607f..cc4939234d3a 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDataFrameAnalyticsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDataFrameAnalyticsAction.java @@ -356,9 +356,9 @@ protected StopDataFrameAnalyticsAction.Response newResponse( ) { if (request.getExpandedIds().size() != tasks.size()) { if (taskOperationFailures.isEmpty() == false) { - throw org.elasticsearch.ExceptionsHelper.convertToElastic(taskOperationFailures.get(0).getCause()); + throw ExceptionsHelper.taskOperationFailureToStatusException(taskOperationFailures.get(0)); } else if (failedNodeExceptions.isEmpty() == false) { - throw org.elasticsearch.ExceptionsHelper.convertToElastic(failedNodeExceptions.get(0)); + throw failedNodeExceptions.get(0); } else { // This can happen when the actual task in the node no longer exists, // which means the data frame analytic(s) have already been closed. diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDatafeedAction.java index fccb8bcc2912..8dab7cd168c2 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDatafeedAction.java @@ -520,9 +520,9 @@ protected StopDatafeedAction.Response newResponse( // tasks, otherwise something went wrong if (request.getResolvedStartedDatafeedIds().length != tasks.size()) { if (taskOperationFailures.isEmpty() == false) { - throw org.elasticsearch.ExceptionsHelper.convertToElastic(taskOperationFailures.get(0).getCause()); + throw ExceptionsHelper.taskOperationFailureToStatusException(taskOperationFailures.get(0)); } else if (failedNodeExceptions.isEmpty() == false) { - throw org.elasticsearch.ExceptionsHelper.convertToElastic(failedNodeExceptions.get(0)); + throw failedNodeExceptions.get(0); } else { // This can happen when the local task in the node no longer exists, // which means the datafeed(s) have already been stopped. It can diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopTrainedModelDeploymentAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopTrainedModelDeploymentAction.java index 2f514df0eefa..d035bb6b3da5 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopTrainedModelDeploymentAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopTrainedModelDeploymentAction.java @@ -254,9 +254,9 @@ protected StopTrainedModelDeploymentAction.Response newResponse( List failedNodeExceptions ) { if (taskOperationFailures.isEmpty() == false) { - throw org.elasticsearch.ExceptionsHelper.convertToElastic(taskOperationFailures.get(0).getCause()); + throw ExceptionsHelper.taskOperationFailureToStatusException(taskOperationFailures.get(0)); } else if (failedNodeExceptions.isEmpty() == false) { - throw org.elasticsearch.ExceptionsHelper.convertToElastic(failedNodeExceptions.get(0)); + throw failedNodeExceptions.get(0); } else { return new StopTrainedModelDeploymentAction.Response(true); } From 2e8200a05ad8df9bdcc73dc1b71495a2348a4e1e Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Mon, 28 Nov 2022 11:18:29 +0100 Subject: [PATCH 095/919] Re-enable bwc tests after #91823. (#91881) --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index ef3f945ac412..e1e11e60e110 100644 --- a/build.gradle +++ b/build.gradle @@ -137,9 +137,9 @@ tasks.register("verifyVersions") { * after the backport of the backcompat code is complete. */ -boolean bwc_tests_enabled = false +boolean bwc_tests_enabled = true // place a PR link here when committing bwc changes: -String bwc_tests_disabled_issue = "https://github.com/elastic/elasticsearch/pull/91823" +String bwc_tests_disabled_issue = "" if (bwc_tests_enabled == false) { if (bwc_tests_disabled_issue.isEmpty()) { throw new GradleException("bwc_tests_disabled_issue must be set when bwc_tests_enabled == false") From fb38095598953f1e5eb3ade7272f2c4e1064f7ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Fern=C3=A1ndez=20Casta=C3=B1o?= Date: Mon, 28 Nov 2022 11:25:38 +0100 Subject: [PATCH 096/919] Set default `cluster.routing.allocation.balance.disk_usage` (#91951) Set the default value for `cluster.routing.allocation.balance.disk_usage` setting to `5e-11f` that seem to have less impact on the write load balancing results. --- docs/changelog/91951.yaml | 5 +++++ .../allocation/allocator/BalancedShardsAllocator.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/91951.yaml diff --git a/docs/changelog/91951.yaml b/docs/changelog/91951.yaml new file mode 100644 index 000000000000..ecc2e0ed889f --- /dev/null +++ b/docs/changelog/91951.yaml @@ -0,0 +1,5 @@ +pr: 91951 +summary: Set default `cluster.routing.allocation.balance.disk_usage` +area: Allocation +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocator.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocator.java index 3dfd098bd3b9..600b4fa372ef 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocator.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocator.java @@ -101,7 +101,7 @@ public class BalancedShardsAllocator implements ShardsAllocator { ); public static final Setting DISK_USAGE_BALANCE_FACTOR_SETTING = Setting.floatSetting( "cluster.routing.allocation.balance.disk_usage", - 0.0f, + 5e-11f, 0.0f, Property.Dynamic, Property.NodeScope From 6c643c59fefa98eae8329b1a4b7c95035605f544 Mon Sep 17 00:00:00 2001 From: David Kyle Date: Mon, 28 Nov 2022 10:40:10 +0000 Subject: [PATCH 097/919] [ML] Guard against input sequences that are too long for Question Answering models (#91924) Adds checks and rejects requests where question + span is > max sequence length. --- docs/changelog/91924.yaml | 5 +++ .../core/ml/action/InferModelAction.java | 4 +- .../nlp/tokenizers/NlpTokenizer.java | 32 +++++++++++-- .../nlp/tokenizers/BertTokenizerTests.java | 45 +++++++++++++++++++ 4 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 docs/changelog/91924.yaml diff --git a/docs/changelog/91924.yaml b/docs/changelog/91924.yaml new file mode 100644 index 000000000000..ff2809b9b269 --- /dev/null +++ b/docs/changelog/91924.yaml @@ -0,0 +1,5 @@ +pr: 91924 +summary: Guard against input sequences that are too long for Question Answering models +area: Machine Learning +type: bug +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/InferModelAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/InferModelAction.java index 72e1e2ed5220..e2e957481680 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/InferModelAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/InferModelAction.java @@ -94,7 +94,7 @@ public Request( boolean previouslyLicensed ) { this.modelId = ExceptionsHelper.requireNonNull(modelId, MODEL_ID); - this.objectsToInfer = Collections.unmodifiableList(ExceptionsHelper.requireNonNull(objectsToInfer, "objects_to_infer")); + this.objectsToInfer = Collections.unmodifiableList(ExceptionsHelper.requireNonNull(objectsToInfer, DOCS.getPreferredName())); this.update = ExceptionsHelper.requireNonNull(inferenceConfig, "inference_config"); this.previouslyLicensed = previouslyLicensed; this.timeout = timeout; @@ -112,7 +112,7 @@ public Request( public Request(String modelId, Map objectToInfer, InferenceConfigUpdate update, boolean previouslyLicensed) { this( modelId, - Collections.singletonList(ExceptionsHelper.requireNonNull(objectToInfer, "objects_to_infer")), + Collections.singletonList(ExceptionsHelper.requireNonNull(objectToInfer, DOCS.getPreferredName())), update, TimeValue.MAX_VALUE, previouslyLicensed diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/NlpTokenizer.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/NlpTokenizer.java index 3cdb3b702ae2..abc370e93c57 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/NlpTokenizer.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/NlpTokenizer.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.ml.inference.nlp.tokenizers; import org.elasticsearch.core.Releasable; +import org.elasticsearch.core.Strings; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.BertTokenization; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.MPNetTokenization; import org.elasticsearch.xpack.core.ml.inference.trainedmodel.RobertaTokenization; @@ -227,15 +228,16 @@ public TokenizationResult.Tokens tokenize( * @return tokenization result for the sequence pair */ public List tokenize(String seq1, String seq2, Tokenization.Truncate truncate, int span, int sequenceId) { + if (isWithSpecialTokens() == false) { + throw new IllegalArgumentException("Unable to do sequence pair tokenization without special tokens"); + } + var innerResultSeq1 = innerTokenize(seq1); List tokenIdsSeq1 = innerResultSeq1.tokens; List tokenPositionMapSeq1 = innerResultSeq1.tokenPositionMap; var innerResultSeq2 = innerTokenize(seq2); List tokenIdsSeq2 = innerResultSeq2.tokens; List tokenPositionMapSeq2 = innerResultSeq2.tokenPositionMap; - if (isWithSpecialTokens() == false) { - throw new IllegalArgumentException("Unable to do sequence pair tokenization without special tokens"); - } int extraTokens = getNumExtraTokensForSeqPair(); int numTokens = tokenIdsSeq1.size() + tokenIdsSeq2.size() + extraTokens; @@ -296,6 +298,30 @@ public List tokenize(String seq1, String seq2, Tokeni List seq1TokenIds = tokenIdsSeq1.stream().map(DelimitedToken.Encoded::getEncoding).collect(Collectors.toList()); final int trueMaxSeqLength = maxSequenceLength() - extraTokens - tokenIdsSeq1.size(); + if (trueMaxSeqLength <= 0) { + throw new IllegalArgumentException( + Strings.format( + "Unable to do sequence pair tokenization: the first sequence [%d tokens] " + + "is longer than the max sequence length [%d tokens]", + tokenIdsSeq1.size() + extraTokens, + maxSequenceLength() + ) + ); + } + + if (span > trueMaxSeqLength) { + throw new IllegalArgumentException( + Strings.format( + "Unable to do sequence pair tokenization: the combined first sequence and span length [%d + %d = %d tokens] " + + "is longer than the max sequence length [%d tokens]. Reduce the size of the [span] window.", + tokenIdsSeq1.size(), + span, + tokenIdsSeq1.size() + span, + maxSequenceLength() + ) + ); + } + while (splitEndPos < tokenIdsSeq2.size()) { splitEndPos = Math.min(splitStartPos + trueMaxSeqLength, tokenIdsSeq2.size()); // Make sure we do not end on a word diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/BertTokenizerTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/BertTokenizerTests.java index 6fa7c00425d7..3bb46ec1cedb 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/BertTokenizerTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/BertTokenizerTests.java @@ -19,6 +19,7 @@ import java.util.stream.Collectors; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; @@ -469,6 +470,50 @@ public void testMultiSeqTokenizationWithSpanOnLongInput() { } } + public void testMultiSeqTokenizationWithSpanFirstInputTooLong() { + try ( + BertTokenizer tokenizer = BertTokenizer.builder(TEST_CASED_VOCAB, Tokenization.createDefault()) + .setDoLowerCase(false) + .setWithSpecialTokens(true) + .setMaxSequenceLength(3) + .build() + ) { + IllegalArgumentException iae = expectThrows( + IllegalArgumentException.class, + () -> tokenizer.tokenize("Elasticsearch is fun", "Godzilla my little red car", Tokenization.Truncate.NONE, 2, 0) + ); + assertThat( + iae.getMessage(), + containsString( + "Unable to do sequence pair tokenization: the first sequence [7 tokens] " + + "is longer than the max sequence length [3 tokens]" + ) + ); + } + } + + public void testMultiSeqTokenizationWithSpanPlusFirstInputTooLong() { + try ( + BertTokenizer tokenizer = BertTokenizer.builder(TEST_CASED_VOCAB, Tokenization.createDefault()) + .setDoLowerCase(false) + .setWithSpecialTokens(true) + .setMaxSequenceLength(8) + .build() + ) { + IllegalArgumentException iae = expectThrows( + IllegalArgumentException.class, + () -> tokenizer.tokenize("Elasticsearch is fun", "Godzilla my little red car", Tokenization.Truncate.NONE, 5, 0) + ); + assertThat( + iae.getMessage(), + containsString( + "Unable to do sequence pair tokenization: the combined first sequence and span length [4 + 5 = 9 tokens] " + + "is longer than the max sequence length [8 tokens]. Reduce the size of the [span] window." + ) + ); + } + } + public void testTokenizeLargeInputMultiSequenceTruncation() { try ( BertTokenizer tokenizer = BertTokenizer.builder( From 3a223d933a4e86543949fa2f1113f32a57e32784 Mon Sep 17 00:00:00 2001 From: Pooya Salehi Date: Mon, 28 Nov 2022 11:51:51 +0100 Subject: [PATCH 098/919] Prevalidate node removal API (pt. 2) (#91256) This PR extends the basic Prevalidation API so that in case there are red non-searchable-snapshot indices in the cluster, we reach out to the nodes (whose removal is being prevalidated) to find out if they have a local copy of any red indices. Closes #87776 --- docs/changelog/91256.yaml | 6 + .../cluster/prevalidate-node-removal.asciidoc | 53 ++++- .../_internal.prevalidate_node_removal.json | 4 + .../10_basic.yml | 6 +- .../cluster/PrevalidateNodeRemovalIT.java | 164 ++++++++++++-- .../cluster/PrevalidateShardPathIT.java | 74 +++++++ .../elasticsearch/action/ActionModule.java | 2 + .../NodePrevalidateShardPathRequest.java | 59 +++++ .../NodePrevalidateShardPathResponse.java | 57 +++++ .../shutdown/NodesRemovalPrevalidation.java | 46 +++- .../PrevalidateNodeRemovalRequest.java | 16 ++ .../shutdown/PrevalidateShardPathRequest.java | 59 +++++ .../PrevalidateShardPathResponse.java | 43 ++++ ...TransportPrevalidateNodeRemovalAction.java | 207 +++++++++++++----- .../TransportPrevalidateShardPathAction.java | 126 +++++++++++ .../RestPrevalidateNodeRemovalAction.java | 1 + ...ateShardPathRequestSerializationTests.java | 36 +++ ...teShardPathResponseSerializationTests.java | 53 +++++ ...emovalPrevalidationSerializationTests.java | 7 +- ...ateShardPathRequestSerializationTests.java | 84 +++++++ ...movalWithSearchableSnapshotIntegTests.java | 3 + .../xpack/security/operator/Constants.java | 3 +- 22 files changed, 1030 insertions(+), 79 deletions(-) create mode 100644 docs/changelog/91256.yaml create mode 100644 server/src/internalClusterTest/java/org/elasticsearch/cluster/PrevalidateShardPathIT.java create mode 100644 server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodePrevalidateShardPathRequest.java create mode 100644 server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodePrevalidateShardPathResponse.java create mode 100644 server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/PrevalidateShardPathRequest.java create mode 100644 server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/PrevalidateShardPathResponse.java create mode 100644 server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/TransportPrevalidateShardPathAction.java create mode 100644 server/src/test/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodePrevalidateShardPathRequestSerializationTests.java create mode 100644 server/src/test/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodePrevalidateShardPathResponseSerializationTests.java create mode 100644 server/src/test/java/org/elasticsearch/action/admin/cluster/node/shutdown/PrevalidateShardPathRequestSerializationTests.java diff --git a/docs/changelog/91256.yaml b/docs/changelog/91256.yaml new file mode 100644 index 000000000000..3c829465073b --- /dev/null +++ b/docs/changelog/91256.yaml @@ -0,0 +1,6 @@ +pr: 91256 +summary: Prevalidate node removal API (pt. 2) +area: Allocation +type: enhancement +issues: + - 87776 diff --git a/docs/reference/cluster/prevalidate-node-removal.asciidoc b/docs/reference/cluster/prevalidate-node-removal.asciidoc index b6312e37f025..d7f0ed64d6c0 100644 --- a/docs/reference/cluster/prevalidate-node-removal.asciidoc +++ b/docs/reference/cluster/prevalidate-node-removal.asciidoc @@ -21,7 +21,9 @@ Prevalidate node removal. [[prevalidate-node-removal-api-desc]] ==== {api-description-title} -This API checks whether attempting to remove the specified node(s) from the cluster is likely to succeed or not. For a cluster with no unassigned shards, removal of any node is considered safe which means the removal of the nodes is likely to succeed. In case the cluster has a <>, it verifies that the removal of the node(s) would not risk removing the last remaining copy of an unassigned shard. +This API checks whether attempting to remove the specified node(s) from the cluster is likely to succeed or not. For a cluster with no unassigned shards, removal of any node is considered safe which means the removal of the nodes is likely to succeed. + +In case the cluster has a <>, it verifies that the removal of the node(s) would not risk removing the last remaining copy of an unassigned shard. If there are red indices in the cluster, the API checks whether the red indices are <> indices, and if not, it sends a request to each of nodes specified in the API call to verify whether the nodes might contain local shard copies of the red indices that are not Searchable Snapshot indices. This request is processed on each receiving node, by checking whether the node has a shard directory for any of the red index shards. The response includes the overall safety of the removal of the specified nodes, and a detailed response for each node. The node-specific part of the response also includes more details on why removal of that node might not succeed. @@ -32,7 +34,7 @@ Note that if the prevalidation result for a set of nodes returns `true` (i.e. it [[prevalidate-node-removal-api-query-params]] ==== {api-query-parms-title} -include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=master-timeout] +include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=timeoutparms] `names`:: (Optional, string) Comma-separated list of node names. @@ -54,11 +56,50 @@ include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=master-timeout] `nodes`:: (object) Prevalidation result for the removal of each of the provided nodes. ++ +.Properties of `nodes` +[%collapsible%open] +==== +``:: + (object) Contains information about the removal prevalidation of a specific node. ++ +.Properties of `` +[%collapsible%open] +======= +`id`:: + (string) node ID +`name`:: + (string) node name +`external_id`:: + (string) node external ID +`result`:: + (object) Contains removal prevalidation result of the node. ++ +.Properties of `result` +[%collapsible%open] +======== +`is_safe`:: + (boolean) Whether the removal of the node is considered safe or not. +`reason`:: + (string) A string that specifies the reason why the prevalidation result is considered safe or not. It can be one of the following values: ++ +-- + * `no_problems`: The prevalidation did not find any issues that could prevent the node from being safely removed. + * `no_red_shards_except_searchable_snapshots`: The node can be safely removed as all red indices are searchable snapshot indices and therefore removing a node does not risk removing the last copy of that index from the cluster. + * `no_red_shards_on_node`: The node does not contain any copies of the red non-searchable-snapshot index shards. + * `red_shards_on_node`: The node might contain shard copies of some non-searchable-snapshot red indices. The list of the shards that might be on the node are specified in the `message` field. + * `unable_to_verify_red_shards`: Contacting the node failed or timed out. More details is provided in the `message` field. +-- +`message`:: + (Optional, string) Detailed information about the removal prevalidation result. +======== +======= +==== [[prevalidate-node-removal-api-example]] ==== {api-examples-title} -This example validates whether it is safe to remove the nodes `node1` and `node2`. The response indicates that it is safe to remove `node1`, but it might not be safe to remove `node2`. Therefore, the overall prevalidation of the removal of the two nodes returns `false`. +This example validates whether it is safe to remove the nodes `node1` and `node2`. The response indicates that it is safe to remove `node1`, but it might not be safe to remove `node2` as it might contain copies of the specified red shards. Therefore, the overall prevalidation of the removal of the two nodes returns `false`. [source,console] -------------------------------------------------- @@ -72,7 +113,7 @@ The API returns the following response: -------------------------------------------------- { "is_safe": false, - "message": "cluster health is RED", + "message": "removal of the following nodes might not be safe: [node2-id]", "nodes": [ { "id": "node1-id", @@ -80,6 +121,7 @@ The API returns the following response: "external_id" : "node1-externalId", "result" : { "is_safe": true, + "reason": "no_red_shards_on_node", "message": "" } }, @@ -89,7 +131,8 @@ The API returns the following response: "external_id" : "node2-externalId", "result" : { "is_safe": false, - "message": "node may contain a copy of a red index shard" + "reason": "red_shards_on_node", + "message": "node contains copies of the following red shards: [[indexName][0]]" } } ] diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/_internal.prevalidate_node_removal.json b/rest-api-spec/src/main/resources/rest-api-spec/api/_internal.prevalidate_node_removal.json index 8563c17ceb53..8c945f2894f2 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/_internal.prevalidate_node_removal.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/_internal.prevalidate_node_removal.json @@ -35,6 +35,10 @@ "master_timeout":{ "type":"time", "description":"Explicit operation timeout for connection to master node" + }, + "timeout":{ + "type":"time", + "description":"Explicit operation timeout" } } } diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.prevalidate_node_removal/10_basic.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.prevalidate_node_removal/10_basic.yml index 6e90d00ca595..740836efcdc4 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.prevalidate_node_removal/10_basic.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.prevalidate_node_removal/10_basic.yml @@ -2,8 +2,8 @@ "Prevalidation basic test": - skip: features: contains - version: "- 8.5.99" - reason: "API added in 8.6.0" + version: "- 8.6.99" + reason: "The reason field was introduced in 8.7.0" # Fetch a node ID and stash it in node_id - do: @@ -16,7 +16,7 @@ ids: $node_id - match: { is_safe: true} - - contains: {nodes: {id: "$node_id", result: {is_safe: true, message: ""}}} + - contains: {nodes: {id: "$node_id", result: {is_safe: true, reason: no_problems, message: ""}}} --- "Prevalidation with no node specified": - skip: diff --git a/server/src/internalClusterTest/java/org/elasticsearch/cluster/PrevalidateNodeRemovalIT.java b/server/src/internalClusterTest/java/org/elasticsearch/cluster/PrevalidateNodeRemovalIT.java index 5f68717217a0..19f8a3c8bc87 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/cluster/PrevalidateNodeRemovalIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/cluster/PrevalidateNodeRemovalIT.java @@ -13,17 +13,40 @@ import org.elasticsearch.action.admin.cluster.node.shutdown.PrevalidateNodeRemovalAction; import org.elasticsearch.action.admin.cluster.node.shutdown.PrevalidateNodeRemovalRequest; import org.elasticsearch.action.admin.cluster.node.shutdown.PrevalidateNodeRemovalResponse; +import org.elasticsearch.action.admin.cluster.node.shutdown.TransportPrevalidateShardPathAction; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.common.Priority; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.env.NodeEnvironment; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.index.shard.ShardPath; +import org.elasticsearch.indices.store.IndicesStore; +import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.test.transport.MockTransportService; +import org.elasticsearch.transport.ConnectTransportException; +import org.elasticsearch.transport.TransportService; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CountDownLatch; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.oneOf; +import static org.hamcrest.Matchers.startsWith; @ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 0) public class PrevalidateNodeRemovalIT extends ESIntegTestCase { + @Override + protected Collection> nodePlugins() { + return Arrays.asList(MockTransportService.TestPlugin.class); + } + public void testNodeRemovalFromNonRedCluster() throws Exception { internalCluster().startMasterOnlyNode(); String node1 = internalCluster().startDataOnlyNode(); @@ -45,10 +68,13 @@ public void testNodeRemovalFromNonRedCluster() throws Exception { } PrevalidateNodeRemovalResponse resp = client().execute(PrevalidateNodeRemovalAction.INSTANCE, req.build()).get(); assertTrue(resp.getPrevalidation().isSafe()); + assertThat(resp.getPrevalidation().message(), equalTo("cluster status is not RED")); assertThat(resp.getPrevalidation().nodes().size(), equalTo(1)); NodesRemovalPrevalidation.NodeResult nodeResult = resp.getPrevalidation().nodes().get(0); assertNotNull(nodeResult); assertThat(nodeResult.name(), equalTo(nodeName)); + assertThat(nodeResult.result().reason(), equalTo(NodesRemovalPrevalidation.Reason.NO_PROBLEMS)); + assertThat(nodeResult.result().message(), equalTo("")); assertTrue(nodeResult.result().isSafe()); // Enforce a replica to get unassigned updateIndexSettings(indexName, Settings.builder().put("index.routing.allocation.require._name", node1)); @@ -56,25 +82,145 @@ public void testNodeRemovalFromNonRedCluster() throws Exception { PrevalidateNodeRemovalRequest req2 = PrevalidateNodeRemovalRequest.builder().setNames(node2).build(); PrevalidateNodeRemovalResponse resp2 = client().execute(PrevalidateNodeRemovalAction.INSTANCE, req2).get(); assertTrue(resp2.getPrevalidation().isSafe()); + assertThat(resp2.getPrevalidation().message(), equalTo("cluster status is not RED")); assertThat(resp2.getPrevalidation().nodes().size(), equalTo(1)); NodesRemovalPrevalidation.NodeResult nodeResult2 = resp2.getPrevalidation().nodes().get(0); assertNotNull(nodeResult2); assertThat(nodeResult2.name(), equalTo(node2)); assertTrue(nodeResult2.result().isSafe()); + assertThat(nodeResult2.result().reason(), equalTo(NodesRemovalPrevalidation.Reason.NO_PROBLEMS)); + assertThat(nodeResult2.result().message(), equalTo("")); } - public void testNodeRemovalFromRedCluster() throws Exception { + // Test that in case the nodes that are being prevalidated do not contain copies of any of the + // red shards, their removal is considered to be safe. + public void testNodeRemovalFromRedClusterWithNoLocalShardCopy() throws Exception { internalCluster().startMasterOnlyNode(); - String node1 = internalCluster().startDataOnlyNode(); - String node2 = internalCluster().startDataOnlyNode(); + String nodeWithIndex = internalCluster().startDataOnlyNode(); + List otherNodes = internalCluster().startDataOnlyNodes(randomIntBetween(1, 3)); // Create an index pinned to one node, and then stop that node so the index is RED. String indexName = "test-idx"; createIndex( indexName, - Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1).put("index.routing.allocation.require._name", node1).build() + Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1) + .put("index.routing.allocation.require._name", nodeWithIndex) + .build() ); ensureYellow(indexName); + internalCluster().stopNode(nodeWithIndex); + ensureRed(indexName); + String[] otherNodeNames = otherNodes.toArray(new String[otherNodes.size()]); + PrevalidateNodeRemovalRequest req = PrevalidateNodeRemovalRequest.builder().setNames(otherNodeNames).build(); + PrevalidateNodeRemovalResponse resp = client().execute(PrevalidateNodeRemovalAction.INSTANCE, req).get(); + assertTrue(resp.getPrevalidation().isSafe()); + assertThat(resp.getPrevalidation().message(), equalTo("")); + assertThat(resp.getPrevalidation().nodes().size(), equalTo(otherNodes.size())); + for (NodesRemovalPrevalidation.NodeResult nodeResult : resp.getPrevalidation().nodes()) { + assertThat(nodeResult.name(), oneOf(otherNodeNames)); + assertThat(nodeResult.result().reason(), equalTo(NodesRemovalPrevalidation.Reason.NO_RED_SHARDS_ON_NODE)); + assertTrue(nodeResult.result().isSafe()); + } + } + + public void testNodeRemovalFromRedClusterWithLocalShardCopy() throws Exception { + internalCluster().startMasterOnlyNode(); + String node1 = internalCluster().startDataOnlyNode(); + String node2 = internalCluster().startDataOnlyNode(); + String indexName = "test-idx"; + createIndex( + indexName, + Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0) + .put("index.routing.allocation.require._name", node1) + .build() + ); + ensureGreen(indexName); + // Prevent node1 from removing its local index shard copies upon removal, by blocking + // its ACTION_SHARD_EXISTS requests since after a relocation, the source first waits + // until the shard exists somewhere else, then it removes it locally. + final CountDownLatch shardActiveRequestSent = new CountDownLatch(1); + MockTransportService node1transport = (MockTransportService) internalCluster().getInstance(TransportService.class, node1); + TransportService node2transport = internalCluster().getInstance(TransportService.class, node2); + node1transport.addSendBehavior(node2transport, (connection, requestId, action, request, options) -> { + if (action.equals(IndicesStore.ACTION_SHARD_EXISTS)) { + shardActiveRequestSent.countDown(); + logger.info("prevent shard active request from being sent"); + throw new ConnectTransportException(connection.getNode(), "DISCONNECT: simulated"); + } + connection.sendRequest(requestId, action, request, options); + }); + logger.info("--> move shard from {} to {}, and wait for relocation to finish", node1, node2); + updateIndexSettings(indexName, Settings.builder().put("index.routing.allocation.require._name", node2)); + shardActiveRequestSent.await(); + ensureGreen(indexName); + // To ensure that the index doesn't get relocated back to node1 after stopping node2, we + // index a doc to make the index copy on node1 (in case not deleted after the relocation) stale. + indexDoc(indexName, "some_id", "foo", "bar"); + internalCluster().stopNode(node2); + ensureRed(indexName); + // Ensure that node1 still has data for the unassigned index + NodeEnvironment nodeEnv = internalCluster().getInstance(NodeEnvironment.class, node1); + Index index = internalCluster().clusterService().state().metadata().index(indexName).getIndex(); + ShardPath shardPath = ShardPath.loadShardPath(logger, nodeEnv, new ShardId(index, 0), ""); + assertNotNull("local index shards not found", shardPath); + // Prevalidate removal of node1 + PrevalidateNodeRemovalRequest req = PrevalidateNodeRemovalRequest.builder().setNames(node1).build(); + PrevalidateNodeRemovalResponse resp = client().execute(PrevalidateNodeRemovalAction.INSTANCE, req).get(); + String node1Id = internalCluster().clusterService(node1).localNode().getId(); + assertFalse(resp.getPrevalidation().isSafe()); + assertThat(resp.getPrevalidation().message(), equalTo("removal of the following nodes might not be safe: [" + node1Id + "]")); + assertThat(resp.getPrevalidation().nodes().size(), equalTo(1)); + NodesRemovalPrevalidation.NodeResult nodeResult = resp.getPrevalidation().nodes().get(0); + assertThat(nodeResult.name(), equalTo(node1)); + assertFalse(nodeResult.result().isSafe()); + assertThat(nodeResult.result().reason(), equalTo(NodesRemovalPrevalidation.Reason.RED_SHARDS_ON_NODE)); + assertThat(nodeResult.result().message(), equalTo("node contains copies of the following red shards: [[" + indexName + "][0]]")); + } + + public void testNodeRemovalFromRedClusterWithTimeout() throws Exception { + internalCluster().startMasterOnlyNode(); + String node1 = internalCluster().startDataOnlyNode(); + String node2 = internalCluster().startDataOnlyNode(); + String indexName = "test-index"; + createIndex( + indexName, + Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0) + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1) + .put("index.routing.allocation.require._name", node1) + .build() + ); + ensureGreen(indexName); + // make it red! internalCluster().stopNode(node1); + ensureRed(indexName); + MockTransportService node2TransportService = (MockTransportService) internalCluster().getInstance(TransportService.class, node2); + node2TransportService.addRequestHandlingBehavior( + TransportPrevalidateShardPathAction.ACTION_NAME + "[n]", + (handler, request, channel, task) -> { logger.info("drop the check shards request"); } + ); + PrevalidateNodeRemovalRequest req = PrevalidateNodeRemovalRequest.builder() + .setNames(node2) + .build() + .timeout(TimeValue.timeValueSeconds(1)); + PrevalidateNodeRemovalResponse resp = client().execute(PrevalidateNodeRemovalAction.INSTANCE, req).get(); + assertFalse("prevalidation result should return false", resp.getPrevalidation().isSafe()); + String node2Id = internalCluster().clusterService(node2).localNode().getId(); + assertThat( + resp.getPrevalidation().message(), + equalTo("cannot prevalidate removal of nodes with the following IDs: [" + node2Id + "]") + ); + assertThat(resp.getPrevalidation().nodes().size(), equalTo(1)); + NodesRemovalPrevalidation.NodeResult nodeResult = resp.getPrevalidation().nodes().get(0); + assertThat(nodeResult.name(), equalTo(node2)); + assertFalse(nodeResult.result().isSafe()); + assertThat(nodeResult.result().message(), startsWith("failed contacting the node")); + assertThat(nodeResult.result().reason(), equalTo(NodesRemovalPrevalidation.Reason.UNABLE_TO_VERIFY)); + } + + private void ensureRed(String indexName) throws Exception { assertBusy(() -> { ClusterHealthResponse healthResponse = client().admin() .cluster() @@ -85,15 +231,5 @@ public void testNodeRemovalFromRedCluster() throws Exception { .actionGet(); assertThat(healthResponse.getStatus(), equalTo(ClusterHealthStatus.RED)); }); - // With a RED non-searchable-snapshot index, node removal is potentially unsafe - // since that node might have the last copy of the unassigned index. - PrevalidateNodeRemovalRequest req = PrevalidateNodeRemovalRequest.builder().setNames(node2).build(); - PrevalidateNodeRemovalResponse resp = client().execute(PrevalidateNodeRemovalAction.INSTANCE, req).get(); - assertFalse(resp.getPrevalidation().isSafe()); - assertThat(resp.getPrevalidation().message(), equalTo("cluster health is RED")); - assertThat(resp.getPrevalidation().nodes().size(), equalTo(1)); - NodesRemovalPrevalidation.NodeResult nodeResult = resp.getPrevalidation().nodes().get(0); - assertThat(nodeResult.name(), equalTo(node2)); - assertFalse(nodeResult.result().isSafe()); } } diff --git a/server/src/internalClusterTest/java/org/elasticsearch/cluster/PrevalidateShardPathIT.java b/server/src/internalClusterTest/java/org/elasticsearch/cluster/PrevalidateShardPathIT.java new file mode 100644 index 000000000000..9b920d31c1a6 --- /dev/null +++ b/server/src/internalClusterTest/java/org/elasticsearch/cluster/PrevalidateShardPathIT.java @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.cluster; + +import org.elasticsearch.action.admin.cluster.node.shutdown.NodePrevalidateShardPathResponse; +import org.elasticsearch.action.admin.cluster.node.shutdown.PrevalidateShardPathRequest; +import org.elasticsearch.action.admin.cluster.node.shutdown.PrevalidateShardPathResponse; +import org.elasticsearch.action.admin.cluster.node.shutdown.TransportPrevalidateShardPathAction; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.routing.ShardRouting; +import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.test.ESIntegTestCase; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.equalTo; + +@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 0) +public class PrevalidateShardPathIT extends ESIntegTestCase { + + public void testCheckShards() throws Exception { + internalCluster().startMasterOnlyNode(); + String node1 = internalCluster().startDataOnlyNode(); + String node2 = internalCluster().startDataOnlyNode(); + String indexName = "index1"; + int index1shards = randomIntBetween(1, 5); + createIndex("index1", Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, index1shards).build()); + ensureGreen(indexName); + var shardIds = clusterService().state() + .routingTable() + .allShards(indexName) + .stream() + .map(ShardRouting::shardId) + .collect(Collectors.toSet()); + String node1Id = internalCluster().clusterService(node1).localNode().getId(); + String node2Id = internalCluster().clusterService(node2).localNode().getId(); + Set shardIdsToCheck = new HashSet<>(shardIds); + boolean includeUnknownShardId = randomBoolean(); + if (includeUnknownShardId) { + shardIdsToCheck.add(new ShardId(randomAlphaOfLength(10), UUIDs.randomBase64UUID(), randomIntBetween(0, 10))); + } + PrevalidateShardPathRequest req = new PrevalidateShardPathRequest(shardIdsToCheck, node1Id, node2Id); + PrevalidateShardPathResponse resp = client().execute(TransportPrevalidateShardPathAction.TYPE, req).get(); + var nodeResponses = resp.getNodes(); + assertThat(nodeResponses.size(), equalTo(2)); + assertThat(nodeResponses.stream().map(r -> r.getNode().getId()).collect(Collectors.toSet()), equalTo(Set.of(node1Id, node2Id))); + assertTrue(resp.failures().isEmpty()); + for (NodePrevalidateShardPathResponse nodeResponse : nodeResponses) { + assertThat(nodeResponse.getShardIds(), equalTo(shardIds)); + } + // Check that after relocation the source node doesn't have the shard path + String node3 = internalCluster().startDataOnlyNode(); + updateIndexSettings(indexName, Settings.builder().put("index.routing.allocation.exclude._name", node2)); + ensureGreen(indexName); + assertBusy(() -> { + // The excluded node should eventually delete the shards + PrevalidateShardPathRequest req2 = new PrevalidateShardPathRequest(shardIdsToCheck, node2Id); + PrevalidateShardPathResponse resp2 = client().execute(TransportPrevalidateShardPathAction.TYPE, req2).get(); + assertThat(resp2.getNodes().size(), equalTo(1)); + assertTrue(resp.failures().isEmpty()); + assertTrue(resp2.getNodes().get(0).getShardIds().isEmpty()); + }); + } +} diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index 172654fc915a..40ee8e4db171 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -41,6 +41,7 @@ import org.elasticsearch.action.admin.cluster.node.reload.TransportNodesReloadSecureSettingsAction; import org.elasticsearch.action.admin.cluster.node.shutdown.PrevalidateNodeRemovalAction; import org.elasticsearch.action.admin.cluster.node.shutdown.TransportPrevalidateNodeRemovalAction; +import org.elasticsearch.action.admin.cluster.node.shutdown.TransportPrevalidateShardPathAction; import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsAction; import org.elasticsearch.action.admin.cluster.node.stats.TransportNodesStatsAction; import org.elasticsearch.action.admin.cluster.node.tasks.cancel.CancelTasksAction; @@ -707,6 +708,7 @@ public void reg actions.register(TransportNodesListShardStoreMetadata.TYPE, TransportNodesListShardStoreMetadata.class); actions.register(TransportShardFlushAction.TYPE, TransportShardFlushAction.class); actions.register(TransportShardRefreshAction.TYPE, TransportShardRefreshAction.class); + actions.register(TransportPrevalidateShardPathAction.TYPE, TransportPrevalidateShardPathAction.class); // desired nodes actions.register(GetDesiredNodesAction.INSTANCE, TransportGetDesiredNodesAction.class); diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodePrevalidateShardPathRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodePrevalidateShardPathRequest.java new file mode 100644 index 000000000000..c7964a8b4dfd --- /dev/null +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodePrevalidateShardPathRequest.java @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.action.admin.cluster.node.shutdown; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.transport.TransportRequest; + +import java.io.IOException; +import java.util.Collection; +import java.util.Objects; +import java.util.Set; + +/** + * A node-specific request derived from the corresponding {@link PrevalidateShardPathRequest}. +*/ +public class NodePrevalidateShardPathRequest extends TransportRequest { + + private final Set shardIds; + + public NodePrevalidateShardPathRequest(Collection shardIds) { + this.shardIds = Set.copyOf(Objects.requireNonNull(shardIds)); + } + + public NodePrevalidateShardPathRequest(StreamInput in) throws IOException { + super(in); + this.shardIds = Set.copyOf(Objects.requireNonNull(in.readSet(ShardId::new))); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeCollection(shardIds, (o, value) -> value.writeTo(o)); + } + + public Set getShardIds() { + return shardIds; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o instanceof NodePrevalidateShardPathRequest == false) return false; + NodePrevalidateShardPathRequest other = (NodePrevalidateShardPathRequest) o; + return Objects.equals(shardIds, other.shardIds); + } + + @Override + public int hashCode() { + return Objects.hash(shardIds); + } +} diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodePrevalidateShardPathResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodePrevalidateShardPathResponse.java new file mode 100644 index 000000000000..9b23439f1c07 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodePrevalidateShardPathResponse.java @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.action.admin.cluster.node.shutdown; + +import org.elasticsearch.action.support.nodes.BaseNodeResponse; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.index.shard.ShardId; + +import java.io.IOException; +import java.util.Objects; +import java.util.Set; + +public class NodePrevalidateShardPathResponse extends BaseNodeResponse { + + private final Set shardIds; + + protected NodePrevalidateShardPathResponse(DiscoveryNode node, Set shardIds) { + super(node); + this.shardIds = Set.copyOf(Objects.requireNonNull(shardIds)); + } + + protected NodePrevalidateShardPathResponse(StreamInput in) throws IOException { + super(in); + shardIds = Set.copyOf(Objects.requireNonNull(in.readSet(ShardId::new))); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeCollection(shardIds); + } + + public Set getShardIds() { + return shardIds; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o instanceof NodePrevalidateShardPathResponse == false) return false; + NodePrevalidateShardPathResponse other = (NodePrevalidateShardPathResponse) o; + return Objects.equals(shardIds, other.shardIds) && Objects.equals(getNode(), other.getNode()); + } + + @Override + public int hashCode() { + return Objects.hash(shardIds, getNode()); + } +} diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodesRemovalPrevalidation.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodesRemovalPrevalidation.java index 0115ee973c1f..eb99f0608f93 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodesRemovalPrevalidation.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodesRemovalPrevalidation.java @@ -119,14 +119,15 @@ public static NodeResult fromXContent(XContentParser parser) throws IOException } // The prevalidation result of a node - public record Result(boolean isSafe, String message) implements ToXContentObject, Writeable { + public record Result(boolean isSafe, Reason reason, String message) implements ToXContentObject, Writeable { private static final ParseField IS_SAFE_FIELD = new ParseField("is_safe"); + private static final ParseField REASON_FIELD = new ParseField("reason"); private static final ParseField MESSAGE_FIELD = new ParseField("message"); private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "nodes_removal_prevalidation_result", - objects -> new Result((boolean) objects[0], (String) objects[1]) + objects -> new Result((boolean) objects[0], Reason.fromString((String) objects[1]), (String) objects[2]) ); static { @@ -135,23 +136,26 @@ public record Result(boolean isSafe, String message) implements ToXContentObject static void configureParser(ConstructingObjectParser parser) { parser.declareBoolean(ConstructingObjectParser.constructorArg(), IS_SAFE_FIELD); + parser.declareString(ConstructingObjectParser.constructorArg(), REASON_FIELD); parser.declareString(ConstructingObjectParser.constructorArg(), MESSAGE_FIELD); } @Override public void writeTo(StreamOutput out) throws IOException { out.writeBoolean(isSafe); + reason.writeTo(out); out.writeString(message); } public static Result readFrom(final StreamInput in) throws IOException { - return new Result(in.readBoolean(), in.readString()); + return new Result(in.readBoolean(), Reason.readFrom(in), in.readString()); } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); builder.field(IS_SAFE_FIELD.getPreferredName(), isSafe); + builder.field(REASON_FIELD.getPreferredName(), reason.reason); builder.field(MESSAGE_FIELD.getPreferredName(), message); builder.endObject(); return builder; @@ -161,4 +165,40 @@ public static Result fromXContent(XContentParser parser) throws IOException { return PARSER.parse(parser, null); } } + + public enum Reason implements Writeable { + NO_PROBLEMS("no_problems"), + NO_RED_SHARDS_ON_NODE("no_red_shards_on_node"), + NO_RED_SHARDS_EXCEPT_SEARCHABLE_SNAPSHOTS("no_red_shards_except_searchable_snapshots"), + RED_SHARDS_ON_NODE("red_shards_on_node"), + UNABLE_TO_VERIFY("unable_to_verify_red_shards"); + + private final String reason; + + Reason(String reason) { + this.reason = reason; + } + + public String reason() { + return reason; + } + + public static Reason readFrom(final StreamInput in) throws IOException { + return fromString(in.readString()); + } + + public static Reason fromString(String s) { + for (Reason r : values()) { + if (s.equalsIgnoreCase(r.reason)) { + return r; + } + } + throw new IllegalArgumentException("unexpected Reason value [" + s + "]"); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(reason); + } + } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/PrevalidateNodeRemovalRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/PrevalidateNodeRemovalRequest.java index 8f5dd9a0f83f..c2581e626576 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/PrevalidateNodeRemovalRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/PrevalidateNodeRemovalRequest.java @@ -13,6 +13,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.core.TimeValue; import java.io.IOException; import java.util.Arrays; @@ -29,6 +30,7 @@ public class PrevalidateNodeRemovalRequest extends MasterNodeReadRequest { + + private final Set shardIds; + + public PrevalidateShardPathRequest(Set shardIds, String... nodeIds) { + super(nodeIds); + this.shardIds = Set.copyOf(Objects.requireNonNull(shardIds)); + } + + public PrevalidateShardPathRequest(StreamInput in) throws IOException { + super(in); + this.shardIds = Set.copyOf(Objects.requireNonNull(in.readSet(ShardId::new))); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeCollection(shardIds); + } + + public Set getShardIds() { + return shardIds; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o instanceof PrevalidateShardPathRequest == false) return false; + PrevalidateShardPathRequest other = (PrevalidateShardPathRequest) o; + return Objects.equals(shardIds, other.shardIds) + && Arrays.equals(nodesIds(), other.nodesIds()) + && Objects.equals(timeout(), other.timeout()); + } + + @Override + public int hashCode() { + return Objects.hash(shardIds, Arrays.hashCode(nodesIds()), timeout()); + } +} diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/PrevalidateShardPathResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/PrevalidateShardPathResponse.java new file mode 100644 index 000000000000..2bbaa6eb2827 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/PrevalidateShardPathResponse.java @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.action.admin.cluster.node.shutdown; + +import org.elasticsearch.action.FailedNodeException; +import org.elasticsearch.action.support.nodes.BaseNodesResponse; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.List; + +public class PrevalidateShardPathResponse extends BaseNodesResponse { + + public PrevalidateShardPathResponse( + ClusterName clusterName, + List nodes, + List failures + ) { + super(clusterName, nodes, failures); + } + + public PrevalidateShardPathResponse(StreamInput in) throws IOException { + super(in); + } + + @Override + protected List readNodesFrom(StreamInput in) throws IOException { + return in.readList(NodePrevalidateShardPathResponse::new); + } + + @Override + protected void writeNodesTo(StreamOutput out, List nodes) throws IOException { + out.writeList(nodes); + } +} diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/TransportPrevalidateNodeRemovalAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/TransportPrevalidateNodeRemovalAction.java index 44a12dd54936..376abf086341 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/TransportPrevalidateNodeRemovalAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/TransportPrevalidateNodeRemovalAction.java @@ -10,8 +10,10 @@ import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.FailedNodeException; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.master.TransportMasterNodeReadAction; +import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.block.ClusterBlockException; import org.elasticsearch.cluster.health.ClusterHealthStatus; @@ -22,12 +24,16 @@ import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.core.Strings; +import org.elasticsearch.core.Tuple; +import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.Logger; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -46,13 +52,16 @@ public class TransportPrevalidateNodeRemovalAction extends TransportMasterNodeRe private static final Logger logger = LogManager.getLogger(TransportPrevalidateNodeRemovalAction.class); + private final NodeClient client; + @Inject public TransportPrevalidateNodeRemovalAction( TransportService transportService, ClusterService clusterService, ThreadPool threadPool, ActionFilters actionFilters, - IndexNameExpressionResolver indexNameExpressionResolver + IndexNameExpressionResolver indexNameExpressionResolver, + NodeClient client ) { super( PrevalidateNodeRemovalAction.NAME, @@ -66,6 +75,7 @@ public TransportPrevalidateNodeRemovalAction( PrevalidateNodeRemovalResponse::new, ThreadPool.Names.SAME ); + this.client = client; } @Override @@ -76,8 +86,8 @@ protected void masterOperation( ActionListener listener ) { try { - Set discoveryNodes = resolveNodes(request, state.nodes()); - doPrevalidation(discoveryNodes, state, listener); + Set requestNodes = resolveNodes(request, state.nodes()); + doPrevalidation(request, requestNodes, state, listener); } catch (Exception e) { listener.onFailure(e); } @@ -141,62 +151,157 @@ protected ClusterBlockException checkBlock(PrevalidateNodeRemovalRequest request } private void doPrevalidation( - Set nodes, + PrevalidateNodeRemovalRequest request, + Set requestNodes, ClusterState clusterState, ActionListener listener ) { - assert nodes != null && nodes.isEmpty() == false; + assert requestNodes != null && requestNodes.isEmpty() == false; - logger.debug(() -> "prevalidate node removal for nodes " + nodes); + logger.debug(() -> "prevalidate node removal for nodes " + requestNodes); ClusterStateHealth clusterStateHealth = new ClusterStateHealth(clusterState); Metadata metadata = clusterState.metadata(); - switch (clusterStateHealth.getStatus()) { - case GREEN, YELLOW -> { - List nodesResults = nodes.stream() - .map(dn -> new NodeResult(dn.getName(), dn.getId(), dn.getExternalId(), new Result(true, ""))) - .toList(); - listener.onResponse( - new PrevalidateNodeRemovalResponse(new NodesRemovalPrevalidation(true, "cluster status is not RED", nodesResults)) - ); - } - case RED -> { - Set redIndices = clusterStateHealth.getIndices() - .entrySet() - .stream() - .filter(entry -> entry.getValue().getStatus() == ClusterHealthStatus.RED) - .map(Map.Entry::getKey) - .collect(Collectors.toSet()); - // If all red indices are searchable snapshot indices, it is safe to remove any node. - Set redNonSSIndices = redIndices.stream() - .map(metadata::index) - .filter(i -> i.isSearchableSnapshot() == false) - .map(im -> im.getIndex().getName()) - .collect(Collectors.toSet()); - if (redNonSSIndices.isEmpty()) { - List nodeResults = nodes.stream() - .map(dn -> new NodeResult(dn.getName(), dn.getId(), dn.getExternalId(), new Result(true, ""))) - .toList(); - listener.onResponse( - new PrevalidateNodeRemovalResponse( - new NodesRemovalPrevalidation(true, "all red indices are searchable snapshot indices", nodeResults) - ) - ); - } else { - List nodeResults = nodes.stream() - .map( - dn -> new NodeResult( - dn.getName(), - dn.getId(), - dn.getExternalId(), - new Result(false, "node may contain a copy of a red index shard") - ) - ) - .toList(); - listener.onResponse( - new PrevalidateNodeRemovalResponse(new NodesRemovalPrevalidation(false, "cluster health is RED", nodeResults)) - ); + DiscoveryNodes clusterNodes = clusterState.getNodes(); + if (clusterStateHealth.getStatus() == ClusterHealthStatus.GREEN || clusterStateHealth.getStatus() == ClusterHealthStatus.YELLOW) { + List nodesResults = requestNodes.stream() + .map( + dn -> new NodeResult( + dn.getName(), + dn.getId(), + dn.getExternalId(), + new Result(true, NodesRemovalPrevalidation.Reason.NO_PROBLEMS, "") + ) + ) + .toList(); + listener.onResponse( + new PrevalidateNodeRemovalResponse(new NodesRemovalPrevalidation(true, "cluster status is not RED", nodesResults)) + ); + return; + } + // RED cluster state + Set redIndices = clusterStateHealth.getIndices() + .entrySet() + .stream() + .filter(entry -> entry.getValue().getStatus() == ClusterHealthStatus.RED) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + // If all red indices are searchable snapshot indices, it is safe to remove any node. + Set redNonSSIndices = redIndices.stream() + .map(metadata::index) + .filter(i -> i.isSearchableSnapshot() == false) + .map(im -> im.getIndex().getName()) + .collect(Collectors.toSet()); + if (redNonSSIndices.isEmpty()) { + List nodeResults = requestNodes.stream() + .map( + dn -> new NodeResult( + dn.getName(), + dn.getId(), + dn.getExternalId(), + new Result(true, NodesRemovalPrevalidation.Reason.NO_RED_SHARDS_EXCEPT_SEARCHABLE_SNAPSHOTS, "") + ) + ) + .toList(); + listener.onResponse( + new PrevalidateNodeRemovalResponse( + new NodesRemovalPrevalidation(true, "all red indices are searchable snapshot indices", nodeResults) + ) + ); + } else { + // Reach out to the nodes to find out whether they contain copies of the red non-searchable-snapshot indices + Set redShards = clusterStateHealth.getIndices() + .entrySet() + .stream() + .filter(indexHealthEntry -> redNonSSIndices.contains(indexHealthEntry.getKey())) + .map(Map.Entry::getValue) // ClusterHealthIndex of red non-searchable-snapshot indices + .flatMap( + redIndexHealth -> redIndexHealth.getShards() + .values() + .stream() + .filter(shardHealth -> shardHealth.getStatus() == ClusterHealthStatus.RED) + .map(redShardHealth -> Tuple.tuple(redIndexHealth.getIndex(), redShardHealth)) + ) // (Index, ClusterShardHealth) of all red shards + .map( + redIndexShardHealthTuple -> new ShardId( + metadata.index(redIndexShardHealthTuple.v1()).getIndex(), + redIndexShardHealthTuple.v2().getShardId() + ) + ) // Convert to ShardId + .collect(Collectors.toSet()); + var nodeIds = requestNodes.stream().map(DiscoveryNode::getId).toList().toArray(new String[0]); + var checkShardsRequest = new PrevalidateShardPathRequest(redShards, nodeIds).timeout(request.timeout()); + client.execute(TransportPrevalidateShardPathAction.TYPE, checkShardsRequest, new ActionListener<>() { + @Override + public void onResponse(PrevalidateShardPathResponse response) { + listener.onResponse(new PrevalidateNodeRemovalResponse(createPrevalidationResult(clusterNodes, response))); + } + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); } + }); + } + } + + private NodesRemovalPrevalidation createPrevalidationResult(DiscoveryNodes nodes, PrevalidateShardPathResponse response) { + List nodeResults = new ArrayList<>(response.getNodes().size() + response.failures().size()); + for (NodePrevalidateShardPathResponse nodeResponse : response.getNodes()) { + Result result; + if (nodeResponse.getShardIds().isEmpty()) { + result = new Result(true, NodesRemovalPrevalidation.Reason.NO_RED_SHARDS_ON_NODE, ""); + } else { + result = new Result( + false, + NodesRemovalPrevalidation.Reason.RED_SHARDS_ON_NODE, + Strings.format("node contains copies of the following red shards: %s", nodeResponse.getShardIds()) + ); } + nodeResults.add( + new NodeResult( + nodeResponse.getNode().getName(), + nodeResponse.getNode().getId(), + nodeResponse.getNode().getExternalId(), + result + ) + ); + } + for (FailedNodeException failedResponse : response.failures()) { + DiscoveryNode node = nodes.get(failedResponse.nodeId()); + nodeResults.add( + new NodeResult( + node.getName(), + node.getId(), + node.getExternalId(), + new Result( + false, + NodesRemovalPrevalidation.Reason.UNABLE_TO_VERIFY, + Strings.format("failed contacting the node: %s", failedResponse.getDetailedMessage()) + ) + ) + ); + } + // determine overall result from the node results. + Set unsafeNodeRemovals = response.getNodes() + .stream() + .filter(r -> r.getShardIds().isEmpty() == false) + .map(r -> r.getNode().getId()) + .collect(Collectors.toSet()); + if (unsafeNodeRemovals.isEmpty() == false) { + return new NodesRemovalPrevalidation( + false, + Strings.format("removal of the following nodes might not be safe: %s", unsafeNodeRemovals), + nodeResults + ); + } + if (response.failures().isEmpty() == false) { + Set unknownNodeRemovals = response.failures().stream().map(FailedNodeException::nodeId).collect(Collectors.toSet()); + return new NodesRemovalPrevalidation( + false, + Strings.format("cannot prevalidate removal of nodes with the following IDs: %s", unknownNodeRemovals), + nodeResults + ); } + return new NodesRemovalPrevalidation(true, "", nodeResults); } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/TransportPrevalidateShardPathAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/TransportPrevalidateShardPathAction.java new file mode 100644 index 000000000000..251129795026 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/TransportPrevalidateShardPathAction.java @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.action.admin.cluster.node.shutdown; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionType; +import org.elasticsearch.action.FailedNodeException; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.nodes.TransportNodesAction; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.NodeEnvironment; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.index.shard.ShardPath; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +/** + * Given a set of shard IDs, checks which of those shards have a matching directory in the local data path. + * This is used by {@link PrevalidateNodeRemovalAction} to find out whether a node may contain some copy + * of a specific shard. The response contains a subset of the request shard IDs which are in the cluster state + * of this node and have a matching shard path on the local data path. + */ +public class TransportPrevalidateShardPathAction extends TransportNodesAction< + PrevalidateShardPathRequest, + PrevalidateShardPathResponse, + NodePrevalidateShardPathRequest, + NodePrevalidateShardPathResponse> { + + public static final String ACTION_NAME = "internal:admin/indices/prevalidate_shard_path"; + public static final ActionType TYPE = new ActionType<>(ACTION_NAME, PrevalidateShardPathResponse::new); + private static final Logger logger = LogManager.getLogger(TransportPrevalidateShardPathAction.class); + + private final TransportService transportService; + private final NodeEnvironment nodeEnv; + private final Settings settings; + + @Inject + public TransportPrevalidateShardPathAction( + ThreadPool threadPool, + ClusterService clusterService, + TransportService transportService, + ActionFilters actionFilters, + NodeEnvironment nodeEnv, + Settings settings + ) { + super( + ACTION_NAME, + threadPool, + clusterService, + transportService, + actionFilters, + PrevalidateShardPathRequest::new, + NodePrevalidateShardPathRequest::new, + ThreadPool.Names.MANAGEMENT, + NodePrevalidateShardPathResponse.class + ); + this.transportService = transportService; + this.nodeEnv = nodeEnv; + this.settings = settings; + } + + @Override + protected PrevalidateShardPathResponse newResponse( + PrevalidateShardPathRequest request, + List nodeResponses, + List failures + ) { + return new PrevalidateShardPathResponse(clusterService.getClusterName(), nodeResponses, failures); + } + + @Override + protected NodePrevalidateShardPathRequest newNodeRequest(PrevalidateShardPathRequest request) { + return new NodePrevalidateShardPathRequest(request.getShardIds()); + } + + @Override + protected NodePrevalidateShardPathResponse newNodeResponse(StreamInput in, DiscoveryNode node) throws IOException { + return new NodePrevalidateShardPathResponse(in); + } + + @Override + protected NodePrevalidateShardPathResponse nodeOperation(NodePrevalidateShardPathRequest request, Task task) { + Set localShards = new HashSet<>(); + ShardPath shardPath = null; + // For each shard we only check whether the shard path exists, regardless of whether the content is a valid index or not. + for (ShardId shardId : request.getShardIds()) { + try { + var indexMetadata = clusterService.state().metadata().index(shardId.getIndex()); + String customDataPath = null; + if (indexMetadata != null) { + customDataPath = new IndexSettings(indexMetadata, settings).customDataPath(); + } else { + // The index is not known to this node. This shouldn't happen, but it can be safely ignored for this operation. + logger.warn("node doesn't have metadata for the index [{}]", shardId.getIndex()); + } + shardPath = ShardPath.loadShardPath(logger, nodeEnv, shardId, customDataPath); + if (shardPath != null) { + localShards.add(shardId); + } + } catch (IOException e) { + final String path = shardPath != null ? shardPath.resolveIndex().toString() : ""; + logger.error(() -> String.format(Locale.ROOT, "error loading shard path for shard [%s]", shardId), e); + } + } + return new NodePrevalidateShardPathResponse(transportService.getLocalNode(), localShards); + } +} diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestPrevalidateNodeRemovalAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestPrevalidateNodeRemovalAction.java index 6280abd2f048..01b404e02f0a 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestPrevalidateNodeRemovalAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestPrevalidateNodeRemovalAction.java @@ -44,6 +44,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli .setExternalIds(externalIds) .build(); prevalidationRequest.masterNodeTimeout(request.paramAsTime("master_timeout", prevalidationRequest.masterNodeTimeout())); + prevalidationRequest.timeout(request.paramAsTime("timeout", prevalidationRequest.timeout())); return channel -> client.execute( PrevalidateNodeRemovalAction.INSTANCE, prevalidationRequest, diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodePrevalidateShardPathRequestSerializationTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodePrevalidateShardPathRequestSerializationTests.java new file mode 100644 index 000000000000..6f2c902bf2ab --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodePrevalidateShardPathRequestSerializationTests.java @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.action.admin.cluster.node.shutdown; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; + +import java.io.IOException; + +import static org.elasticsearch.action.admin.cluster.node.shutdown.PrevalidateShardPathRequestSerializationTests.createSetMutation; + +public class NodePrevalidateShardPathRequestSerializationTests extends AbstractWireSerializingTestCase { + + @Override + protected Writeable.Reader instanceReader() { + return NodePrevalidateShardPathRequest::new; + } + + @Override + protected NodePrevalidateShardPathRequest createTestInstance() { + return new NodePrevalidateShardPathRequest(randomSet(0, 50, PrevalidateShardPathRequestSerializationTests::randomShardId)); + } + + @Override + protected NodePrevalidateShardPathRequest mutateInstance(NodePrevalidateShardPathRequest request) throws IOException { + return new NodePrevalidateShardPathRequest( + createSetMutation(request.getShardIds(), PrevalidateShardPathRequestSerializationTests::randomShardId) + ); + } +} diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodePrevalidateShardPathResponseSerializationTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodePrevalidateShardPathResponseSerializationTests.java new file mode 100644 index 000000000000..ff8bfa93a962 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodePrevalidateShardPathResponseSerializationTests.java @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.action.admin.cluster.node.shutdown; + +import org.elasticsearch.Version; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; + +import java.io.IOException; + +import static org.elasticsearch.action.admin.cluster.node.shutdown.PrevalidateShardPathRequestSerializationTests.createSetMutation; + +public class NodePrevalidateShardPathResponseSerializationTests extends AbstractWireSerializingTestCase { + + @Override + protected Writeable.Reader instanceReader() { + return NodePrevalidateShardPathResponse::new; + } + + @Override + protected NodePrevalidateShardPathResponse createTestInstance() { + return getRandomResponse(); + } + + public static NodePrevalidateShardPathResponse getRandomResponse() { + return new NodePrevalidateShardPathResponse( + getRandomNode(), + randomSet(0, 100, PrevalidateShardPathRequestSerializationTests::randomShardId) + ); + } + + public static DiscoveryNode getRandomNode() { + return new DiscoveryNode(randomAlphaOfLength(10), buildNewFakeTransportAddress(), Version.CURRENT); + } + + @Override + protected NodePrevalidateShardPathResponse mutateInstance(NodePrevalidateShardPathResponse response) throws IOException { + if (randomBoolean()) { + return new NodePrevalidateShardPathResponse(getRandomNode(), response.getShardIds()); + } + return new NodePrevalidateShardPathResponse( + response.getNode(), + createSetMutation(response.getShardIds(), PrevalidateShardPathRequestSerializationTests::randomShardId) + ); + } +} diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodesRemovalPrevalidationSerializationTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodesRemovalPrevalidationSerializationTests.java index 54b428bdd2cd..fc11d259b83d 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodesRemovalPrevalidationSerializationTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodesRemovalPrevalidationSerializationTests.java @@ -87,7 +87,6 @@ public static NodeResult mutateNodeResult(final NodeResult instance) { public static NodesRemovalPrevalidation randomNodesRemovalPrevalidation() { int noOfNodes = randomIntBetween(1, 10); List nodes = new ArrayList<>(noOfNodes); - NodesRemovalPrevalidation.Result result = randomResult(); for (int i = 0; i < noOfNodes; i++) { nodes.add( new NodesRemovalPrevalidation.NodeResult( @@ -102,6 +101,10 @@ public static NodesRemovalPrevalidation randomNodesRemovalPrevalidation() { } private static NodesRemovalPrevalidation.Result randomResult() { - return new NodesRemovalPrevalidation.Result(randomBoolean(), randomAlphaOfLengthBetween(0, 1000)); + return new NodesRemovalPrevalidation.Result( + randomBoolean(), + randomFrom(NodesRemovalPrevalidation.Reason.values()), + randomAlphaOfLengthBetween(0, 1000) + ); } } diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/shutdown/PrevalidateShardPathRequestSerializationTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/shutdown/PrevalidateShardPathRequestSerializationTests.java new file mode 100644 index 000000000000..a2491ef53798 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/shutdown/PrevalidateShardPathRequestSerializationTests.java @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.action.admin.cluster.node.shutdown; + +import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.test.AbstractWireSerializingTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.IntFunction; +import java.util.function.Supplier; + +public class PrevalidateShardPathRequestSerializationTests extends AbstractWireSerializingTestCase { + + @Override + protected Writeable.Reader instanceReader() { + return PrevalidateShardPathRequest::new; + } + + @Override + protected PrevalidateShardPathRequest createTestInstance() { + Set shardIds = randomSet(0, 100, PrevalidateShardPathRequestSerializationTests::randomShardId); + String[] nodeIds = randomArray(1, 5, String[]::new, () -> randomAlphaOfLength(20)); + PrevalidateShardPathRequest request = new PrevalidateShardPathRequest(shardIds, nodeIds); + return randomBoolean() ? request : request.timeout(randomTimeValue()); + } + + @Override + protected PrevalidateShardPathRequest mutateInstance(PrevalidateShardPathRequest request) throws IOException { + int i = randomInt(2); + return switch (i) { + case 0 -> new PrevalidateShardPathRequest( + createSetMutation(request.getShardIds(), PrevalidateShardPathRequestSerializationTests::randomShardId), + request.nodesIds() + ).timeout(request.timeout()); + case 1 -> new PrevalidateShardPathRequest( + request.getShardIds(), + createArrayMutation(request.nodesIds(), () -> randomAlphaOfLength(20), String[]::new) + ).timeout(request.timeout()); + case 2 -> new PrevalidateShardPathRequest(request.getShardIds(), request.nodesIds()).timeout( + randomValueOtherThan(request.timeout(), () -> new TimeValue(randomLongBetween(1000, 10000))) + ); + default -> throw new IllegalStateException("unexpected value: " + i); + }; + } + + public static ShardId randomShardId() { + return new ShardId(randomAlphaOfLength(20), UUIDs.randomBase64UUID(), randomIntBetween(0, 25)); + } + + public static void mutateList(List list, Supplier supplier) { + if (list.size() > 0 && randomBoolean()) { + // just remove one + list.remove(randomInt(list.size() - 1)); + } else { + list.add(supplier.get()); + } + } + + public static Set createSetMutation(Set set, Supplier supplier) { + List list = new ArrayList<>(set); + mutateList(list, supplier); + return new HashSet<>(list); + } + + public static T[] createArrayMutation(T[] array, Supplier supplier, IntFunction arrayConstructor) { + List list = new ArrayList<>(Arrays.asList(array)); + mutateList(list, supplier); + return list.toArray(arrayConstructor.apply(list.size())); + } +} diff --git a/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/PrevalidateNodeRemovalWithSearchableSnapshotIntegTests.java b/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/PrevalidateNodeRemovalWithSearchableSnapshotIntegTests.java index 5d62f984be54..44b5d734ed0c 100644 --- a/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/PrevalidateNodeRemovalWithSearchableSnapshotIntegTests.java +++ b/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/PrevalidateNodeRemovalWithSearchableSnapshotIntegTests.java @@ -71,6 +71,7 @@ public void testNodeRemovalFromClusterWihRedSearchableSnapshotIndex() throws Exc } PrevalidateNodeRemovalResponse resp = client().execute(PrevalidateNodeRemovalAction.INSTANCE, req.build()).get(); assertTrue(resp.getPrevalidation().isSafe()); + assertThat(resp.getPrevalidation().message(), equalTo("all red indices are searchable snapshot indices")); assertThat(resp.getPrevalidation().nodes().size(), equalTo(1)); NodesRemovalPrevalidation.NodeResult nodeResult = resp.getPrevalidation().nodes().get(0); assertNotNull(nodeResult); @@ -78,5 +79,7 @@ public void testNodeRemovalFromClusterWihRedSearchableSnapshotIndex() throws Exc assertThat(nodeResult.Id(), not(emptyString())); assertThat(nodeResult.externalId(), not(emptyString())); assertTrue(nodeResult.result().isSafe()); + assertThat(nodeResult.result().reason(), equalTo(NodesRemovalPrevalidation.Reason.NO_RED_SHARDS_EXCEPT_SEARCHABLE_SNAPSHOTS)); + assertThat(nodeResult.result().message(), equalTo("")); } } diff --git a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java index f92f7f89061a..57d3a3aec972 100644 --- a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java +++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java @@ -488,6 +488,7 @@ public class Constants { "internal:cluster/master_history/get", "internal:cluster/coordination_diagnostics/info", "internal:cluster/formation/info", - "internal:gateway/local/started_shards" + "internal:gateway/local/started_shards", + "internal:admin/indices/prevalidate_shard_path" ); } From 7c5b6483a1994fbacbeeb98a47a67e95d288c440 Mon Sep 17 00:00:00 2001 From: Luca Belluccini Date: Mon, 28 Nov 2022 12:55:47 +0000 Subject: [PATCH 099/919] [DOCS] Typo in Search speed (#91934) * [DOCS] Typo in Search speed The PR https://github.com/elastic/elasticsearch/pull/89782 introduced some broken tags to leak in the text * Fix tags * Make all headings discrete Co-authored-by: Abdon Pijpelink --- docs/reference/how-to/search-speed.asciidoc | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/reference/how-to/search-speed.asciidoc b/docs/reference/how-to/search-speed.asciidoc index 04b670c1a724..0db3ca04e99a 100644 --- a/docs/reference/how-to/search-speed.asciidoc +++ b/docs/reference/how-to/search-speed.asciidoc @@ -10,7 +10,7 @@ goes to the filesystem cache so that Elasticsearch can keep hot regions of the index in physical memory. [discrete] -tag::readahead[] +// tag::readahead[] === Avoid page cache thrashing by using modest readahead values on Linux Search can cause a lot of randomized read I/O. When the underlying block @@ -35,7 +35,7 @@ as a transient setting). We recommend a value of `128KiB` for readahead. WARNING: `blockdev` expects values in 512 byte sectors whereas `lsblk` reports values in `KiB`. As an example, to temporarily set readahead to `128KiB` for `/dev/nvme0n1`, specify `blockdev --setra 256 /dev/nvme0n1`. -end::readahead[] +// end::readahead[] [discrete] === Use faster hardware @@ -358,7 +358,7 @@ PUT index } -------------------------------------------------- -tag::warm-fs-cache[] +// tag::warm-fs-cache[] [discrete] === Warm up the filesystem cache @@ -372,7 +372,7 @@ depending on the file extension using the WARNING: Loading data into the filesystem cache eagerly on too many indices or too many files will make search _slower_ if the filesystem cache is not large enough to hold all the data. Use with caution. -end::warm-fs-cache[] +// end::warm-fs-cache[] [discrete] === Use index sorting to speed up conjunctions @@ -424,6 +424,7 @@ be able to cope with `max_failures` node failures at once at most, then the right number of replicas for you is `max(max_failures, ceil(num_nodes / num_primaries) - 1)`. +[discrete] === Tune your queries with the Search Profiler The {ref}/search-profile.html[Profile API] provides detailed information about @@ -438,6 +439,7 @@ Because the Profile API itself adds significant overhead to the query, this information is best used to understand the relative cost of the various query components. It does not provide a reliable measure of actual processing time. +[discrete] [[faster-phrase-queries]] === Faster phrase queries with `index_phrases` @@ -446,6 +448,7 @@ indexes 2-shingles and is automatically leveraged by query parsers to run phrase queries that don't have a slop. If your use-case involves running lots of phrase queries, this can speed up queries significantly. +[discrete] [[faster-prefix-queries]] === Faster prefix queries with `index_prefixes` @@ -454,6 +457,7 @@ indexes prefixes of all terms and is automatically leveraged by query parsers to run prefix queries. If your use-case involves running lots of prefix queries, this can speed up queries significantly. +[discrete] [[faster-filtering-with-constant-keyword]] === Use `constant_keyword` to speed up filtering From 6871283fd173efcbf716f39dd16ca3fb5edca5ac Mon Sep 17 00:00:00 2001 From: Artem Prigoda Date: Mon, 28 Nov 2022 14:00:41 +0100 Subject: [PATCH 100/919] Fix enforcing that only one recovery gets retried and succeeds (#91845) We have to register an explicit handler for an PeerRecoveryTargetService.Actions.FINALIZE event in order to finalizeReceived to be set. Fixes #91087 Fixes #91041 --- .../indices/recovery/IndexRecoveryIT.java | 1 - .../AbstractIndexRecoveryIntegTestCase.java | 23 +++++-------------- .../IndexRecoveryWithSnapshotsIT.java | 1 - 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java b/server/src/internalClusterTest/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java index c86e9db4650c..31544cc63725 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java @@ -798,7 +798,6 @@ private void validateIndexRecoveryState(RecoveryState.Index indexState) { assertThat(indexState.recoveredBytesPercent(), lessThanOrEqualTo(100.0f)); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/91087") public void testTransientErrorsDuringRecoveryAreRetried() throws Exception { final String recoveryActionToBlock = randomFrom( PeerRecoveryTargetService.Actions.PREPARE_TRANSLOG, diff --git a/test/framework/src/main/java/org/elasticsearch/indices/recovery/AbstractIndexRecoveryIntegTestCase.java b/test/framework/src/main/java/org/elasticsearch/indices/recovery/AbstractIndexRecoveryIntegTestCase.java index 02d46839b29e..4d099a96644c 100644 --- a/test/framework/src/main/java/org/elasticsearch/indices/recovery/AbstractIndexRecoveryIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/indices/recovery/AbstractIndexRecoveryIntegTestCase.java @@ -178,12 +178,11 @@ protected void checkTransientErrorsDuringRecoveryAreRetried(String recoveryActio redTransportService.disconnectFromNode(blueTransportService.getLocalDiscoNode()); } }; - TransientReceiveRejected handlingBehavior = new TransientReceiveRejected( - recoveryActionToBlock, - recoveryStarted, - finalizeReceived, - connectionBreaker - ); + TransientReceiveRejected handlingBehavior = new TransientReceiveRejected(recoveryActionToBlock, recoveryStarted, connectionBreaker); + redTransportService.addRequestHandlingBehavior(PeerRecoveryTargetService.Actions.FINALIZE, (handler, request, channel, task) -> { + finalizeReceived.set(true); + handler.messageReceived(request, channel, task); + }); redTransportService.addRequestHandlingBehavior(recoveryActionToBlock, handlingBehavior); try { @@ -580,19 +579,12 @@ private class TransientReceiveRejected implements StubbableTransport.RequestHand private final String actionName; private final AtomicBoolean recoveryStarted; - private final AtomicBoolean finalizeReceived; private final Runnable connectionBreaker; private final AtomicInteger blocksRemaining; - private TransientReceiveRejected( - String actionName, - AtomicBoolean recoveryStarted, - AtomicBoolean finalizeReceived, - Runnable connectionBreaker - ) { + private TransientReceiveRejected(String actionName, AtomicBoolean recoveryStarted, Runnable connectionBreaker) { this.actionName = actionName; this.recoveryStarted = recoveryStarted; - this.finalizeReceived = finalizeReceived; this.connectionBreaker = connectionBreaker; this.blocksRemaining = new AtomicInteger(randomIntBetween(1, 3)); } @@ -605,9 +597,6 @@ public void messageReceived( Task task ) throws Exception { recoveryStarted.set(true); - if (actionName.equals(PeerRecoveryTargetService.Actions.FINALIZE)) { - finalizeReceived.set(true); - } if (blocksRemaining.getAndUpdate(i -> i == 0 ? 0 : i - 1) != 0) { String rejected = "rejected"; String circuit = "circuit"; diff --git a/x-pack/plugin/snapshot-based-recoveries/src/internalClusterTest/java/org/elasticsearch/xpack/snapshotbasedrecoveries/recovery/IndexRecoveryWithSnapshotsIT.java b/x-pack/plugin/snapshot-based-recoveries/src/internalClusterTest/java/org/elasticsearch/xpack/snapshotbasedrecoveries/recovery/IndexRecoveryWithSnapshotsIT.java index a025db2d5dc8..25bfacbf293b 100644 --- a/x-pack/plugin/snapshot-based-recoveries/src/internalClusterTest/java/org/elasticsearch/xpack/snapshotbasedrecoveries/recovery/IndexRecoveryWithSnapshotsIT.java +++ b/x-pack/plugin/snapshot-based-recoveries/src/internalClusterTest/java/org/elasticsearch/xpack/snapshotbasedrecoveries/recovery/IndexRecoveryWithSnapshotsIT.java @@ -22,7 +22,6 @@ protected Collection> nodePlugins() { return CollectionUtils.appendToCopy(super.nodePlugins(), ConfigurableMockSnapshotBasedRecoveriesPlugin.class); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/91087") public void testTransientErrorsDuringRecoveryAreRetried() throws Exception { checkTransientErrorsDuringRecoveryAreRetried(PeerRecoveryTargetService.Actions.RESTORE_FILE_FROM_SNAPSHOT); } From 4b06d9ce86f6ce9463063d91d12358b92b7bed6a Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 28 Nov 2022 15:50:14 +0000 Subject: [PATCH 101/919] Link bug report template to EOL page (#91955) Adds links to https://www.elastic.co/support/eol to the Github bug report template to reduce bug reports involving EOL versions. --- .github/ISSUE_TEMPLATE/bug.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 0bae5de1d0f8..94a5fa5ef138 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -4,12 +4,13 @@ labels: [">bug", "needs:triage"] body: - type: markdown attributes: - value: | + value: > Github is reserved for bug reports and feature requests; it is not the place for general questions. If you have a question or an unconfirmed bug, please visit the [forums](https://discuss.elastic.co/c/elasticsearch). - Please also check your OS is [supported](https://www.elastic.co/support/matrix#show_os). - If it is not, the issue is likely to be closed. + Please also check your OS is [supported](https://www.elastic.co/support/matrix#show_os), + and that the version of Elasticsearch has not passed [end-of-life](https://www.elastic.co/support/eol). + If you are using an unsupported OS or an unsupported version then the issue is likely to be closed. For security vulnerabilities please only send reports to security@elastic.co. See https://www.elastic.co/community/security for more information. @@ -19,7 +20,9 @@ body: id: es_version attributes: label: Elasticsearch Version - description: The version of Elasticsearch you are running, found with `bin/elasticsearch --version` + description: >- + The version of Elasticsearch you are running, found with `bin/elasticsearch --version`. + Ensure this version has not [passed end-of-life](https://www.elastic.co/support/eol). validations: required: true - type: input From 75424c0416ab1440dac39fe6229c50e5e9479ed9 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 28 Nov 2022 15:54:48 +0000 Subject: [PATCH 102/919] Fix up whitespace after #91955 --- .github/ISSUE_TEMPLATE/bug.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 94a5fa5ef138..b434572cf6f3 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -12,9 +12,11 @@ body: and that the version of Elasticsearch has not passed [end-of-life](https://www.elastic.co/support/eol). If you are using an unsupported OS or an unsupported version then the issue is likely to be closed. + For security vulnerabilities please only send reports to security@elastic.co. See https://www.elastic.co/community/security for more information. + Please fill in the following details to help us reproduce the bug: - type: input id: es_version From 29aea35a9efab3194ef6a33c403b821ebe8bda94 Mon Sep 17 00:00:00 2001 From: Pooya Salehi Date: Mon, 28 Nov 2022 17:25:55 +0100 Subject: [PATCH 103/919] Check version for the new fields in the Prevalidation API (#91972) Check version when (de)serializing the new fields in the Prevalidation request/response Closes #91965 --- .../shutdown/NodesRemovalPrevalidation.java | 20 +++++++++++++++---- .../PrevalidateNodeRemovalRequest.java | 9 +++++++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodesRemovalPrevalidation.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodesRemovalPrevalidation.java index eb99f0608f93..0b34c3d91699 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodesRemovalPrevalidation.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/NodesRemovalPrevalidation.java @@ -8,9 +8,11 @@ package org.elasticsearch.action.admin.cluster.node.shutdown; +import org.elasticsearch.Version; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.core.Nullable; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.ToXContentObject; @@ -118,8 +120,11 @@ public static NodeResult fromXContent(XContentParser parser) throws IOException } } - // The prevalidation result of a node - public record Result(boolean isSafe, Reason reason, String message) implements ToXContentObject, Writeable { + /** + * The prevalidation result of a node. + * @param reason is nullable only for BWC between 8.6 and 8.7. In a fully-upgraded 8.7, it should always be non-null. + */ + public record Result(boolean isSafe, @Nullable Reason reason, String message) implements ToXContentObject, Writeable { private static final ParseField IS_SAFE_FIELD = new ParseField("is_safe"); private static final ParseField REASON_FIELD = new ParseField("reason"); @@ -143,11 +148,16 @@ static void configureParser(ConstructingObjectParser parser) { @Override public void writeTo(StreamOutput out) throws IOException { out.writeBoolean(isSafe); - reason.writeTo(out); + if (out.getVersion().onOrAfter(Version.V_8_7_0)) { + reason.writeTo(out); + } out.writeString(message); } public static Result readFrom(final StreamInput in) throws IOException { + if (in.getVersion().before(Version.V_8_7_0)) { + return new Result(in.readBoolean(), null, in.readString()); + } return new Result(in.readBoolean(), Reason.readFrom(in), in.readString()); } @@ -155,7 +165,9 @@ public static Result readFrom(final StreamInput in) throws IOException { public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); builder.field(IS_SAFE_FIELD.getPreferredName(), isSafe); - builder.field(REASON_FIELD.getPreferredName(), reason.reason); + if (reason != null) { + builder.field(REASON_FIELD.getPreferredName(), reason.reason); + } builder.field(MESSAGE_FIELD.getPreferredName(), message); builder.endObject(); return builder; diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/PrevalidateNodeRemovalRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/PrevalidateNodeRemovalRequest.java index c2581e626576..9f65b98799ed 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/PrevalidateNodeRemovalRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/shutdown/PrevalidateNodeRemovalRequest.java @@ -8,6 +8,7 @@ package org.elasticsearch.action.admin.cluster.node.shutdown; +import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.support.master.MasterNodeReadRequest; import org.elasticsearch.common.Strings; @@ -43,7 +44,9 @@ public PrevalidateNodeRemovalRequest(final StreamInput in) throws IOException { names = in.readStringArray(); ids = in.readStringArray(); externalIds = in.readStringArray(); - timeout = in.readTimeValue(); + if (in.getVersion().onOrAfter(Version.V_8_7_0)) { + timeout = in.readTimeValue(); + } } @Override @@ -52,7 +55,9 @@ public void writeTo(StreamOutput out) throws IOException { out.writeStringArray(names); out.writeStringArray(ids); out.writeStringArray(externalIds); - out.writeTimeValue(timeout); + if (out.getVersion().onOrAfter(Version.V_8_7_0)) { + out.writeTimeValue(timeout); + } } @Override From 046592eaf81907c0dd60d56539d18f193ca5e0a6 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Tue, 29 Nov 2022 10:53:29 +0100 Subject: [PATCH 104/919] Implement repair functionality for aliases colliding with indices bug (#91887) Follow up to #91456 implementing an automated fix for indices corrupted in 8.5. --- .../cluster/metadata/IndexMetadata.java | 28 +++++++++--- .../cluster/metadata/IndexMetadataTests.java | 45 ++++++++++++++----- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadata.java index 77ea513099e4..c0e4b4962a79 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadata.java @@ -8,6 +8,8 @@ package org.elasticsearch.cluster.metadata; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.Version; import org.elasticsearch.action.admin.indices.rollover.RolloverInfo; import org.elasticsearch.action.support.ActiveShardCount; @@ -83,6 +85,8 @@ public class IndexMetadata implements Diffable, ToXContentFragment { + private static final Logger logger = LogManager.getLogger(IndexMetadata.class); + public static final ClusterBlock INDEX_READ_ONLY_BLOCK = new ClusterBlock( 5, "index read-only (api)", @@ -1589,7 +1593,7 @@ public IndexMetadata apply(IndexMetadata part) { builder.stats(stats); builder.indexWriteLoadForecast(indexWriteLoadForecast); builder.shardSizeInBytesForecast(shardSizeInBytesForecast); - return builder.build(); + return builder.build(true); } } @@ -1656,7 +1660,7 @@ public static IndexMetadata readFrom(StreamInput in, @Nullable Function IndexMetadata.builder("index") - .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT)) - .numberOfShards(1) - .numberOfReplicas(0) - .putAlias(AliasMetadata.builder("index").build()) - .build() - ); - assertEquals("alias name [index] self-conflicts with index name", iae.getMessage()); + { + final IllegalArgumentException iae = expectThrows( + IllegalArgumentException.class, + () -> IndexMetadata.builder("index") + .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(0) + .putAlias(AliasMetadata.builder("index").build()) + .build(randomBoolean()) + ); + assertEquals("alias name [index] self-conflicts with index name", iae.getMessage()); + } + { + final IllegalArgumentException iae = expectThrows( + IllegalArgumentException.class, + () -> IndexMetadata.builder("index") + .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, Version.V_8_5_0)) + .numberOfShards(1) + .numberOfReplicas(0) + .putAlias(AliasMetadata.builder("index").build()) + .build(false) + ); + assertEquals("alias name [index] self-conflicts with index name", iae.getMessage()); + } + } + + public void testRepairIndexAndAliasWithSameName() { + final IndexMetadata indexMetadata = IndexMetadata.builder("index") + .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, Version.V_8_5_0)) + .numberOfShards(1) + .numberOfReplicas(0) + .putAlias(AliasMetadata.builder("index").build()) + .build(true); + assertThat(indexMetadata.getAliases(), hasKey("index-alias-corrupted-by-8-5")); } private static Settings indexSettingsWithDataTier(String dataTier) { From 58237b2ccf0a6d83bb06a9d61c4afb9afa783bdd Mon Sep 17 00:00:00 2001 From: Thiago Barbosa Date: Tue, 29 Nov 2022 11:49:27 +0100 Subject: [PATCH 105/919] fixing Apache HttpHost url on java-rest doc (#91945) --- docs/java-rest/low-level/usage.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/java-rest/low-level/usage.asciidoc b/docs/java-rest/low-level/usage.asciidoc index 68b91e06d6fa..7638fe2b5178 100644 --- a/docs/java-rest/low-level/usage.asciidoc +++ b/docs/java-rest/low-level/usage.asciidoc @@ -152,7 +152,7 @@ A `RestClient` instance can be built through the corresponding `RestClientBuilder` class, created via `RestClient#builder(HttpHost...)` static method. The only required argument is one or more hosts that the client will communicate with, provided as instances of -https://hc.apache.org/httpcomponents-core-ga/httpcore/apidocs/org/apache/http/HttpHost.html[HttpHost] +https://hc.apache.org/httpcomponents-core-5.2.x/current/httpcore5/apidocs/org/apache/hc/core5/http/HttpHost.html[HttpHost] as follows: ["source","java",subs="attributes,callouts,macros"] From 4026ff2d6e9c2d1e102951c8ecf52970d1bd5419 Mon Sep 17 00:00:00 2001 From: Joe Gallo Date: Tue, 29 Nov 2022 09:03:38 -0500 Subject: [PATCH 106/919] Handle any exception thrown while generating source for an IngestDocument (#91981) --- docs/changelog/91981.yaml | 5 +++++ .../elasticsearch/ingest/IngestService.java | 18 ++++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 docs/changelog/91981.yaml diff --git a/docs/changelog/91981.yaml b/docs/changelog/91981.yaml new file mode 100644 index 000000000000..c34fd6959c8d --- /dev/null +++ b/docs/changelog/91981.yaml @@ -0,0 +1,5 @@ +pr: 91981 +summary: Handle any exception thrown while generating source for an `IngestDocument` +area: Ingest Node +type: bug +issues: [] diff --git a/server/src/main/java/org/elasticsearch/ingest/IngestService.java b/server/src/main/java/org/elasticsearch/ingest/IngestService.java index 95016b688db9..3362aae1b513 100644 --- a/server/src/main/java/org/elasticsearch/ingest/IngestService.java +++ b/server/src/main/java/org/elasticsearch/ingest/IngestService.java @@ -945,10 +945,8 @@ private void innerExecute( boolean ensureNoSelfReferences = ingestDocument.doNoSelfReferencesCheck(); indexRequest.source(ingestDocument.getSource(), indexRequest.getContentType(), ensureNoSelfReferences); } catch (IllegalArgumentException ex) { - // An IllegalArgumentException can be thrown when an ingest - // processor creates a source map that is self-referencing. - // In that case, we catch and wrap the exception so we can - // include which pipeline failed. + // An IllegalArgumentException can be thrown when an ingest processor creates a source map that is self-referencing. + // In that case, we catch and wrap the exception, so we can include which pipeline failed. handler.accept( new IllegalArgumentException( "Failed to generate the source document for ingest pipeline [" + pipeline.getId() + "]", @@ -956,6 +954,18 @@ private void innerExecute( ) ); return; + } catch (Exception ex) { + // If anything goes wrong here, we want to know, and cannot proceed with normal execution. For example, + // *rarely*, a ConcurrentModificationException could be thrown if a pipeline leaks a reference to a shared mutable + // collection, and another indexing thread modifies the shared reference while we're trying to ensure it has + // no self references. + handler.accept( + new RuntimeException( + "Failed to generate the source document for ingest pipeline [" + pipeline.getId() + "]", + ex + ) + ); + return; } Map map; if ((map = metadata.getDynamicTemplates()) != null) { From 788750b7487223b0847e31c10baaa7bb01629ccd Mon Sep 17 00:00:00 2001 From: William Brafford Date: Tue, 29 Nov 2022 11:08:50 -0500 Subject: [PATCH 107/919] Load stable plugins as synthetic modules (#91869) * Load nonmodular stable plugins as ubermodules We can use the ubermodule classloader to load stable plugins if the plugin descriptor does not give us a module to load from the plugin bundle. We create the synthetic module name by munging the plugin name into a suitable form. For testing, we have to provide the uber module classloader read access to the app classloader's unnamed module, where the stable api modules are loaded by default. * Update docs/changelog/91869.yaml --- docs/changelog/91869.yaml | 5 +++ .../elasticsearch/plugins/PluginsService.java | 21 ++++++++++++ .../plugins/UberModuleClassLoader.java | 7 ++++ .../plugins/PluginsServiceTests.java | 33 +++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 docs/changelog/91869.yaml diff --git a/docs/changelog/91869.yaml b/docs/changelog/91869.yaml new file mode 100644 index 000000000000..1e38cfd9dd6a --- /dev/null +++ b/docs/changelog/91869.yaml @@ -0,0 +1,5 @@ +pr: 91869 +summary: Load stable plugins as synthetic modules +area: Infra/Plugins +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/plugins/PluginsService.java b/server/src/main/java/org/elasticsearch/plugins/PluginsService.java index 424f58e2d557..5bad422d8f06 100644 --- a/server/src/main/java/org/elasticsearch/plugins/PluginsService.java +++ b/server/src/main/java/org/elasticsearch/plugins/PluginsService.java @@ -523,12 +523,33 @@ static LayerAndLoader createPlugin( extendedPlugins.stream().map(LoadedPlugin::layer) ).toList(); return createPluginModuleLayer(bundle, pluginParentLoader, parentLayers); + } else if (plugin.isStable()) { + logger.debug(() -> "Loading bundle: " + plugin.getName() + ", non-modular as synthetic module"); + return LayerAndLoader.ofLoader( + UberModuleClassLoader.getInstance( + pluginParentLoader, + ModuleLayer.boot(), + "synthetic." + toModuleName(plugin.getName()), + bundle.allUrls, + Set.of("org.elasticsearch.server") // TODO: instead of denying server, allow only jvm + stable API modules + ) + ); } else { logger.debug(() -> "Loading bundle: " + plugin.getName() + ", non-modular"); return LayerAndLoader.ofLoader(URLClassLoader.newInstance(bundle.urls.toArray(URL[]::new), pluginParentLoader)); } } + // package-visible for testing + static String toModuleName(String name) { + String result = name.replaceAll("\\W+", ".") // replace non-alphanumeric character strings with dots + .replaceAll("(^[^A-Za-z_]*)", "") // trim non-alpha or underscore characters from start + .replaceAll("\\.$", "") // trim trailing dot + .toLowerCase(Locale.getDefault()); + assert ModuleSupport.isPackageName(result); + return result; + } + private static void checkDeprecations( PluginIntrospector inspector, List pluginDescriptors, diff --git a/server/src/main/java/org/elasticsearch/plugins/UberModuleClassLoader.java b/server/src/main/java/org/elasticsearch/plugins/UberModuleClassLoader.java index 0981d30a9225..5f4bec977bcf 100644 --- a/server/src/main/java/org/elasticsearch/plugins/UberModuleClassLoader.java +++ b/server/src/main/java/org/elasticsearch/plugins/UberModuleClassLoader.java @@ -280,6 +280,13 @@ protected Class loadClass(String cn, boolean resolve) throws ClassNotFoundExc } } + // For testing in cases where code must be given access to an unnamed module + void addReadsSystemClassLoaderUnnamedModule() { + moduleController.layer() + .modules() + .forEach(module -> moduleController.addReads(module, ClassLoader.getSystemClassLoader().getUnnamedModule())); + } + /** * Returns the package name for the given class name */ diff --git a/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java b/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java index d06410ff0ac2..5a75a9c89d45 100644 --- a/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java +++ b/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java @@ -17,6 +17,8 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.index.IndexModule; +import org.elasticsearch.plugin.analysis.api.CharFilterFactory; +import org.elasticsearch.plugins.scanners.PluginInfo; import org.elasticsearch.plugins.spi.BarPlugin; import org.elasticsearch.plugins.spi.BarTestService; import org.elasticsearch.plugins.spi.TestService; @@ -823,6 +825,23 @@ public Reader create(Reader reader) { assertThat(pluginInfos.get(0).descriptor().getName(), equalTo("stable-plugin")); assertThat(pluginInfos.get(0).descriptor().isStable(), is(true)); + // check ubermodule classloader usage + Collection stablePluginInfos = pluginService.getStablePluginRegistry() + .getPluginInfosForExtensible("org.elasticsearch.plugin.analysis.api.CharFilterFactory"); + assertThat(stablePluginInfos, hasSize(1)); + ClassLoader stablePluginClassLoader = stablePluginInfos.stream().findFirst().orElseThrow().loader(); + assertThat(stablePluginClassLoader, instanceOf(UberModuleClassLoader.class)); + + if (CharFilterFactory.class.getModule().isNamed() == false) { + // test frameworks run with stable api classes on classpath, so we + // have no choice but to let our class read the unnamed module that + // owns the stable api classes + ((UberModuleClassLoader) stablePluginClassLoader).addReadsSystemClassLoaderUnnamedModule(); + } + + Class stableClass = stablePluginClassLoader.loadClass("p.A"); + assertThat(stableClass.getModule().getName(), equalTo("synthetic.stable.plugin")); + // TODO should we add something to pluginInfos.get(0).pluginApiInfo() ? } finally { closePluginLoaders(pluginService); @@ -838,6 +857,20 @@ public void testCanCreateAClassLoader() { assertEquals(this.getClass().getClassLoader(), loader.getParent()); } + public void testToModuleName() { + assertThat(PluginsService.toModuleName("module.name"), equalTo("module.name")); + assertThat(PluginsService.toModuleName("module-name"), equalTo("module.name")); + assertThat(PluginsService.toModuleName("module-name1"), equalTo("module.name1")); + assertThat(PluginsService.toModuleName("1module-name"), equalTo("module.name")); + assertThat(PluginsService.toModuleName("module-name!"), equalTo("module.name")); + assertThat(PluginsService.toModuleName("module!@#name!"), equalTo("module.name")); + assertThat(PluginsService.toModuleName("!module-name!"), equalTo("module.name")); + assertThat(PluginsService.toModuleName("module_name"), equalTo("module_name")); + assertThat(PluginsService.toModuleName("-module-name-"), equalTo("module.name")); + assertThat(PluginsService.toModuleName("_module_name"), equalTo("_module_name")); + assertThat(PluginsService.toModuleName("_"), equalTo("_")); + } + static final class Loader extends ClassLoader { Loader(ClassLoader parent) { super(parent); From 47641341051c7d52ed218d501ac45467a41c971d Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 29 Nov 2022 17:10:06 +0000 Subject: [PATCH 108/919] Clarify writability in Netty4HttpPipeliningHandler (#91982) TIL we don't really mean that the physical channel is writable in these loops, so this commit adds a couple of comments to record that lesson. --- .../http/netty4/Netty4HttpPipeliningHandler.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpPipeliningHandler.java b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpPipeliningHandler.java index 61c98788f17f..f5a32a0ec768 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpPipeliningHandler.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpPipeliningHandler.java @@ -216,7 +216,8 @@ private void doWrite(ChannelHandlerContext ctx, Netty4ChunkedHttpResponse readyR combiner.add((Future) first); currentChunkedWrite = new ChunkedWrite(combiner, promise, readyResponse); if (enqueueWrite(ctx, readyResponse, first)) { - // we were able to write out the first chunk directly, try writing out subsequent chunks until the channel becomes unwritable + // We were able to write out the first chunk directly, try writing out subsequent chunks until the channel becomes unwritable. + // NB "writable" means there's space in the downstream ChannelOutboundBuffer, we aren't trying to saturate the physical channel. while (ctx.channel().isWritable()) { if (writeChunk(ctx, combiner, readyResponse.body())) { finishChunkedWrite(); @@ -280,6 +281,7 @@ private boolean doFlush(ChannelHandlerContext ctx) throws IOException { return false; } while (channel.isWritable()) { + // NB "writable" means there's space in the downstream ChannelOutboundBuffer, we aren't trying to saturate the physical channel. WriteOperation currentWrite = queuedWrites.poll(); if (currentWrite == null) { doWriteQueued(ctx); From 2b268d359dd049ae4b108fc7e1bdb1257b5dbdb8 Mon Sep 17 00:00:00 2001 From: Nick Canzoneri Date: Tue, 29 Nov 2022 12:10:30 -0500 Subject: [PATCH 109/919] [docs] Update search-settings documentation to reflect the fact that the indices.query.bool.max_clause_count setting has been deprecated (#91811) * Update search-settings documentation to reflect the fact that the indices.query.bool.max_clause_count setting has been deprecated * Fix indentation * Replace Elasticsearch with {es} * Add deprecation entry to release notes Co-authored-by: Abdon Pijpelink --- .../modules/indices/search-settings.asciidoc | 31 ++++++++++--------- docs/reference/release-notes/8.1.0.asciidoc | 3 ++ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/docs/reference/modules/indices/search-settings.asciidoc b/docs/reference/modules/indices/search-settings.asciidoc index 17fe9ce08b54..e43ec076578d 100644 --- a/docs/reference/modules/indices/search-settings.asciidoc +++ b/docs/reference/modules/indices/search-settings.asciidoc @@ -6,23 +6,24 @@ limits. [[indices-query-bool-max-clause-count]] `indices.query.bool.max_clause_count`:: +deprecated:[8.0.0] (<>, integer) -Maximum number of clauses a query can contain. Defaults to `4096`. +This deprecated setting has no effect. + -This setting limits the total number of clauses that a query tree can have. The default of 4096 -is quite high and should normally be sufficient. This limit applies to the rewritten query, so -not only `bool` queries can contribute high numbers of clauses, but also all queries that rewrite -to `bool` queries internally such as `fuzzy` queries. The limit is in place to prevent searches -from becoming too large, and taking up too much CPU and memory. In case you're considering -increasing this setting, make sure you've exhausted all other options to avoid having to do this. -Higher values can lead to performance degradations and memory issues, especially in clusters with -a high load or few resources. - -Elasticsearch offers some tools to avoid running into issues with regards to the maximum number of -clauses such as the <> query, which allows querying many distinct -values while still counting as a single clause, or the <> option -of <> fields, which allows executing prefix queries that expand to a high -number of terms as a single term query. +{es} will now dynamically set the maximum number of allowed clauses in a query, using +a heuristic based on the size of the search thread pool and the size of the heap allocated to +the JVM. This limit has a minimum value of 1024 and will in most cases be larger (for example, +a node with 30Gb RAM and 48 CPUs will have a maximum clause count of around 27,000). Larger +heaps lead to higher values, and larger thread pools result in lower values. ++ +Queries with many clauses should be avoided whenever possible. If you previously bumped this +setting to accommodate heavy queries, you might need to increase the amount of memory available +to {es}, or to reduce the size of your search thread pool so that more memory is +available to each concurrent search. ++ +In previous versions of Lucene you could get around this limit by nesting boolean queries +within each other, but the limit is now based on the total number of leaf queries within the +query as a whole and this workaround will no longer help. [[search-settings-max-buckets]] `search.max_buckets`:: diff --git a/docs/reference/release-notes/8.1.0.asciidoc b/docs/reference/release-notes/8.1.0.asciidoc index 8cce7d90c03e..c1c44871d641 100644 --- a/docs/reference/release-notes/8.1.0.asciidoc +++ b/docs/reference/release-notes/8.1.0.asciidoc @@ -143,6 +143,9 @@ CRUD:: Cluster Coordination:: * Remove last few mentions of Zen discovery {es-pull}80410[#80410] +Search:: +* Deprecate the `indices.query.bool.max_clause_count` node setting {es-pull}81525[#81525] (issue: {es-issue}46433[#46433]) + SQL:: * Deprecate `index_include_frozen` request parameter {es-pull}83943[#83943] (issue: {es-issue}81939[#81939]) From d3fbb3efe65c060f35734ee6a9b3d650b602da29 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 29 Nov 2022 17:24:45 +0000 Subject: [PATCH 110/919] Fix PersistentTasksClusterServiceTests (#92002) Today these tests don't wait for the assertions to run before shutting down the master service, and today if the master service shuts down then work (such as these assertions) is silently dropped. This commit ensures that the assertions have run before the test completes. --- .../PersistentTasksClusterServiceTests.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/persistent/PersistentTasksClusterServiceTests.java b/server/src/test/java/org/elasticsearch/persistent/PersistentTasksClusterServiceTests.java index 5e5d97a7bee2..f58ff9ae5ecd 100644 --- a/server/src/test/java/org/elasticsearch/persistent/PersistentTasksClusterServiceTests.java +++ b/server/src/test/java/org/elasticsearch/persistent/PersistentTasksClusterServiceTests.java @@ -50,6 +50,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -527,7 +528,7 @@ public void testPeriodicRecheckOffMaster() { assertFalse(service.getPeriodicRechecker().isScheduled()); } - public void testUnassignTask() { + public void testUnassignTask() throws InterruptedException { ClusterState clusterState = initialState(); ClusterState.Builder builder = ClusterState.builder(clusterState); PersistentTasksCustomMetadata.Builder tasks = PersistentTasksCustomMetadata.builder( @@ -545,14 +546,18 @@ public void testUnassignTask() { clusterState = builder.metadata(metadata).nodes(nodes).build(); setState(clusterService, clusterState); PersistentTasksClusterService service = createService((params, candidateNodes, currentState) -> new Assignment("_node_2", "test")); + final var countDownLatch = new CountDownLatch(1); service.unassignPersistentTask(unassignedId, tasks.getLastAllocationId(), "unassignment test", ActionListener.wrap(task -> { assertThat(task.getAssignment().getExecutorNode(), is(nullValue())); assertThat(task.getId(), equalTo(unassignedId)); assertThat(task.getAssignment().getExplanation(), equalTo("unassignment test")); + countDownLatch.countDown(); }, e -> fail())); + + assertTrue(countDownLatch.await(10, TimeUnit.SECONDS)); } - public void testUnassignNonExistentTask() { + public void testUnassignNonExistentTask() throws InterruptedException { ClusterState clusterState = initialState(); ClusterState.Builder builder = ClusterState.builder(clusterState); PersistentTasksCustomMetadata.Builder tasks = PersistentTasksCustomMetadata.builder( @@ -568,12 +573,18 @@ public void testUnassignNonExistentTask() { clusterState = builder.metadata(metadata).nodes(nodes).build(); setState(clusterService, clusterState); PersistentTasksClusterService service = createService((params, candidateNodes, currentState) -> new Assignment("_node_2", "test")); + final var countDownLatch = new CountDownLatch(1); service.unassignPersistentTask( "missing-task", tasks.getLastAllocationId(), "unassignment test", - ActionListener.wrap(task -> fail(), e -> assertThat(e, instanceOf(ResourceNotFoundException.class))) + ActionListener.wrap(task -> fail(), e -> { + assertThat(e, instanceOf(ResourceNotFoundException.class)); + countDownLatch.countDown(); + }) ); + + assertTrue(countDownLatch.await(10, TimeUnit.SECONDS)); } public void testTasksNotAssignedToShuttingDownNodes() { From f2512d4694b8b13cab3bbc7d376c8395687c137d Mon Sep 17 00:00:00 2001 From: amyjtechwriter <61687663+amyjtechwriter@users.noreply.github.com> Date: Tue, 29 Nov 2022 17:40:01 +0000 Subject: [PATCH 111/919] [DOCS] fixes issue number 91889 - missing [discrete] header (#91976) * [DOCS] fixes issue number 91889 - missing [discrete] header * Update docs/plugins/plugin-script.asciidoc Co-authored-by: Abdon Pijpelink Co-authored-by: Abdon Pijpelink --- docs/plugins/plugin-script.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/plugins/plugin-script.asciidoc b/docs/plugins/plugin-script.asciidoc index f04c18115230..9d924a8d2606 100644 --- a/docs/plugins/plugin-script.asciidoc +++ b/docs/plugins/plugin-script.asciidoc @@ -188,6 +188,7 @@ purge the configuration files while removing a plugin, use `-p` or `--purge`. This can option can be used after a plugin is removed to remove any lingering configuration files. +[discrete] [[removing-multiple-plugins]] === Removing multiple plugins From 150462c10a59df589a48ab12b0f8123514e14c62 Mon Sep 17 00:00:00 2001 From: Joe Gallo Date: Tue, 29 Nov 2022 16:56:57 -0500 Subject: [PATCH 112/919] Drop the ingest listener call count tracking (#92003) --- .../ingest/CompoundProcessor.java | 40 ++---- .../ingest/ConditionalProcessor.java | 29 +--- .../elasticsearch/ingest/IngestService.java | 124 ++++++++---------- .../org/elasticsearch/ingest/Pipeline.java | 21 +-- 4 files changed, 73 insertions(+), 141 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/ingest/CompoundProcessor.java b/server/src/main/java/org/elasticsearch/ingest/CompoundProcessor.java index 8f1ad6ede6d3..5f62f31eab5e 100644 --- a/server/src/main/java/org/elasticsearch/ingest/CompoundProcessor.java +++ b/server/src/main/java/org/elasticsearch/ingest/CompoundProcessor.java @@ -8,15 +8,12 @@ package org.elasticsearch.ingest; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.core.Tuple; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; import java.util.function.LongSupplier; import java.util.stream.Collectors; @@ -31,8 +28,6 @@ public class CompoundProcessor implements Processor { public static final String ON_FAILURE_PROCESSOR_TAG_FIELD = "on_failure_processor_tag"; public static final String ON_FAILURE_PIPELINE_FIELD = "on_failure_pipeline"; - private static final Logger logger = LogManager.getLogger(CompoundProcessor.class); - private final boolean ignoreFailure; private final List processors; private final List onFailureProcessors; @@ -198,43 +193,24 @@ void innerExecute(int currentProcessor, IngestDocument ingestDocument, final BiC final IngestMetric finalMetric = processorsWithMetrics.get(currentProcessor).v2(); final Processor finalProcessor = processorsWithMetrics.get(currentProcessor).v1(); final IngestDocument finalIngestDocument = ingestDocument; - /* - * Our assumption is that the listener passed to the processor is only ever called once. However, there is no way to enforce - * that in all processors and all the code that they call. If the listener is called more than once it causes problems - * such as the metrics being wrong. The listenerHasBeenCalled variable is used to make sure that the code in the listener - * is only executed once. - */ - final AtomicBoolean listenerHasBeenCalled = new AtomicBoolean(false); finalMetric.preIngest(); - final AtomicBoolean postIngestHasBeenCalled = new AtomicBoolean(false); try { finalProcessor.execute(ingestDocument, (result, e) -> { - if (listenerHasBeenCalled.getAndSet(true)) { - logger.warn("A listener was unexpectedly called more than once", new RuntimeException(e)); - assert false : "A listener was unexpectedly called more than once"; + long ingestTimeInNanos = relativeTimeProvider.getAsLong() - startTimeInNanos; + finalMetric.postIngest(ingestTimeInNanos); + if (e != null) { + executeOnFailureOuter(finalCurrentProcessor, finalIngestDocument, handler, finalProcessor, finalMetric, e); } else { - long ingestTimeInNanos = relativeTimeProvider.getAsLong() - startTimeInNanos; - finalMetric.postIngest(ingestTimeInNanos); - postIngestHasBeenCalled.set(true); - if (e != null) { - executeOnFailureOuter(finalCurrentProcessor, finalIngestDocument, handler, finalProcessor, finalMetric, e); + if (result != null) { + innerExecute(nextProcessor, result, handler); } else { - if (result != null) { - innerExecute(nextProcessor, result, handler); - } else { - handler.accept(null, null); - } + handler.accept(null, null); } } }); } catch (Exception e) { long ingestTimeInNanos = relativeTimeProvider.getAsLong() - startTimeInNanos; - if (postIngestHasBeenCalled.get()) { - logger.warn("Preventing postIngest from being called more than once", e); - assert false : "Attempt to call postIngest more than once"; - } else { - finalMetric.postIngest(ingestTimeInNanos); - } + finalMetric.postIngest(ingestTimeInNanos); executeOnFailureOuter(finalCurrentProcessor, finalIngestDocument, handler, finalProcessor, finalMetric, e); } } diff --git a/server/src/main/java/org/elasticsearch/ingest/ConditionalProcessor.java b/server/src/main/java/org/elasticsearch/ingest/ConditionalProcessor.java index 597307d433d6..528bb402a59e 100644 --- a/server/src/main/java/org/elasticsearch/ingest/ConditionalProcessor.java +++ b/server/src/main/java/org/elasticsearch/ingest/ConditionalProcessor.java @@ -8,8 +8,6 @@ package org.elasticsearch.ingest; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.elasticsearch.common.logging.DeprecationCategory; import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.script.DynamicMap; @@ -28,7 +26,6 @@ import java.util.ListIterator; import java.util.Map; import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.LongSupplier; @@ -48,8 +45,6 @@ public class ConditionalProcessor extends AbstractProcessor implements WrappingP return value; }); - private static final Logger logger = LogManager.getLogger(ConditionalProcessor.class); - static final String TYPE = "conditional"; private final Script condition; @@ -125,27 +120,15 @@ public void execute(IngestDocument ingestDocument, BiConsumer { - if (listenerHasBeenCalled.getAndSet(true)) { - logger.warn("A listener was unexpectedly called more than once", new RuntimeException(e)); - assert false : "A listener was unexpectedly called more than once"; + long ingestTimeInNanos = relativeTimeProvider.getAsLong() - startTimeInNanos; + metric.postIngest(ingestTimeInNanos); + if (e != null) { + metric.ingestFailed(); + handler.accept(null, e); } else { - long ingestTimeInNanos = relativeTimeProvider.getAsLong() - startTimeInNanos; - metric.postIngest(ingestTimeInNanos); - if (e != null) { - metric.ingestFailed(); - handler.accept(null, e); - } else { - handler.accept(result, null); - } + handler.accept(result, null); } }); } else { diff --git a/server/src/main/java/org/elasticsearch/ingest/IngestService.java b/server/src/main/java/org/elasticsearch/ingest/IngestService.java index 3362aae1b513..123731f54258 100644 --- a/server/src/main/java/org/elasticsearch/ingest/IngestService.java +++ b/server/src/main/java/org/elasticsearch/ingest/IngestService.java @@ -72,7 +72,6 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -905,78 +904,63 @@ private void innerExecute( VersionType versionType = indexRequest.versionType(); Map sourceAsMap = indexRequest.sourceAsMap(); IngestDocument ingestDocument = new IngestDocument(index, id, version, routing, versionType, sourceAsMap); - /* - * Our assumption is that the listener passed to the processor is only ever called once. However, there is no way to enforce - * that in all processors and all of the code that they call. If the listener is called more than once it causes problems - * such as the metrics being wrong. The listenerHasBeenCalled variable is used to make sure that the code in the listener - * is only executed once. - */ - final AtomicBoolean listenerHasBeenCalled = new AtomicBoolean(false); ingestDocument.executePipeline(pipeline, (result, e) -> { - if (listenerHasBeenCalled.getAndSet(true)) { - logger.warn("A listener was unexpectedly called more than once", new RuntimeException(e)); - assert false : "A listener was unexpectedly called more than once"; + if (e != null) { + handler.accept(e); + } else if (result == null) { + itemDroppedHandler.accept(slot); + handler.accept(null); } else { - if (e != null) { - handler.accept(e); - } else if (result == null) { - itemDroppedHandler.accept(slot); - handler.accept(null); - } else { - org.elasticsearch.script.Metadata metadata = ingestDocument.getMetadata(); - - // it's fine to set all metadata fields all the time, as ingest document holds their starting values - // before ingestion, which might also get modified during ingestion. - indexRequest.index(metadata.getIndex()); - indexRequest.id(metadata.getId()); - indexRequest.routing(metadata.getRouting()); - indexRequest.version(metadata.getVersion()); - if (metadata.getVersionType() != null) { - indexRequest.versionType(VersionType.fromString(metadata.getVersionType())); - } - Number number; - if ((number = metadata.getIfSeqNo()) != null) { - indexRequest.setIfSeqNo(number.longValue()); - } - if ((number = metadata.getIfPrimaryTerm()) != null) { - indexRequest.setIfPrimaryTerm(number.longValue()); - } - try { - boolean ensureNoSelfReferences = ingestDocument.doNoSelfReferencesCheck(); - indexRequest.source(ingestDocument.getSource(), indexRequest.getContentType(), ensureNoSelfReferences); - } catch (IllegalArgumentException ex) { - // An IllegalArgumentException can be thrown when an ingest processor creates a source map that is self-referencing. - // In that case, we catch and wrap the exception, so we can include which pipeline failed. - handler.accept( - new IllegalArgumentException( - "Failed to generate the source document for ingest pipeline [" + pipeline.getId() + "]", - ex - ) - ); - return; - } catch (Exception ex) { - // If anything goes wrong here, we want to know, and cannot proceed with normal execution. For example, - // *rarely*, a ConcurrentModificationException could be thrown if a pipeline leaks a reference to a shared mutable - // collection, and another indexing thread modifies the shared reference while we're trying to ensure it has - // no self references. - handler.accept( - new RuntimeException( - "Failed to generate the source document for ingest pipeline [" + pipeline.getId() + "]", - ex - ) - ); - return; - } - Map map; - if ((map = metadata.getDynamicTemplates()) != null) { - Map mergedDynamicTemplates = new HashMap<>(indexRequest.getDynamicTemplates()); - mergedDynamicTemplates.putAll(map); - indexRequest.setDynamicTemplates(mergedDynamicTemplates); - } - postIngest(ingestDocument, indexRequest); - - handler.accept(null); + org.elasticsearch.script.Metadata metadata = ingestDocument.getMetadata(); + + // it's fine to set all metadata fields all the time, as ingest document holds their starting values + // before ingestion, which might also get modified during ingestion. + indexRequest.index(metadata.getIndex()); + indexRequest.id(metadata.getId()); + indexRequest.routing(metadata.getRouting()); + indexRequest.version(metadata.getVersion()); + if (metadata.getVersionType() != null) { + indexRequest.versionType(VersionType.fromString(metadata.getVersionType())); + } + Number number; + if ((number = metadata.getIfSeqNo()) != null) { + indexRequest.setIfSeqNo(number.longValue()); + } + if ((number = metadata.getIfPrimaryTerm()) != null) { + indexRequest.setIfPrimaryTerm(number.longValue()); + } + try { + boolean ensureNoSelfReferences = ingestDocument.doNoSelfReferencesCheck(); + indexRequest.source(ingestDocument.getSource(), indexRequest.getContentType(), ensureNoSelfReferences); + } catch (IllegalArgumentException ex) { + // An IllegalArgumentException can be thrown when an ingest processor creates a source map that is self-referencing. + // In that case, we catch and wrap the exception, so we can include which pipeline failed. + handler.accept( + new IllegalArgumentException( + "Failed to generate the source document for ingest pipeline [" + pipeline.getId() + "]", + ex + ) + ); + return; + } catch (Exception ex) { + // If anything goes wrong here, we want to know, and cannot proceed with normal execution. For example, + // *rarely*, a ConcurrentModificationException could be thrown if a pipeline leaks a reference to a shared mutable + // collection, and another indexing thread modifies the shared reference while we're trying to ensure it has + // no self references. + handler.accept( + new RuntimeException("Failed to generate the source document for ingest pipeline [" + pipeline.getId() + "]", ex) + ); + return; } + Map map; + if ((map = metadata.getDynamicTemplates()) != null) { + Map mergedDynamicTemplates = new HashMap<>(indexRequest.getDynamicTemplates()); + mergedDynamicTemplates.putAll(map); + indexRequest.setDynamicTemplates(mergedDynamicTemplates); + } + postIngest(ingestDocument, indexRequest); + + handler.accept(null); } }); } diff --git a/server/src/main/java/org/elasticsearch/ingest/Pipeline.java b/server/src/main/java/org/elasticsearch/ingest/Pipeline.java index a18119bdffba..f4d60ecf5ce3 100644 --- a/server/src/main/java/org/elasticsearch/ingest/Pipeline.java +++ b/server/src/main/java/org/elasticsearch/ingest/Pipeline.java @@ -8,8 +8,6 @@ package org.elasticsearch.ingest; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.core.Nullable; import org.elasticsearch.script.ScriptService; @@ -17,7 +15,6 @@ import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; import java.util.function.LongSupplier; @@ -32,8 +29,6 @@ public final class Pipeline { public static final String ON_FAILURE_KEY = "on_failure"; public static final String META_KEY = "_meta"; - private static final Logger logger = LogManager.getLogger(Pipeline.class); - private final String id; @Nullable private final String description; @@ -119,20 +114,14 @@ public void execute(IngestDocument ingestDocument, BiConsumer { - if (listenerHasBeenCalled.getAndSet(true)) { - logger.warn("A listener was unexpectedly called more than once", new RuntimeException(e)); - assert false : "A listener was unexpectedly called more than once"; - } else { - long ingestTimeInNanos = relativeTimeProvider.getAsLong() - startTimeInNanos; - metrics.postIngest(ingestTimeInNanos); - if (e != null) { - metrics.ingestFailed(); - } - handler.accept(result, e); + long ingestTimeInNanos = relativeTimeProvider.getAsLong() - startTimeInNanos; + metrics.postIngest(ingestTimeInNanos); + if (e != null) { + metrics.ingestFailed(); } + handler.accept(result, e); }); } From da119b0d4dd41a378ffbc6ac90d3987d00ca5170 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 30 Nov 2022 08:47:36 +0100 Subject: [PATCH 113/919] Speedup time_series agg by caching current tsid ordinal, parent bucket ordinal and buck ordinal (#91784) This avoids needlessly adding the same parent bucket ordinal or TSIDs to `BytesKeyedBucketOrds`. Relates to #74660 --- .../timeseries/TimeSeriesAggregator.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/timeseries/TimeSeriesAggregator.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/timeseries/TimeSeriesAggregator.java index 2d1e45183986..d30a825264d0 100644 --- a/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/timeseries/TimeSeriesAggregator.java +++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/timeseries/TimeSeriesAggregator.java @@ -92,8 +92,23 @@ protected void doClose() { protected LeafBucketCollector getLeafCollector(AggregationExecutionContext aggCtx, LeafBucketCollector sub) throws IOException { return new LeafBucketCollectorBase(sub, null) { + // Keeping track of these fields helps to reduce time spent attempting to add bucket + tsid combos that already were added. + long currentTsidOrd = -1; + long currentBucket = -1; + long currentBucketOrdinal; + @Override public void collect(int doc, long bucket) throws IOException { + // Naively comparing bucket against currentBucket and tsid ord to currentBucket can work really well. + // TimeSeriesIndexSearcher ensures that docs are emitted in tsid and timestamp order, so if tsid ordinal + // changes to what is stored in currentTsidOrd then that ordinal well never occur again. Same applies + // currentBucket if there is no parent aggregation or the immediate parent aggregation creates buckets + // based on @timestamp field or dimension fields (fields that make up the tsid). + if (currentBucket == bucket && currentTsidOrd == aggCtx.getTsidOrd()) { + collectExistingBucket(sub, doc, currentBucketOrdinal); + return; + } + long bucketOrdinal = bucketOrds.add(bucket, aggCtx.getTsid()); if (bucketOrdinal < 0) { // already seen bucketOrdinal = -1 - bucketOrdinal; @@ -101,6 +116,10 @@ public void collect(int doc, long bucket) throws IOException { } else { collectBucket(sub, doc, bucketOrdinal); } + + currentBucketOrdinal = bucketOrdinal; + currentTsidOrd = aggCtx.getTsidOrd(); + currentBucket = bucket; } }; } From 015e7fb3da60225c776ecd0e437bae072e27e1df Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 30 Nov 2022 10:00:02 +0000 Subject: [PATCH 114/919] Add chunking to ClusterState.Custom impls (#91963) Still combines the chunks together at the upper level, but this is a step towards full chunking support for `GET _cluster/state`. Relates #89838 --- .../http/ClusterStateRestCancellationIT.java | 5 +- .../cluster/SimpleClusterStateIT.java | 6 +- .../elasticsearch/cluster/ClusterState.java | 9 +- .../cluster/RepositoryCleanupInProgress.java | 24 ++- .../cluster/RestoreInProgress.java | 74 +++---- .../cluster/SnapshotDeletionsInProgress.java | 42 ++-- .../cluster/SnapshotsInProgress.java | 15 +- .../health/metadata/HealthMetadata.java | 20 +- .../RepositoryCleanupInProgressTests.java | 38 ++++ .../cluster/RestoreInProgressTests.java | 55 +++++ .../SnapshotDeletionsInProgressTests.java | 32 ++- .../coordination/CoordinatorTests.java | 14 +- .../JoinValidationServiceTests.java | 7 +- .../ClusterSerializationTests.java | 30 +-- .../HealthMetadataSerializationTests.java | 22 ++ ...SnapshotsInProgressSerializationTests.java | 203 +++++++++--------- .../core/security/authc/TokenMetadata.java | 8 +- 17 files changed, 382 insertions(+), 222 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/cluster/RepositoryCleanupInProgressTests.java create mode 100644 server/src/test/java/org/elasticsearch/cluster/RestoreInProgressTests.java diff --git a/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/ClusterStateRestCancellationIT.java b/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/ClusterStateRestCancellationIT.java index a0a4642d25ee..d13b7eda30d2 100644 --- a/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/ClusterStateRestCancellationIT.java +++ b/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/ClusterStateRestCancellationIT.java @@ -24,10 +24,11 @@ import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.tasks.TaskInfo; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.concurrent.CancellationException; import java.util.function.UnaryOperator; @@ -116,7 +117,7 @@ public void writeTo(StreamOutput out) { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) { + public Iterator toXContentChunked(ToXContent.Params params) { throw new AssertionError("task should have been cancelled before serializing this custom"); } } diff --git a/server/src/internalClusterTest/java/org/elasticsearch/cluster/SimpleClusterStateIT.java b/server/src/internalClusterTest/java/org/elasticsearch/cluster/SimpleClusterStateIT.java index 0be8f79a3e1c..e596db1e8809 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/cluster/SimpleClusterStateIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/cluster/SimpleClusterStateIT.java @@ -42,6 +42,7 @@ import org.elasticsearch.tracing.Tracer; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xcontent.NamedXContentRegistry; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; import org.junit.Before; @@ -50,6 +51,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; @@ -401,8 +403,8 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - return builder; + public Iterator toXContentChunked(ToXContent.Params params) { + return Collections.emptyIterator(); } static NamedDiff readDiffFrom(StreamInput in) throws IOException { diff --git a/server/src/main/java/org/elasticsearch/cluster/ClusterState.java b/server/src/main/java/org/elasticsearch/cluster/ClusterState.java index 3ece7e345ffd..0fc23ae86a52 100644 --- a/server/src/main/java/org/elasticsearch/cluster/ClusterState.java +++ b/server/src/main/java/org/elasticsearch/cluster/ClusterState.java @@ -43,7 +43,9 @@ import org.elasticsearch.common.io.stream.VersionedNamedWriteable; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.core.Nullable; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.ToXContentFragment; import org.elasticsearch.xcontent.XContent; import org.elasticsearch.xcontent.XContentBuilder; @@ -51,6 +53,7 @@ import java.io.IOException; import java.util.EnumSet; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -105,7 +108,7 @@ public class ClusterState implements ToXContentFragment, Diffable public static final ClusterState EMPTY_STATE = builder(ClusterName.CLUSTER_NAME_SETTING.getDefault(Settings.EMPTY)).build(); - public interface Custom extends NamedDiffable, ToXContentFragment { + public interface Custom extends NamedDiffable, ChunkedToXContent { /** * Returns true iff this {@link Custom} is private to the cluster and should never be send to a client. @@ -121,7 +124,7 @@ default boolean isPrivate() { * the more faithful it is the more useful it is for diagnostics. */ @Override - XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException; + Iterator toXContentChunked(Params params); } private static final NamedDiffableValueSerializer CUSTOM_VALUE_SERIALIZER = new NamedDiffableValueSerializer<>(Custom.class); @@ -619,7 +622,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws if (metrics.contains(Metric.CUSTOMS)) { for (Map.Entry cursor : customs.entrySet()) { builder.startObject(cursor.getKey()); - cursor.getValue().toXContent(builder, params); + ChunkedToXContent.wrapAsXContentObject(cursor.getValue()).toXContent(builder, params); builder.endObject(); } } diff --git a/server/src/main/java/org/elasticsearch/cluster/RepositoryCleanupInProgress.java b/server/src/main/java/org/elasticsearch/cluster/RepositoryCleanupInProgress.java index 948e055e06ee..47db3993f95f 100644 --- a/server/src/main/java/org/elasticsearch/cluster/RepositoryCleanupInProgress.java +++ b/server/src/main/java/org/elasticsearch/cluster/RepositoryCleanupInProgress.java @@ -9,13 +9,15 @@ import org.elasticsearch.Version; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.repositories.RepositoryOperation; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import java.io.IOException; +import java.util.Iterator; import java.util.List; public final class RepositoryCleanupInProgress extends AbstractNamedDiffable implements ClusterState.Custom { @@ -62,17 +64,17 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startArray(TYPE); - for (Entry entry : entries) { - builder.startObject(); - { + public Iterator toXContentChunked(ToXContent.Params ignored) { + return Iterators.concat( + Iterators.single((builder, params) -> builder.startArray(TYPE)), + entries.stream().map(entry -> (builder, params) -> { + builder.startObject(); builder.field("repository", entry.repository); - } - builder.endObject(); - } - builder.endArray(); - return builder; + builder.endObject(); + return builder; + }).iterator(), + Iterators.single((builder, params) -> builder.endArray()) + ); } @Override diff --git a/server/src/main/java/org/elasticsearch/cluster/RestoreInProgress.java b/server/src/main/java/org/elasticsearch/cluster/RestoreInProgress.java index 3b19fee0210b..ab7a4a9589b9 100644 --- a/server/src/main/java/org/elasticsearch/cluster/RestoreInProgress.java +++ b/server/src/main/java/org/elasticsearch/cluster/RestoreInProgress.java @@ -11,6 +11,7 @@ import org.elasticsearch.Version; import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; import org.elasticsearch.cluster.ClusterState.Custom; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; @@ -18,7 +19,6 @@ import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.snapshots.Snapshot; import org.elasticsearch.xcontent.ToXContent; -import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; import java.util.Collections; @@ -398,49 +398,41 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { - builder.startArray("snapshots"); - for (Entry entry : entries.values()) { - toXContent(entry, builder); - } - builder.endArray(); - return builder; - } - - /** - * Serializes single restore operation - * - * @param entry restore operation metadata - * @param builder XContent builder - */ - public static void toXContent(Entry entry, XContentBuilder builder) throws IOException { - builder.startObject(); - builder.field("snapshot", entry.snapshot().getSnapshotId().getName()); - builder.field("repository", entry.snapshot().getRepository()); - builder.field("state", entry.state()); - builder.startArray("indices"); - { - for (String index : entry.indices()) { - builder.value(index); - } - } - builder.endArray(); - builder.startArray("shards"); - { - for (Map.Entry shardEntry : entry.shards.entrySet()) { - ShardId shardId = shardEntry.getKey(); - ShardRestoreStatus status = shardEntry.getValue(); + public Iterator toXContentChunked(ToXContent.Params ignored) { + return Iterators.concat( + Iterators.single((builder, params) -> builder.startArray("snapshots")), + entries.values().stream().map(entry -> (builder, params) -> { builder.startObject(); + builder.field("snapshot", entry.snapshot().getSnapshotId().getName()); + builder.field("repository", entry.snapshot().getRepository()); + builder.field("state", entry.state()); + builder.startArray("indices"); { - builder.field("index", shardId.getIndex()); - builder.field("shard", shardId.getId()); - builder.field("state", status.state()); + for (String index : entry.indices()) { + builder.value(index); + } + } + builder.endArray(); + builder.startArray("shards"); + { + for (Map.Entry shardEntry : entry.shards.entrySet()) { + ShardId shardId = shardEntry.getKey(); + ShardRestoreStatus status = shardEntry.getValue(); + builder.startObject(); + { + builder.field("index", shardId.getIndex()); + builder.field("shard", shardId.getId()); + builder.field("state", status.state()); + } + builder.endObject(); + } } - builder.endObject(); - } - } - builder.endArray(); - builder.endObject(); + builder.endArray(); + builder.endObject(); + return builder; + }).iterator(), + Iterators.single((builder, params) -> builder.endArray()) + ); } } diff --git a/server/src/main/java/org/elasticsearch/cluster/SnapshotDeletionsInProgress.java b/server/src/main/java/org/elasticsearch/cluster/SnapshotDeletionsInProgress.java index ba9531fbe297..26fb5d20eace 100644 --- a/server/src/main/java/org/elasticsearch/cluster/SnapshotDeletionsInProgress.java +++ b/server/src/main/java/org/elasticsearch/cluster/SnapshotDeletionsInProgress.java @@ -11,19 +11,21 @@ import org.elasticsearch.Version; import org.elasticsearch.cluster.ClusterState.Custom; import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.repositories.RepositoryOperation; import org.elasticsearch.snapshots.SnapshotId; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Set; @@ -160,25 +162,27 @@ public Version getMinimalSupportedVersion() { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startArray(TYPE); - for (Entry entry : entries) { - builder.startObject(); - { - builder.field("repository", entry.repository()); - builder.startArray("snapshots"); - for (SnapshotId snapshot : entry.snapshots) { - builder.value(snapshot.getName()); + public Iterator toXContentChunked(ToXContent.Params ignored) { + return Iterators.concat( + Iterators.single((builder, params) -> builder.startArray(TYPE)), + entries.stream().map(entry -> (builder, params) -> { + builder.startObject(); + { + builder.field("repository", entry.repository()); + builder.startArray("snapshots"); + for (SnapshotId snapshot : entry.snapshots) { + builder.value(snapshot.getName()); + } + builder.endArray(); + builder.timeField("start_time_millis", "start_time", entry.startTime); + builder.field("repository_state_id", entry.repositoryStateId); + builder.field("state", entry.state); } - builder.endArray(); - builder.timeField("start_time_millis", "start_time", entry.startTime); - builder.field("repository_state_id", entry.repositoryStateId); - builder.field("state", entry.state); - } - builder.endObject(); - } - builder.endArray(); - return builder; + builder.endObject(); + return builder; + }).iterator(), + Iterators.single((builder, params) -> builder.endArray()) + ); } @Override diff --git a/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java b/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java index 279e7ba4a6b9..1256032b050b 100644 --- a/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java +++ b/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java @@ -12,6 +12,7 @@ import org.elasticsearch.cluster.ClusterState.Custom; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; @@ -212,14 +213,12 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { - builder.startArray("snapshots"); - final Iterator iterator = asStream().iterator(); - while (iterator.hasNext()) { - iterator.next().toXContent(builder, params); - } - builder.endArray(); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return Iterators.concat( + Iterators.single((builder, params) -> builder.startArray("snapshots")), + asStream().iterator(), + Iterators.single((builder, params) -> builder.endArray()) + ); } @Override diff --git a/server/src/main/java/org/elasticsearch/health/metadata/HealthMetadata.java b/server/src/main/java/org/elasticsearch/health/metadata/HealthMetadata.java index 4c64e6f404a5..9c6979291424 100644 --- a/server/src/main/java/org/elasticsearch/health/metadata/HealthMetadata.java +++ b/server/src/main/java/org/elasticsearch/health/metadata/HealthMetadata.java @@ -12,16 +12,19 @@ import org.elasticsearch.cluster.AbstractNamedDiffable; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.NamedDiff; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.RelativeByteSizeValue; import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.ToXContentFragment; import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; +import java.util.Iterator; import java.util.Objects; /** @@ -63,22 +66,19 @@ public static NamedDiff readDiffFrom(StreamInput in) throws } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(DISK_METADATA.getPreferredName()); - diskMetadata.toXContent(builder, params); - builder.endObject(); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return Iterators.single((builder, params) -> { + builder.startObject(DISK_METADATA.getPreferredName()); + diskMetadata.toXContent(builder, params); + builder.endObject(); + return builder; + }); } public static HealthMetadata getFromClusterState(ClusterState clusterState) { return clusterState.custom(HealthMetadata.TYPE); } - @Override - public boolean isFragment() { - return true; - } - public Disk getDiskMetadata() { return diskMetadata; } diff --git a/server/src/test/java/org/elasticsearch/cluster/RepositoryCleanupInProgressTests.java b/server/src/test/java/org/elasticsearch/cluster/RepositoryCleanupInProgressTests.java new file mode 100644 index 000000000000..d001a7c5a262 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/cluster/RepositoryCleanupInProgressTests.java @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.cluster; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.ToXContent; + +import java.io.IOException; + +import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; +import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; + +public class RepositoryCleanupInProgressTests extends ESTestCase { + public void testChunking() throws IOException { + final var instance = new RepositoryCleanupInProgress( + randomList(10, () -> new RepositoryCleanupInProgress.Entry(randomAlphaOfLength(10), randomNonNegativeLong())) + ); + + int chunkCount = 0; + try (var builder = jsonBuilder()) { + builder.startObject(); + final var iterator = instance.toXContentChunked(EMPTY_PARAMS); + while (iterator.hasNext()) { + iterator.next().toXContent(builder, ToXContent.EMPTY_PARAMS); + chunkCount += 1; + } + builder.endObject(); + } // closing the builder verifies that the XContent is well-formed + + assertEquals(instance.entries().size() + 2, chunkCount); + } +} diff --git a/server/src/test/java/org/elasticsearch/cluster/RestoreInProgressTests.java b/server/src/test/java/org/elasticsearch/cluster/RestoreInProgressTests.java new file mode 100644 index 000000000000..a9383fe08af1 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/cluster/RestoreInProgressTests.java @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.cluster; + +import org.elasticsearch.snapshots.Snapshot; +import org.elasticsearch.snapshots.SnapshotId; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.ToXContent; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; +import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; + +public class RestoreInProgressTests extends ESTestCase { + public void testChunking() throws IOException { + final var ripBuilder = new RestoreInProgress.Builder(); + final var entryCount = between(0, 5); + for (int i = 0; i < entryCount; i++) { + ripBuilder.add( + new RestoreInProgress.Entry( + "uuid-" + i, + new Snapshot(randomAlphaOfLength(10), new SnapshotId(randomAlphaOfLength(10), randomAlphaOfLength(10))), + randomFrom(RestoreInProgress.State.values()), + randomBoolean(), + List.of(), + Map.of() + ) + ); + } + + final var instance = ripBuilder.build(); + + int chunkCount = 0; + try (var builder = jsonBuilder()) { + builder.startObject(); + final var iterator = instance.toXContentChunked(EMPTY_PARAMS); + while (iterator.hasNext()) { + iterator.next().toXContent(builder, ToXContent.EMPTY_PARAMS); + chunkCount += 1; + } + builder.endObject(); + } // closing the builder verifies that the XContent is well-formed + + assertEquals(entryCount + 2, chunkCount); + } +} diff --git a/server/src/test/java/org/elasticsearch/cluster/SnapshotDeletionsInProgressTests.java b/server/src/test/java/org/elasticsearch/cluster/SnapshotDeletionsInProgressTests.java index b9946a2bd603..8c18ee15afa7 100644 --- a/server/src/test/java/org/elasticsearch/cluster/SnapshotDeletionsInProgressTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/SnapshotDeletionsInProgressTests.java @@ -9,6 +9,7 @@ package org.elasticsearch.cluster; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xcontent.ToXContent; @@ -18,6 +19,7 @@ import java.util.Collections; import java.util.List; +import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.equalTo; @@ -38,7 +40,7 @@ public void testXContent() throws IOException { try (XContentBuilder builder = jsonBuilder()) { builder.humanReadable(true); builder.startObject(); - sdip.toXContent(builder, ToXContent.EMPTY_PARAMS); + ChunkedToXContent.wrapAsXContentObject(sdip).toXContent(builder, ToXContent.EMPTY_PARAMS); builder.endObject(); String json = Strings.toString(builder); assertThat(json, equalTo(XContentHelper.stripWhitespace(""" @@ -56,4 +58,32 @@ public void testXContent() throws IOException { }"""))); } } + + public void testChunking() throws IOException { + final var instance = SnapshotDeletionsInProgress.of( + randomList( + 10, + () -> new SnapshotDeletionsInProgress.Entry( + Collections.emptyList(), + randomAlphaOfLength(10), + randomNonNegativeLong(), + randomNonNegativeLong(), + randomFrom(SnapshotDeletionsInProgress.State.values()) + ) + ) + ); + + int chunkCount = 0; + try (var builder = jsonBuilder()) { + builder.startObject(); + final var iterator = instance.toXContentChunked(EMPTY_PARAMS); + while (iterator.hasNext()) { + iterator.next().toXContent(builder, ToXContent.EMPTY_PARAMS); + chunkCount += 1; + } + builder.endObject(); + } // closing the builder verifies that the XContent is well-formed + + assertEquals(instance.getEntries().size() + 2, chunkCount); + } } diff --git a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java index 5bf0b302991b..e24be3b79d5a 100644 --- a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java @@ -44,11 +44,13 @@ import org.elasticsearch.test.MockLogAppender; import org.elasticsearch.test.junit.annotations.TestLogging; import org.elasticsearch.transport.TransportService; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; @@ -1179,10 +1181,8 @@ class DelayedCustom extends AbstractNamedDiffable implement } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.endObject(); - return builder; + public Iterator toXContentChunked(ToXContent.Params params) { + return Collections.emptyIterator(); } @Override @@ -1979,8 +1979,8 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - return builder; + public Iterator toXContentChunked(ToXContent.Params params) { + return Collections.emptyIterator(); } } diff --git a/server/src/test/java/org/elasticsearch/cluster/coordination/JoinValidationServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/coordination/JoinValidationServiceTests.java index 0b0ee295cea9..be076eb668ca 100644 --- a/server/src/test/java/org/elasticsearch/cluster/coordination/JoinValidationServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/coordination/JoinValidationServiceTests.java @@ -41,10 +41,11 @@ import org.elasticsearch.transport.TransportRequestOptions; import org.elasticsearch.transport.TransportResponse; import org.elasticsearch.transport.TransportService; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Random; import java.util.Set; @@ -232,8 +233,8 @@ public void testJoinValidationRejectsUnreadableClusterState() { class BadCustom implements SimpleDiffable, ClusterState.Custom { @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) { - return builder; + public Iterator toXContentChunked(ToXContent.Params params) { + return Collections.emptyIterator(); } @Override diff --git a/server/src/test/java/org/elasticsearch/cluster/serialization/ClusterSerializationTests.java b/server/src/test/java/org/elasticsearch/cluster/serialization/ClusterSerializationTests.java index 2b676bc4d3e3..4b88627cc026 100644 --- a/server/src/test/java/org/elasticsearch/cluster/serialization/ClusterSerializationTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/serialization/ClusterSerializationTests.java @@ -27,6 +27,7 @@ import org.elasticsearch.cluster.routing.RoutingTable; import org.elasticsearch.cluster.routing.allocation.AllocationService; import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; @@ -36,11 +37,12 @@ import org.elasticsearch.snapshots.Snapshot; import org.elasticsearch.snapshots.SnapshotId; import org.elasticsearch.test.VersionUtils; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import java.io.IOException; import java.util.Arrays; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Map; @@ -285,13 +287,12 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - { - builder.field("custom_string_object", strObject); - } - builder.endObject(); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return Iterators.concat( + Iterators.single((builder, params) -> builder.startObject()), + Iterators.single((builder, params) -> builder.field("custom_string_object", strObject)), + Iterators.single((builder, params) -> builder.endObject()) + ); } @Override @@ -329,13 +330,12 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - { - builder.field("custom_integer_object", intObject); - } - builder.endObject(); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return Iterators.concat( + Iterators.single((builder, params) -> builder.startObject()), + Iterators.single((builder, params) -> builder.field("custom_integer_object", intObject)), + Iterators.single((builder, params) -> builder.endObject()) + ); } @Override diff --git a/server/src/test/java/org/elasticsearch/health/metadata/HealthMetadataSerializationTests.java b/server/src/test/java/org/elasticsearch/health/metadata/HealthMetadataSerializationTests.java index 3661726f3d72..433a4a784efc 100644 --- a/server/src/test/java/org/elasticsearch/health/metadata/HealthMetadataSerializationTests.java +++ b/server/src/test/java/org/elasticsearch/health/metadata/HealthMetadataSerializationTests.java @@ -16,9 +16,14 @@ import org.elasticsearch.common.unit.RatioValue; import org.elasticsearch.common.unit.RelativeByteSizeValue; import org.elasticsearch.test.SimpleDiffableWireSerializationTestCase; +import org.elasticsearch.xcontent.ToXContent; +import java.io.IOException; import java.util.List; +import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; +import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; + public class HealthMetadataSerializationTests extends SimpleDiffableWireSerializationTestCase { @Override @@ -102,4 +107,21 @@ static HealthMetadata.Disk mutateDiskMetadata(HealthMetadata.Disk base) { private HealthMetadata mutate(HealthMetadata base) { return new HealthMetadata(mutateDiskMetadata(base.getDiskMetadata())); } + + public void testToXContentChunking() throws IOException { + final var instance = createTestInstance(); + + int chunkCount = 0; + try (var builder = jsonBuilder()) { + builder.startObject(); + final var iterator = instance.toXContentChunked(EMPTY_PARAMS); + while (iterator.hasNext()) { + iterator.next().toXContent(builder, ToXContent.EMPTY_PARAMS); + chunkCount += 1; + } + builder.endObject(); + } // closing the builder verifies that the XContent is well-formed + + assertEquals(1, chunkCount); + } } diff --git a/server/src/test/java/org/elasticsearch/snapshots/SnapshotsInProgressSerializationTests.java b/server/src/test/java/org/elasticsearch/snapshots/SnapshotsInProgressSerializationTests.java index afc5005f8fd1..2842c593c6a7 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/SnapshotsInProgressSerializationTests.java +++ b/server/src/test/java/org/elasticsearch/snapshots/SnapshotsInProgressSerializationTests.java @@ -42,6 +42,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.equalTo; @@ -418,115 +419,123 @@ public void testXContent() throws IOException { ) ); + String json; + long chunkCount = 0; try (XContentBuilder builder = jsonBuilder()) { builder.humanReadable(true); builder.startObject(); - sip.toXContent(builder, ToXContent.EMPTY_PARAMS); + final var iterator = sip.toXContentChunked(EMPTY_PARAMS); + while (iterator.hasNext()) { + iterator.next().toXContent(builder, ToXContent.EMPTY_PARAMS); + chunkCount += 1; + } builder.endObject(); - String json = Strings.toString(builder); - assertThat( - json, - anyOf( - equalTo(XContentHelper.stripWhitespace(""" + json = Strings.toString(builder); + } + + assertEquals(2 + sip.asStream().count(), chunkCount); + assertThat( + json, + anyOf( + equalTo(XContentHelper.stripWhitespace(""" + { + "snapshots": [ { - "snapshots": [ + "repository": "repo", + "snapshot": "name", + "uuid": "uuid", + "include_global_state": true, + "partial": true, + "state": "SUCCESS", + "indices": [ { "name": "index", "id": "uuid" } ], + "start_time": "1970-01-01T00:20:34.567Z", + "start_time_millis": 1234567, + "repository_state_id": 0, + "shards": [ { - "repository": "repo", - "snapshot": "name", - "uuid": "uuid", - "include_global_state": true, - "partial": true, + "index": { + "index_name": "index", + "index_uuid": "uuid" + }, + "shard": 0, "state": "SUCCESS", - "indices": [ { "name": "index", "id": "uuid" } ], - "start_time": "1970-01-01T00:20:34.567Z", - "start_time_millis": 1234567, - "repository_state_id": 0, - "shards": [ - { - "index": { - "index_name": "index", - "index_uuid": "uuid" - }, - "shard": 0, - "state": "SUCCESS", - "generation": "shardgen", - "node": "nodeId", - "result": { - "generation": "shardgen", - "size": "1b", - "size_in_bytes": 1, - "segments": 1 - } - }, - { - "index": { - "index_name": "index", - "index_uuid": "uuid" - }, - "shard": 1, - "state": "FAILED", - "generation": "fail-gen", - "node": "nodeId", - "reason": "failure-reason" - } - ], - "feature_states": [], - "data_streams": [] + "generation": "shardgen", + "node": "nodeId", + "result": { + "generation": "shardgen", + "size": "1b", + "size_in_bytes": 1, + "segments": 1 + } + }, + { + "index": { + "index_name": "index", + "index_uuid": "uuid" + }, + "shard": 1, + "state": "FAILED", + "generation": "fail-gen", + "node": "nodeId", + "reason": "failure-reason" } - ] - }""")), - // or the shards might be in the other order: - equalTo(XContentHelper.stripWhitespace(""" + ], + "feature_states": [], + "data_streams": [] + } + ] + }""")), + // or the shards might be in the other order: + equalTo(XContentHelper.stripWhitespace(""" + { + "snapshots": [ { - "snapshots": [ + "repository": "repo", + "snapshot": "name", + "uuid": "uuid", + "include_global_state": true, + "partial": true, + "state": "SUCCESS", + "indices": [ { "name": "index", "id": "uuid" } ], + "start_time": "1970-01-01T00:20:34.567Z", + "start_time_millis": 1234567, + "repository_state_id": 0, + "shards": [ + { + "index": { + "index_name": "index", + "index_uuid": "uuid" + }, + "shard": 1, + "state": "FAILED", + "generation": "fail-gen", + "node": "nodeId", + "reason": "failure-reason" + }, { - "repository": "repo", - "snapshot": "name", - "uuid": "uuid", - "include_global_state": true, - "partial": true, + "index": { + "index_name": "index", + "index_uuid": "uuid" + }, + "shard": 0, "state": "SUCCESS", - "indices": [ { "name": "index", "id": "uuid" } ], - "start_time": "1970-01-01T00:20:34.567Z", - "start_time_millis": 1234567, - "repository_state_id": 0, - "shards": [ - { - "index": { - "index_name": "index", - "index_uuid": "uuid" - }, - "shard": 1, - "state": "FAILED", - "generation": "fail-gen", - "node": "nodeId", - "reason": "failure-reason" - }, - { - "index": { - "index_name": "index", - "index_uuid": "uuid" - }, - "shard": 0, - "state": "SUCCESS", - "generation": "shardgen", - "node": "nodeId", - "result": { - "generation": "shardgen", - "size": "1b", - "size_in_bytes": 1, - "segments": 1 - } - } - ], - "feature_states": [], - "data_streams": [] + "generation": "shardgen", + "node": "nodeId", + "result": { + "generation": "shardgen", + "size": "1b", + "size_in_bytes": 1, + "segments": 1 + } } - ] - }""")) - ) - ); - } + ], + "feature_states": [], + "data_streams": [] + } + ] + }""")) + ) + ); } public static State randomState(Map shards) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/TokenMetadata.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/TokenMetadata.java index 58366184a491..9d5017209edc 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/TokenMetadata.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/TokenMetadata.java @@ -12,10 +12,12 @@ import org.elasticsearch.cluster.NamedDiff; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; import java.util.List; public final class TokenMetadata extends AbstractNamedDiffable implements ClusterState.Custom { @@ -63,9 +65,9 @@ public String getWriteableName() { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + public Iterator toXContentChunked(ToXContent.Params params) { // never render this to the user - return builder; + return Collections.emptyIterator(); } @Override From 26bc894334b7d39557692d6d85f06f0a101d29c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Wed, 30 Nov 2022 13:37:20 +0100 Subject: [PATCH 115/919] [DOCS] Updates ML decider docs by mentioning CPU as scaling criterion (#92018) Co-authored-by: Abdon Pijpelink --- .../machine-learning-decider.asciidoc | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/reference/autoscaling/deciders/machine-learning-decider.asciidoc b/docs/reference/autoscaling/deciders/machine-learning-decider.asciidoc index 1a2b230cf520..26ced6ad7bb2 100644 --- a/docs/reference/autoscaling/deciders/machine-learning-decider.asciidoc +++ b/docs/reference/autoscaling/deciders/machine-learning-decider.asciidoc @@ -2,25 +2,26 @@ [[autoscaling-machine-learning-decider]] === Machine learning decider -The {ml} decider (`ml`) calculates the memory required to run {ml} jobs. +The {ml} decider (`ml`) calculates the memory and CPU requirements to run {ml} +jobs and trained models. The {ml} decider is enabled for policies governing `ml` nodes. -NOTE: For {ml} jobs to open when the cluster is not appropriately -scaled, set `xpack.ml.max_lazy_ml_nodes` to the largest number of possible {ml} -jobs (refer to <> for more information). In {ess}, this is +NOTE: For {ml} jobs to open when the cluster is not appropriately scaled, set +`xpack.ml.max_lazy_ml_nodes` to the largest number of possible {ml} nodes (refer +to <> for more information). In {ess}, this is automatically set. [[autoscaling-machine-learning-decider-settings]] ==== Configuration settings Both `num_anomaly_jobs_in_queue` and `num_analytics_jobs_in_queue` are designed -to delay a scale-up event. If the cluster is too small, these settings indicate how many jobs of each type can be -unassigned from a node. Both settings are -only considered for jobs that can be opened given the current scale. If a job is -too large for any node size or if a job can't be assigned without user -intervention (for example, a user calling `_stop` against a real-time -{anomaly-job}), the numbers are ignored for that particular job. +to delay a scale-up event. If the cluster is too small, these settings indicate +how many jobs of each type can be unassigned from a node. Both settings are only +considered for jobs that can be opened given the current scale. If a job is too +large for any node size or if a job can't be assigned without user intervention +(for example, a user calling `_stop` against a real-time {anomaly-job}), the +numbers are ignored for that particular job. `num_anomaly_jobs_in_queue`:: (Optional, integer) From c8953310cddf6b5a9be33b1a5273a5f6202268fb Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Wed, 30 Nov 2022 15:19:58 +0100 Subject: [PATCH 116/919] Simplify shardsWithState (#91991) --- .../cluster/routing/RoutingNode.java | 95 +++++-------------- .../cluster/routing/RoutingNodeTests.java | 1 - .../allocation/FailedShardsRoutingTests.java | 25 +++-- .../TenShardsOneReplicaRoutingTests.java | 10 +- .../cluster/routing/RoutingNodesHelper.java | 22 ++--- .../watcher/WatcherIndexingListenerTests.java | 2 +- 6 files changed, 59 insertions(+), 96 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/RoutingNode.java b/server/src/main/java/org/elasticsearch/cluster/routing/RoutingNode.java index af6b27fef98f..399618de90e2 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/RoutingNode.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/RoutingNode.java @@ -25,6 +25,9 @@ import java.util.Objects; import java.util.Set; import java.util.function.Predicate; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toCollection; /** * A {@link RoutingNode} represents a cluster node associated with a single {@link DiscoveryNode} including all shards @@ -211,29 +214,11 @@ void remove(ShardRouting shard) { /** * Determine the number of shards with a specific state - * @param states set of states which should be counted + * @param state which should be counted * @return number of shards */ - public int numberOfShardsWithState(ShardRoutingState... states) { - if (states.length == 1) { - if (states[0] == ShardRoutingState.INITIALIZING) { - return initializingShards.size(); - } else if (states[0] == ShardRoutingState.RELOCATING) { - return relocatingShards.size(); - } else if (states[0] == ShardRoutingState.STARTED) { - return startedShards.size(); - } - } - - int count = 0; - for (ShardRouting shardEntry : this) { - for (ShardRoutingState state : states) { - if (shardEntry.state() == state) { - count++; - } - } - } - return count; + public int numberOfShardsWithState(ShardRoutingState state) { + return internalGetShardsWithState(state).size(); } /** @@ -242,20 +227,7 @@ public int numberOfShardsWithState(ShardRoutingState... states) { * @return List of shards */ public List shardsWithState(ShardRoutingState state) { - if (state == ShardRoutingState.INITIALIZING) { - return new ArrayList<>(initializingShards); - } else if (state == ShardRoutingState.RELOCATING) { - return new ArrayList<>(relocatingShards); - } else if (state == ShardRoutingState.STARTED) { - return new ArrayList<>(startedShards); - } - List shards = new ArrayList<>(); - for (ShardRouting shardEntry : this) { - if (shardEntry.state() == state) { - shards.add(shardEntry); - } - } - return shards; + return new ArrayList<>(internalGetShardsWithState(state)); } private static final ShardRouting[] EMPTY_SHARD_ROUTING_ARRAY = new ShardRouting[0]; @@ -279,49 +251,28 @@ public ShardRouting[] started() { * @return a list of shards */ public List shardsWithState(String index, ShardRoutingState... states) { - List shards = new ArrayList<>(); - - if (states.length == 1) { - if (states[0] == ShardRoutingState.INITIALIZING) { - for (ShardRouting shardEntry : initializingShards) { - if (shardEntry.getIndexName().equals(index) == false) { - continue; - } - shards.add(shardEntry); - } - return shards; - } else if (states[0] == ShardRoutingState.RELOCATING) { - for (ShardRouting shardEntry : relocatingShards) { - if (shardEntry.getIndexName().equals(index) == false) { - continue; - } - shards.add(shardEntry); - } - return shards; - } else if (states[0] == ShardRoutingState.STARTED) { - for (ShardRouting shardEntry : startedShards) { - if (shardEntry.getIndexName().equals(index) == false) { - continue; - } - shards.add(shardEntry); - } - return shards; - } - } + return Stream.of(states).flatMap(state -> shardsWithState(index, state).stream()).collect(toCollection(ArrayList::new)); + } - for (ShardRouting shardEntry : this) { - if (shardEntry.getIndexName().equals(index) == false) { - continue; - } - for (ShardRoutingState state : states) { - if (shardEntry.state() == state) { - shards.add(shardEntry); - } + public List shardsWithState(String index, ShardRoutingState state) { + var shards = new ArrayList(); + for (ShardRouting shardEntry : internalGetShardsWithState(state)) { + if (shardEntry.getIndexName().equals(index)) { + shards.add(shardEntry); } } return shards; } + private LinkedHashSet internalGetShardsWithState(ShardRoutingState state) { + return switch (state) { + case UNASSIGNED -> throw new IllegalArgumentException("Unassigned shards are not linked to a routing node"); + case INITIALIZING -> initializingShards; + case STARTED -> startedShards; + case RELOCATING -> relocatingShards; + }; + } + /** * The number of shards on this node that will not be eventually relocated. */ diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/RoutingNodeTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/RoutingNodeTests.java index f78c0ff3fe6b..c40f95f384b2 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/RoutingNodeTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/RoutingNodeTests.java @@ -96,7 +96,6 @@ public void testRemove() { } public void testNumberOfShardsWithState() { - assertThat(routingNode.numberOfShardsWithState(ShardRoutingState.INITIALIZING, ShardRoutingState.STARTED), equalTo(2)); assertThat(routingNode.numberOfShardsWithState(ShardRoutingState.STARTED), equalTo(1)); assertThat(routingNode.numberOfShardsWithState(ShardRoutingState.RELOCATING), equalTo(1)); assertThat(routingNode.numberOfShardsWithState(ShardRoutingState.INITIALIZING), equalTo(1)); diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/FailedShardsRoutingTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/FailedShardsRoutingTests.java index 86ed2badab0e..5db2ed8de77f 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/FailedShardsRoutingTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/FailedShardsRoutingTests.java @@ -41,7 +41,6 @@ import static org.elasticsearch.cluster.routing.ShardRoutingState.UNASSIGNED; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; @@ -507,10 +506,14 @@ public void testRebalanceFailure() { RoutingNodes routingNodes = clusterState.getRoutingNodes(); assertThat(clusterState.routingTable().index("test").size(), equalTo(2)); - assertThat(routingNodes.node("node1").numberOfShardsWithState(STARTED, RELOCATING), equalTo(2)); - assertThat(routingNodes.node("node1").numberOfShardsWithState(STARTED), lessThan(3)); - assertThat(routingNodes.node("node2").numberOfShardsWithState(STARTED, RELOCATING), equalTo(2)); - assertThat(routingNodes.node("node2").numberOfShardsWithState(STARTED), lessThan(3)); + assertThat( + routingNodes.node("node1").numberOfShardsWithState(STARTED) + routingNodes.node("node1").numberOfShardsWithState(RELOCATING), + equalTo(2) + ); + assertThat( + routingNodes.node("node2").numberOfShardsWithState(STARTED) + routingNodes.node("node2").numberOfShardsWithState(RELOCATING), + equalTo(2) + ); assertThat(routingNodes.node("node3").numberOfShardsWithState(INITIALIZING), equalTo(1)); logger.info("Fail the shards on node 3"); @@ -521,10 +524,14 @@ public void testRebalanceFailure() { routingNodes = clusterState.getRoutingNodes(); assertThat(clusterState.routingTable().index("test").size(), equalTo(2)); - assertThat(routingNodes.node("node1").numberOfShardsWithState(STARTED, RELOCATING), equalTo(2)); - assertThat(routingNodes.node("node1").numberOfShardsWithState(STARTED), lessThan(3)); - assertThat(routingNodes.node("node2").numberOfShardsWithState(STARTED, RELOCATING), equalTo(2)); - assertThat(routingNodes.node("node2").numberOfShardsWithState(STARTED), lessThan(3)); + assertThat( + routingNodes.node("node1").numberOfShardsWithState(STARTED) + routingNodes.node("node1").numberOfShardsWithState(RELOCATING), + equalTo(2) + ); + assertThat( + routingNodes.node("node2").numberOfShardsWithState(STARTED) + routingNodes.node("node2").numberOfShardsWithState(RELOCATING), + equalTo(2) + ); if (strategy.isBalancedShardsAllocator()) { assertThat(routingNodes.node("node3").numberOfShardsWithState(INITIALIZING), equalTo(1)); diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/TenShardsOneReplicaRoutingTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/TenShardsOneReplicaRoutingTests.java index f03683dd3675..e1134699db62 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/TenShardsOneReplicaRoutingTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/TenShardsOneReplicaRoutingTests.java @@ -143,10 +143,16 @@ public void testSingleIndexFirstStartPrimaryThenBackups() { routingNodes = clusterState.getRoutingNodes(); assertThat(clusterState.routingTable().index("test").size(), equalTo(10)); - assertThat(routingNodes.node("node1").numberOfShardsWithState(STARTED, RELOCATING), equalTo(10)); assertThat(routingNodes.node("node1").numberOfShardsWithState(STARTED), lessThan(10)); - assertThat(routingNodes.node("node2").numberOfShardsWithState(STARTED, RELOCATING), equalTo(10)); + assertThat( + routingNodes.node("node1").numberOfShardsWithState(STARTED) + routingNodes.node("node1").numberOfShardsWithState(RELOCATING), + equalTo(10) + ); assertThat(routingNodes.node("node2").numberOfShardsWithState(STARTED), lessThan(10)); + assertThat( + routingNodes.node("node2").numberOfShardsWithState(STARTED) + routingNodes.node("node2").numberOfShardsWithState(RELOCATING), + equalTo(10) + ); assertThat(routingNodes.node("node3").numberOfShardsWithState(INITIALIZING), equalTo(6)); logger.info("Start the shards on node 3"); diff --git a/test/framework/src/main/java/org/elasticsearch/cluster/routing/RoutingNodesHelper.java b/test/framework/src/main/java/org/elasticsearch/cluster/routing/RoutingNodesHelper.java index 4387f36effa0..95420293e80c 100644 --- a/test/framework/src/main/java/org/elasticsearch/cluster/routing/RoutingNodesHelper.java +++ b/test/framework/src/main/java/org/elasticsearch/cluster/routing/RoutingNodesHelper.java @@ -21,28 +21,29 @@ private RoutingNodesHelper() {} public static List shardsWithState(RoutingNodes routingNodes, ShardRoutingState state) { List shards = new ArrayList<>(); - for (RoutingNode routingNode : routingNodes) { - shards.addAll(routingNode.shardsWithState(state)); - } if (state == ShardRoutingState.UNASSIGNED) { routingNodes.unassigned().forEach(shards::add); + } else { + for (RoutingNode routingNode : routingNodes) { + shards.addAll(routingNode.shardsWithState(state)); + } } return shards; } - public static List shardsWithState(RoutingNodes routingNodes, String index, ShardRoutingState... state) { + public static List shardsWithState(RoutingNodes routingNodes, String index, ShardRoutingState... states) { List shards = new ArrayList<>(); - for (RoutingNode routingNode : routingNodes) { - shards.addAll(routingNode.shardsWithState(index, state)); - } - for (ShardRoutingState s : state) { - if (s == ShardRoutingState.UNASSIGNED) { + for (ShardRoutingState state : states) { + if (state == ShardRoutingState.UNASSIGNED) { for (ShardRouting unassignedShard : routingNodes.unassigned()) { if (unassignedShard.index().getName().equals(index)) { shards.add(unassignedShard); } } - break; + } else { + for (RoutingNode routingNode : routingNodes) { + shards.addAll(routingNode.shardsWithState(index, state)); + } } } return shards; @@ -64,7 +65,6 @@ public static RoutingNode routingNode(String nodeId, DiscoveryNode node, ShardRo for (ShardRouting shardRouting : shards) { routingNode.add(shardRouting); } - return routingNode; } } diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherIndexingListenerTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherIndexingListenerTests.java index c682ca93488d..985a0948516e 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherIndexingListenerTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherIndexingListenerTests.java @@ -330,7 +330,7 @@ public void testClusterChangedWatchAliasChanged() throws Exception { boolean emptyShards = randomBoolean(); if (emptyShards) { - when(routingNode.shardsWithState(eq(newActiveWatchIndex), any())).thenReturn(Collections.emptyList()); + when(routingNode.shardsWithState(eq(newActiveWatchIndex), any(ShardRoutingState[].class))).thenReturn(Collections.emptyList()); } else { Index index = new Index(newActiveWatchIndex, "uuid"); ShardId shardId = new ShardId(index, 0); From 75de8f85cf41374fcdc5da3a36f86f9098b81951 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 30 Nov 2022 14:47:25 +0000 Subject: [PATCH 117/919] Chunked encoding for RestGetIndicesAction (#92016) This response scales with the number of indices requested and can reach many MiB in size in a large cluster, let's use chunking here. Relates #89838 --- .../rest/Netty4HeadBodyIsEmptyIT.java | 8 +- .../elasticsearch/action/ActionModule.java | 2 +- .../admin/indices/get/GetIndexResponse.java | 95 ++++++++++--------- .../admin/indices/RestGetIndicesAction.java | 14 +-- .../indices/get/GetIndexResponseTests.java | 19 ++++ .../indices/RestGetIndicesActionTests.java | 5 +- 6 files changed, 77 insertions(+), 66 deletions(-) diff --git a/modules/transport-netty4/src/javaRestTest/java/org/elasticsearch/rest/Netty4HeadBodyIsEmptyIT.java b/modules/transport-netty4/src/javaRestTest/java/org/elasticsearch/rest/Netty4HeadBodyIsEmptyIT.java index b1e2a6d7fdd1..e705c4303687 100644 --- a/modules/transport-netty4/src/javaRestTest/java/org/elasticsearch/rest/Netty4HeadBodyIsEmptyIT.java +++ b/modules/transport-netty4/src/javaRestTest/java/org/elasticsearch/rest/Netty4HeadBodyIsEmptyIT.java @@ -25,6 +25,7 @@ import static org.elasticsearch.rest.RestStatus.OK; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.nullValue; public class Netty4HeadBodyIsEmptyIT extends ESRestTestCase { public void testHeadRoot() throws IOException { @@ -59,8 +60,8 @@ public void testDocumentExists() throws IOException { public void testIndexExists() throws IOException { createTestDoc(); - headTestCase("/test", emptyMap(), greaterThan(0)); - headTestCase("/test", singletonMap("pretty", "true"), greaterThan(0)); + headTestCase("/test", emptyMap(), nullValue(Integer.class)); + headTestCase("/test", singletonMap("pretty", "true"), nullValue(Integer.class)); } public void testAliasExists() throws IOException { @@ -177,7 +178,8 @@ private void headTestCase( request.setOptions(expectWarnings(expectedWarnings)); Response response = client().performRequest(request); assertEquals(expectedStatusCode, response.getStatusLine().getStatusCode()); - assertThat(Integer.valueOf(response.getHeader("Content-Length")), matcher); + final var contentLength = response.getHeader("Content-Length"); + assertThat(contentLength == null ? null : Integer.valueOf(contentLength), matcher); assertNull("HEAD requests shouldn't have a response body but " + url + " did", response.getEntity()); } diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index 40ee8e4db171..59d055e27415 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -768,7 +768,7 @@ public void initRestHandlers(Supplier nodesInCluster) { registerHandler.accept(new RestResetFeatureStateAction()); registerHandler.accept(new RestGetFeatureUpgradeStatusAction()); registerHandler.accept(new RestPostFeatureUpgradeAction()); - registerHandler.accept(new RestGetIndicesAction(threadPool)); + registerHandler.accept(new RestGetIndicesAction()); registerHandler.accept(new RestIndicesStatsAction()); registerHandler.accept(new RestIndicesSegmentsAction()); registerHandler.accept(new RestIndicesShardStoresAction()); diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/get/GetIndexResponse.java b/server/src/main/java/org/elasticsearch/action/admin/indices/get/GetIndexResponse.java index 4f8f24ea5b72..1e96b950c7a1 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/get/GetIndexResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/get/GetIndexResponse.java @@ -13,16 +13,18 @@ import org.elasticsearch.cluster.metadata.AliasMetadata; import org.elasticsearch.cluster.metadata.MappingMetadata; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.core.RestApiVersion; import org.elasticsearch.index.mapper.MapperService; -import org.elasticsearch.xcontent.ToXContentObject; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import java.io.IOException; import java.util.Arrays; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -33,7 +35,7 @@ /** * A response for a get index action. */ -public class GetIndexResponse extends ActionResponse implements ToXContentObject { +public class GetIndexResponse extends ActionResponse implements ChunkedToXContent { private Map mappings = Map.of(); private Map> aliases = Map.of(); @@ -178,59 +180,58 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - { - for (final String index : indices) { + public Iterator toXContentChunked(ToXContent.Params ignored) { + return Iterators.concat( + Iterators.single((builder, params) -> builder.startObject()), + Arrays.stream(indices).map(index -> (builder, params) -> { builder.startObject(index); - { - builder.startObject("aliases"); - List indexAliases = aliases.get(index); - if (indexAliases != null) { - for (final AliasMetadata alias : indexAliases) { - AliasMetadata.Builder.toXContent(alias, builder, params); - } - } - builder.endObject(); - - MappingMetadata indexMappings = mappings.get(index); - if (indexMappings == null) { - builder.startObject("mappings").endObject(); - } else { - if (builder.getRestApiVersion() == RestApiVersion.V_7 - && params.paramAsBoolean(INCLUDE_TYPE_NAME_PARAMETER, DEFAULT_INCLUDE_TYPE_NAME_POLICY)) { - builder.startObject("mappings"); - builder.field(MapperService.SINGLE_MAPPING_NAME, indexMappings.sourceAsMap()); - builder.endObject(); - } else { - builder.field("mappings", indexMappings.sourceAsMap()); - } - } - builder.startObject("settings"); - Settings indexSettings = settings.get(index); - if (indexSettings != null) { - indexSettings.toXContent(builder, params); + builder.startObject("aliases"); + List indexAliases = aliases.get(index); + if (indexAliases != null) { + for (final AliasMetadata alias : indexAliases) { + AliasMetadata.Builder.toXContent(alias, builder, params); } - builder.endObject(); + } + builder.endObject(); - Settings defaultIndexSettings = defaultSettings.get(index); - if (defaultIndexSettings != null && defaultIndexSettings.isEmpty() == false) { - builder.startObject("defaults"); - defaultIndexSettings.toXContent(builder, params); + MappingMetadata indexMappings = mappings.get(index); + if (indexMappings == null) { + builder.startObject("mappings").endObject(); + } else { + if (builder.getRestApiVersion() == RestApiVersion.V_7 + && params.paramAsBoolean(INCLUDE_TYPE_NAME_PARAMETER, DEFAULT_INCLUDE_TYPE_NAME_POLICY)) { + builder.startObject("mappings"); + builder.field(MapperService.SINGLE_MAPPING_NAME, indexMappings.sourceAsMap()); builder.endObject(); + } else { + builder.field("mappings", indexMappings.sourceAsMap()); } + } - String dataStream = dataStreams.get(index); - if (dataStream != null) { - builder.field("data_stream", dataStream); - } + builder.startObject("settings"); + Settings indexSettings = settings.get(index); + if (indexSettings != null) { + indexSettings.toXContent(builder, params); } builder.endObject(); - } - } - builder.endObject(); - return builder; + + Settings defaultIndexSettings = defaultSettings.get(index); + if (defaultIndexSettings != null && defaultIndexSettings.isEmpty() == false) { + builder.startObject("defaults"); + defaultIndexSettings.toXContent(builder, params); + builder.endObject(); + } + + String dataStream = dataStreams.get(index); + if (dataStream != null) { + builder.field("data_stream", dataStream); + } + + return builder.endObject(); + }).iterator(), + Iterators.single((builder, params) -> builder.endObject()) + ); } @Override diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesAction.java index 11c725695aa1..46a686d6e74b 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesAction.java @@ -17,9 +17,8 @@ import org.elasticsearch.core.RestApiVersion; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; -import org.elasticsearch.rest.action.DispatchingRestToXContentListener; import org.elasticsearch.rest.action.RestCancellableNodeClient; -import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.rest.action.RestChunkedToXContentListener; import java.io.IOException; import java.util.List; @@ -39,12 +38,6 @@ public class RestGetIndicesAction extends BaseRestHandler { private static final Set COMPATIBLE_RESPONSE_PARAMS = addToCopy(Settings.FORMAT_PARAMS, INCLUDE_TYPE_NAME_PARAMETER); - private final ThreadPool threadPool; - - public RestGetIndicesAction(ThreadPool threadPool) { - this.threadPool = threadPool; - } - @Override public List routes() { return List.of(new Route(GET, "/{index}"), new Route(HEAD, "/{index}")); @@ -76,10 +69,7 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC final var httpChannel = request.getHttpChannel(); return channel -> new RestCancellableNodeClient(client, httpChannel).admin() .indices() - .getIndex( - getIndexRequest, - new DispatchingRestToXContentListener<>(threadPool.executor(ThreadPool.Names.MANAGEMENT), channel, request) - ); + .getIndex(getIndexRequest, new RestChunkedToXContentListener<>(channel)); } /** diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/get/GetIndexResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/get/GetIndexResponseTests.java index 3f6f0baaabf2..85ab886542a8 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/get/GetIndexResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/get/GetIndexResponseTests.java @@ -18,7 +18,9 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.RandomCreateIndexGenerator; import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.xcontent.ToXContent; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -27,6 +29,9 @@ import java.util.Locale; import java.util.Map; +import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; +import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; + public class GetIndexResponseTests extends AbstractWireSerializingTestCase { @Override @@ -73,4 +78,18 @@ protected GetIndexResponse createTestInstance() { } return new GetIndexResponse(indices, mappings, aliases, settings, defaultSettings, dataStreams); } + + public void testChunking() throws IOException { + final var response = createTestInstance(); + + try (var builder = jsonBuilder()) { + int chunkCount = 0; + final var iterator = response.toXContentChunked(EMPTY_PARAMS); + while (iterator.hasNext()) { + iterator.next().toXContent(builder, ToXContent.EMPTY_PARAMS); + chunkCount += 1; + } + assertEquals(response.getIndices().length + 2, chunkCount); + } // closing the builder verifies that the XContent is well-formed + } } diff --git a/server/src/test/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesActionTests.java b/server/src/test/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesActionTests.java index 164fc38d15c4..5885c6f8c988 100644 --- a/server/src/test/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesActionTests.java +++ b/server/src/test/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesActionTests.java @@ -9,7 +9,6 @@ package org.elasticsearch.rest.action.admin.indices; import org.elasticsearch.client.internal.node.NodeClient; -import org.elasticsearch.common.util.concurrent.DeterministicTaskQueue; import org.elasticsearch.core.RestApiVersion; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.test.ESTestCase; @@ -37,7 +36,7 @@ public void testIncludeTypeNamesWarning() throws IOException { Map.of("Content-Type", contentTypeHeader, "Accept", contentTypeHeader) ).withMethod(RestRequest.Method.GET).withPath("/some_index").withParams(params).build(); - RestGetIndicesAction handler = new RestGetIndicesAction(new DeterministicTaskQueue().getThreadPool()); + RestGetIndicesAction handler = new RestGetIndicesAction(); handler.prepareRequest(request, mock(NodeClient.class)); assertCriticalWarnings(RestGetIndicesAction.TYPES_DEPRECATION_MESSAGE); @@ -58,7 +57,7 @@ public void testIncludeTypeNamesWarningExists() throws IOException { Map.of("Content-Type", contentTypeHeader, "Accept", contentTypeHeader) ).withMethod(RestRequest.Method.HEAD).withPath("/some_index").withParams(params).build(); - RestGetIndicesAction handler = new RestGetIndicesAction(new DeterministicTaskQueue().getThreadPool()); + RestGetIndicesAction handler = new RestGetIndicesAction(); handler.prepareRequest(request, mock(NodeClient.class)); } } From 37988dd094ff90fcd755550256a30df285362389 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 30 Nov 2022 15:03:55 +0000 Subject: [PATCH 118/919] Ensure cached time elapses in ClusterServiceIT (#91986) Rather than just checking `System.nanoTime()` we should verify that each thread pool's cached time has elapsed here. --- .../cluster/service/ClusterServiceIT.java | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/cluster/service/ClusterServiceIT.java b/server/src/internalClusterTest/java/org/elasticsearch/cluster/service/ClusterServiceIT.java index cd2f3ebf561d..24950fe160e8 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/cluster/service/ClusterServiceIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/cluster/service/ClusterServiceIT.java @@ -17,6 +17,7 @@ import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.test.ESIntegTestCase.ClusterScope; import org.elasticsearch.test.ESIntegTestCase.Scope; +import org.elasticsearch.threadpool.ThreadPool; import java.util.Arrays; import java.util.HashSet; @@ -25,6 +26,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.StreamSupport; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; @@ -414,11 +416,7 @@ public void onFailure(Exception e) { }); } - final var startNanoTime = System.nanoTime(); - while (TimeUnit.MILLISECONDS.convert(System.nanoTime() - startNanoTime, TimeUnit.NANOSECONDS) <= 0) { - // noinspection BusyWait - Thread.sleep(100); - } + waitForTimeToElapse(); pendingClusterTasks = clusterService.getMasterService().pendingTasks(); assertThat(pendingClusterTasks.size(), greaterThanOrEqualTo(5)); @@ -441,4 +439,28 @@ public void onFailure(Exception e) { block2.countDown(); } } + + private static void waitForTimeToElapse() throws InterruptedException { + final ThreadPool[] threadPools = StreamSupport.stream(internalCluster().getInstances(ClusterService.class).spliterator(), false) + .map(ClusterService::threadPool) + .toArray(ThreadPool[]::new); + final long[] startTimes = Arrays.stream(threadPools).mapToLong(ThreadPool::relativeTimeInMillis).toArray(); + + final var startNanoTime = System.nanoTime(); + while (TimeUnit.MILLISECONDS.convert(System.nanoTime() - startNanoTime, TimeUnit.NANOSECONDS) <= 100) { + // noinspection BusyWait + Thread.sleep(100); + } + + outer: do { + for (int i = 0; i < threadPools.length; i++) { + if (threadPools[i].relativeTimeInMillis() <= startTimes[i]) { + // noinspection BusyWait + Thread.sleep(100); + continue outer; + } + } + return; + } while (true); + } } From 4798f050e8d6531eb989756432eb52a5121acdc6 Mon Sep 17 00:00:00 2001 From: Joe Gallo Date: Wed, 30 Nov 2022 11:09:52 -0500 Subject: [PATCH 119/919] Remove IndexerState from HLRC (#92023) --- .../client/core/IndexerState.java | 47 ------------------- x-pack/qa/rolling-upgrade/build.gradle | 1 + .../upgrades/TransformSurvivesUpgradeIT.java | 2 +- 3 files changed, 2 insertions(+), 48 deletions(-) delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/core/IndexerState.java diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/IndexerState.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/IndexerState.java deleted file mode 100644 index 95ca55505261..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/IndexerState.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client.core; - -import java.util.Locale; - -/** - * IndexerState represents the internal state of the indexer. It - * is also persistent when changing from started/stopped in case the allocated - * task is restarted elsewhere. - */ -public enum IndexerState { - /** Indexer is running, but not actively indexing data (e.g. it's idle). */ - STARTED, - - /** Indexer is actively indexing data. */ - INDEXING, - - /** - * Transition state to where an indexer has acknowledged the stop - * but is still in process of halting. - */ - STOPPING, - - /** Indexer is "paused" and ignoring scheduled triggers. */ - STOPPED, - - /** - * Something (internal or external) has requested the indexer abort - * and shutdown. - */ - ABORTING; - - public static IndexerState fromString(String name) { - return valueOf(name.trim().toUpperCase(Locale.ROOT)); - } - - public String value() { - return name().toLowerCase(Locale.ROOT); - } -} diff --git a/x-pack/qa/rolling-upgrade/build.gradle b/x-pack/qa/rolling-upgrade/build.gradle index 1a55844b41a4..02a2074b2b5b 100644 --- a/x-pack/qa/rolling-upgrade/build.gradle +++ b/x-pack/qa/rolling-upgrade/build.gradle @@ -8,6 +8,7 @@ apply plugin: 'elasticsearch.bwc-test' apply plugin: 'elasticsearch.rest-resources' dependencies { + testImplementation testArtifact(project(xpackModule('core'))) testImplementation project(':x-pack:qa') testImplementation project(':client:rest-high-level') } diff --git a/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/TransformSurvivesUpgradeIT.java b/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/TransformSurvivesUpgradeIT.java index 0deec0317b3a..71469b3fe2fa 100644 --- a/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/TransformSurvivesUpgradeIT.java +++ b/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/TransformSurvivesUpgradeIT.java @@ -14,13 +14,13 @@ import org.elasticsearch.client.Response; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestClientBuilder; -import org.elasticsearch.client.core.IndexerState; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.support.XContentMapValues; import org.elasticsearch.core.Booleans; import org.elasticsearch.core.TimeValue; import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xpack.core.indexing.IndexerState; import java.io.IOException; import java.time.Instant; From 73122f01e231139230dc314ae2ba8ff7e545d5c1 Mon Sep 17 00:00:00 2001 From: Joe Gallo Date: Wed, 30 Nov 2022 11:40:06 -0500 Subject: [PATCH 120/919] Remove unused methods and classes from HLRC (#92012) --- .../client/GetAliasesResponse.java | 187 ---------- .../elasticsearch/client/NodesResponse.java | 49 --- .../client/NodesResponseHeader.java | 127 ------- .../client/RequestConverters.java | 344 ------------------ .../client/RestHighLevelClient.java | 69 ---- .../elasticsearch/client/TimedRequest.java | 57 --- .../asyncsearch/DeleteAsyncSearchRequest.java | 43 --- .../asyncsearch/GetAsyncSearchRequest.java | 72 ---- .../asyncsearch/SubmitAsyncSearchRequest.java | 265 -------------- .../client/cluster/RemoteInfoRequest.java | 17 - .../client/cluster/RemoteInfoResponse.java | 48 --- .../client/core/IndexerJobStats.java | 226 ------------ .../client/license/LicenseStatus.java | 37 -- 13 files changed, 1541 deletions(-) delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/GetAliasesResponse.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/NodesResponse.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/NodesResponseHeader.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/TimedRequest.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/asyncsearch/DeleteAsyncSearchRequest.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/asyncsearch/GetAsyncSearchRequest.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/asyncsearch/SubmitAsyncSearchRequest.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoRequest.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoResponse.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/core/IndexerJobStats.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/license/LicenseStatus.java diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/GetAliasesResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/GetAliasesResponse.java deleted file mode 100644 index c5a1d97b7edf..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/GetAliasesResponse.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client; - -import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.cluster.metadata.AliasMetadata; -import org.elasticsearch.common.xcontent.StatusToXContentObject; -import org.elasticsearch.rest.RestStatus; -import org.elasticsearch.xcontent.ToXContent; -import org.elasticsearch.xcontent.XContentBuilder; -import org.elasticsearch.xcontent.XContentParser; -import org.elasticsearch.xcontent.XContentParser.Token; - -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; - -/** - * Response obtained from the get aliases API. - * The format is pretty horrible as it holds aliases, but at the same time errors can come back through the status and error fields. - * Such errors are mostly 404 - NOT FOUND for aliases that were specified but not found. In such case the client won't throw exception - * so it allows to retrieve the returned aliases, while at the same time checking if errors were returned. - * There's also the case where an exception is returned, like for instance an {@link org.elasticsearch.index.IndexNotFoundException}. - * We would usually throw such exception, but we configure the client to not throw for 404 to support the case above, hence we also not - * throw in case an index is not found, although it is a hard error that doesn't come back with aliases. - */ -public class GetAliasesResponse implements StatusToXContentObject { - - private final RestStatus status; - private final String error; - private final ElasticsearchException exception; - - private final Map> aliases; - - GetAliasesResponse(RestStatus status, String error, Map> aliases) { - this.status = status; - this.error = error; - this.aliases = aliases; - this.exception = null; - } - - private GetAliasesResponse(RestStatus status, ElasticsearchException exception) { - this.status = status; - this.error = null; - this.aliases = Collections.emptyMap(); - this.exception = exception; - } - - @Override - public RestStatus status() { - return status; - } - - /** - * Return the possibly returned error, null otherwise - */ - public String getError() { - return error; - } - - /** - * Return the exception that may have been returned - */ - public ElasticsearchException getException() { - return exception; - } - - /** - * Return the requested aliases - */ - public Map> getAliases() { - return aliases; - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - { - if (status != RestStatus.OK) { - builder.field("error", error); - builder.field("status", status.getStatus()); - } - - for (Map.Entry> entry : aliases.entrySet()) { - builder.startObject(entry.getKey()); - { - builder.startObject("aliases"); - { - for (final AliasMetadata alias : entry.getValue()) { - AliasMetadata.Builder.toXContent(alias, builder, ToXContent.EMPTY_PARAMS); - } - } - builder.endObject(); - } - builder.endObject(); - } - } - builder.endObject(); - return builder; - } - - /** - * Parse the get aliases response - */ - public static GetAliasesResponse fromXContent(XContentParser parser) throws IOException { - if (parser.currentToken() == null) { - parser.nextToken(); - } - ensureExpectedToken(Token.START_OBJECT, parser.currentToken(), parser); - Map> aliases = new HashMap<>(); - - String currentFieldName; - Token token; - String error = null; - ElasticsearchException exception = null; - RestStatus status = RestStatus.OK; - - while (parser.nextToken() != Token.END_OBJECT) { - if (parser.currentToken() == Token.FIELD_NAME) { - currentFieldName = parser.currentName(); - - if ("status".equals(currentFieldName)) { - if ((token = parser.nextToken()) != Token.FIELD_NAME) { - ensureExpectedToken(Token.VALUE_NUMBER, token, parser); - status = RestStatus.fromCode(parser.intValue()); - } - } else if ("error".equals(currentFieldName)) { - token = parser.nextToken(); - if (token == Token.VALUE_STRING) { - error = parser.text(); - } else if (token == Token.START_OBJECT) { - parser.nextToken(); - exception = ElasticsearchException.innerFromXContent(parser, true); - } else if (token == Token.START_ARRAY) { - parser.skipChildren(); - } - } else { - String indexName = parser.currentName(); - if (parser.nextToken() == Token.START_OBJECT) { - Set parseInside = parseAliases(parser); - aliases.put(indexName, parseInside); - } - } - } - } - if (exception != null) { - assert error == null; - assert aliases.isEmpty(); - return new GetAliasesResponse(status, exception); - } - return new GetAliasesResponse(status, error, aliases); - } - - private static Set parseAliases(XContentParser parser) throws IOException { - Set aliases = new HashSet<>(); - Token token; - String currentFieldName = null; - while ((token = parser.nextToken()) != Token.END_OBJECT) { - if (token == Token.FIELD_NAME) { - currentFieldName = parser.currentName(); - } else if (token == Token.START_OBJECT) { - if ("aliases".equals(currentFieldName)) { - while (parser.nextToken() != Token.END_OBJECT) { - AliasMetadata fromXContent = AliasMetadata.Builder.fromXContent(parser); - aliases.add(fromXContent); - } - } else { - parser.skipChildren(); - } - } else if (token == Token.START_ARRAY) { - parser.skipChildren(); - } - } - return aliases; - } -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/NodesResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/NodesResponse.java deleted file mode 100644 index a0c38498591f..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/NodesResponse.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client; - -import org.elasticsearch.xcontent.ConstructingObjectParser; -import org.elasticsearch.xcontent.ParseField; - -/** - * Base class for responses that are node responses. These responses always contain the cluster - * name and the {@link NodesResponseHeader}. - */ -public abstract class NodesResponse { - - private final NodesResponseHeader header; - private final String clusterName; - - protected NodesResponse(NodesResponseHeader header, String clusterName) { - this.header = header; - this.clusterName = clusterName; - } - - /** - * Get the cluster name associated with all of the nodes. - * - * @return Never {@code null}. - */ - public String getClusterName() { - return clusterName; - } - - /** - * Gets information about the number of total, successful and failed nodes the request was run on. - * Also includes exceptions if relevant. - */ - public NodesResponseHeader getHeader() { - return header; - } - - public static void declareCommonNodesResponseParsing(ConstructingObjectParser parser) { - parser.declareObject(ConstructingObjectParser.constructorArg(), NodesResponseHeader::fromXContent, new ParseField("_nodes")); - parser.declareString(ConstructingObjectParser.constructorArg(), new ParseField("cluster_name")); - } -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/NodesResponseHeader.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/NodesResponseHeader.java deleted file mode 100644 index e22326dc88fb..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/NodesResponseHeader.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -package org.elasticsearch.client; - -import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.action.support.nodes.BaseNodesResponse; -import org.elasticsearch.core.Nullable; -import org.elasticsearch.rest.action.RestActions; -import org.elasticsearch.xcontent.ConstructingObjectParser; -import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.ToXContent; -import org.elasticsearch.xcontent.XContentBuilder; -import org.elasticsearch.xcontent.XContentParser; - -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -/** - * A utility class to parse the Nodes Header returned by - * {@link RestActions#buildNodesHeader(XContentBuilder, ToXContent.Params, BaseNodesResponse)}. - */ -public final class NodesResponseHeader { - - public static final ParseField TOTAL = new ParseField("total"); - public static final ParseField SUCCESSFUL = new ParseField("successful"); - public static final ParseField FAILED = new ParseField("failed"); - public static final ParseField FAILURES = new ParseField("failures"); - - @SuppressWarnings("unchecked") - public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - "nodes_response_header", - true, - (a) -> { - int i = 0; - int total = (Integer) a[i++]; - int successful = (Integer) a[i++]; - int failed = (Integer) a[i++]; - List failures = (List) a[i++]; - return new NodesResponseHeader(total, successful, failed, failures); - } - ); - - static { - PARSER.declareInt(ConstructingObjectParser.constructorArg(), TOTAL); - PARSER.declareInt(ConstructingObjectParser.constructorArg(), SUCCESSFUL); - PARSER.declareInt(ConstructingObjectParser.constructorArg(), FAILED); - PARSER.declareObjectArray( - ConstructingObjectParser.optionalConstructorArg(), - (p, c) -> ElasticsearchException.fromXContent(p), - FAILURES - ); - } - - private final int total; - private final int successful; - private final int failed; - private final List failures; - - public NodesResponseHeader(int total, int successful, int failed, @Nullable List failures) { - this.total = total; - this.successful = successful; - this.failed = failed; - this.failures = failures == null ? Collections.emptyList() : failures; - } - - public static NodesResponseHeader fromXContent(XContentParser parser, Void context) throws IOException { - return PARSER.parse(parser, context); - } - - /** the total number of nodes that the operation was carried on */ - public int getTotal() { - return total; - } - - /** the number of nodes that the operation has failed on */ - public int getFailed() { - return failed; - } - - /** the number of nodes that the operation was successful on */ - public int getSuccessful() { - return successful; - } - - /** - * Get the failed node exceptions. - * - * @return Never {@code null}. Can be empty. - */ - public List getFailures() { - return failures; - } - - /** - * Determine if there are any node failures in {@link #failures}. - * - * @return {@code true} if {@link #failures} contains at least 1 exception. - */ - public boolean hasFailures() { - return failures.isEmpty() == false; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - NodesResponseHeader that = (NodesResponseHeader) o; - return total == that.total && successful == that.successful && failed == that.failed && Objects.equals(failures, that.failures); - } - - @Override - public int hashCode() { - return Objects.hash(total, successful, failed, failures); - } - -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java index 1cba9261ec09..6d2b06e1d1c3 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java @@ -18,10 +18,6 @@ import org.apache.http.nio.entity.NByteArrayEntity; import org.apache.lucene.util.BytesRef; import org.elasticsearch.action.DocWriteRequest; -import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest; -import org.elasticsearch.action.admin.cluster.storedscripts.DeleteStoredScriptRequest; -import org.elasticsearch.action.admin.cluster.storedscripts.GetStoredScriptRequest; -import org.elasticsearch.action.admin.cluster.storedscripts.PutStoredScriptRequest; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.explain.ExplainRequest; @@ -29,10 +25,7 @@ import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.get.MultiGetRequest; import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.action.search.ClearScrollRequest; -import org.elasticsearch.action.search.ClosePointInTimeRequest; import org.elasticsearch.action.search.MultiSearchRequest; -import org.elasticsearch.action.search.OpenPointInTimeRequest; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchScrollRequest; import org.elasticsearch.action.support.ActiveShardCount; @@ -41,11 +34,8 @@ import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.client.core.CountRequest; import org.elasticsearch.client.core.GetSourceRequest; -import org.elasticsearch.client.core.MultiTermVectorsRequest; import org.elasticsearch.client.core.TermVectorsRequest; import org.elasticsearch.client.internal.Requests; -import org.elasticsearch.cluster.health.ClusterHealthStatus; -import org.elasticsearch.common.Priority; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.lucene.uid.Versions; @@ -55,15 +45,10 @@ import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.TimeValue; import org.elasticsearch.index.VersionType; -import org.elasticsearch.index.rankeval.RankEvalRequest; import org.elasticsearch.index.reindex.AbstractBulkByScrollRequest; -import org.elasticsearch.index.reindex.DeleteByQueryRequest; import org.elasticsearch.index.reindex.ReindexRequest; -import org.elasticsearch.index.reindex.UpdateByQueryRequest; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.rest.action.search.RestSearchAction; -import org.elasticsearch.script.mustache.MultiSearchTemplateRequest; -import org.elasticsearch.script.mustache.SearchTemplateRequest; import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.xcontent.DeprecationHandler; import org.elasticsearch.xcontent.NamedXContentRegistry; @@ -78,9 +63,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; -import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Locale; import java.util.Map; import java.util.StringJoiner; @@ -335,10 +318,6 @@ static Request index(IndexRequest indexRequest) { return request; } - static Request ping() { - return new Request(HttpHead.METHOD_NAME, "/"); - } - static Request update(UpdateRequest updateRequest) throws IOException { String endpoint = endpoint(updateRequest.index(), "_update", updateRequest.id()); Request request = new Request(HttpPost.METHOD_NAME, endpoint); @@ -439,31 +418,6 @@ static Request searchScroll(SearchScrollRequest searchScrollRequest) throws IOEx return request; } - static Request clearScroll(ClearScrollRequest clearScrollRequest) throws IOException { - Request request = new Request(HttpDelete.METHOD_NAME, "/_search/scroll"); - request.setEntity(createEntity(clearScrollRequest, REQUEST_BODY_CONTENT_TYPE)); - return request; - } - - static Request openPointInTime(OpenPointInTimeRequest openRequest) { - Request request = new Request(HttpPost.METHOD_NAME, endpoint(openRequest.indices(), "_pit")); - Params params = new Params(); - if (OpenPointInTimeRequest.DEFAULT_INDICES_OPTIONS.equals(openRequest.indicesOptions()) == false) { - params.withIndicesOptions(openRequest.indicesOptions()); - } - params.withRouting(openRequest.routing()); - params.withPreference(openRequest.preference()); - params.putParam("keep_alive", openRequest.keepAlive()); - request.addParameters(params.asMap()); - return request; - } - - static Request closePointInTime(ClosePointInTimeRequest closeRequest) throws IOException { - Request request = new Request(HttpDelete.METHOD_NAME, "/_pit"); - request.setEntity(createEntity(closeRequest, REQUEST_BODY_CONTENT_TYPE)); - return request; - } - static Request multiSearch(MultiSearchRequest multiSearchRequest) throws IOException { Request request = new Request(HttpPost.METHOD_NAME, "/_msearch"); @@ -480,41 +434,6 @@ static Request multiSearch(MultiSearchRequest multiSearchRequest) throws IOExcep return request; } - static Request searchTemplate(SearchTemplateRequest searchTemplateRequest) throws IOException { - Request request; - - if (searchTemplateRequest.isSimulate()) { - request = new Request(HttpGet.METHOD_NAME, "_render/template"); - } else { - SearchRequest searchRequest = searchTemplateRequest.getRequest(); - String endpoint = endpoint(searchRequest.indices(), "_search/template"); - request = new Request(HttpPost.METHOD_NAME, endpoint); - - Params params = new Params(); - addSearchRequestParams(params, searchRequest); - request.addParameters(params.asMap()); - } - - request.setEntity(createEntity(searchTemplateRequest, REQUEST_BODY_CONTENT_TYPE)); - return request; - } - - static Request multiSearchTemplate(MultiSearchTemplateRequest multiSearchTemplateRequest) throws IOException { - Request request = new Request(HttpPost.METHOD_NAME, "/_msearch/template"); - - Params params = new Params(); - params.putParam(RestSearchAction.TYPED_KEYS_PARAM, "true"); - if (multiSearchTemplateRequest.maxConcurrentSearchRequests() != MultiSearchRequest.MAX_CONCURRENT_SEARCH_REQUESTS_DEFAULT) { - params.putParam("max_concurrent_searches", Integer.toString(multiSearchTemplateRequest.maxConcurrentSearchRequests())); - } - request.addParameters(params.asMap()); - - XContent xContent = REQUEST_BODY_CONTENT_TYPE.xContent(); - byte[] source = MultiSearchTemplateRequest.writeMultiLineFormat(multiSearchTemplateRequest, xContent); - request.setEntity(new NByteArrayEntity(source, createContentType(xContent.type()))); - return request; - } - static Request count(CountRequest countRequest) throws IOException { Request request = new Request(HttpPost.METHOD_NAME, endpoint(countRequest.indices(), countRequest.types(), "_count")); Params params = new Params(); @@ -562,31 +481,10 @@ static Request fieldCaps(FieldCapabilitiesRequest fieldCapabilitiesRequest) thro return request; } - static Request rankEval(RankEvalRequest rankEvalRequest) throws IOException { - Request request = new Request(HttpGet.METHOD_NAME, endpoint(rankEvalRequest.indices(), Strings.EMPTY_ARRAY, "_rank_eval")); - - Params params = new Params(); - if (SearchRequest.DEFAULT_INDICES_OPTIONS.equals(rankEvalRequest.indicesOptions()) == false) { - params.withIndicesOptions(rankEvalRequest.indicesOptions()); - } - params.putParam("search_type", rankEvalRequest.searchType().name().toLowerCase(Locale.ROOT)); - request.addParameters(params.asMap()); - request.setEntity(createEntity(rankEvalRequest.getRankEvalSpec(), REQUEST_BODY_CONTENT_TYPE)); - return request; - } - static Request reindex(ReindexRequest reindexRequest) throws IOException { return prepareReindexRequest(reindexRequest, true); } - static Request deleteByQuery(DeleteByQueryRequest deleteByQueryRequest) throws IOException { - return prepareDeleteByQueryRequest(deleteByQueryRequest, true); - } - - static Request updateByQuery(UpdateByQueryRequest updateByQueryRequest) throws IOException { - return prepareUpdateByQueryRequest(updateByQueryRequest, true); - } - private static Request prepareReindexRequest(ReindexRequest reindexRequest, boolean waitForCompletion) throws IOException { String endpoint = new EndpointBuilder().addPathPart("_reindex").build(); Request request = new Request(HttpPost.METHOD_NAME, endpoint); @@ -607,109 +505,6 @@ private static Request prepareReindexRequest(ReindexRequest reindexRequest, bool return request; } - private static Request prepareDeleteByQueryRequest(DeleteByQueryRequest deleteByQueryRequest, boolean waitForCompletion) - throws IOException { - String endpoint = endpoint(deleteByQueryRequest.indices(), "_delete_by_query"); - Request request = new Request(HttpPost.METHOD_NAME, endpoint); - Params params = new Params().withRouting(deleteByQueryRequest.getRouting()) - .withRefresh(deleteByQueryRequest.isRefresh()) - .withTimeout(deleteByQueryRequest.getTimeout()) - .withWaitForActiveShards(deleteByQueryRequest.getWaitForActiveShards()) - .withRequestsPerSecond(deleteByQueryRequest.getRequestsPerSecond()) - .withWaitForCompletion(waitForCompletion) - .withSlices(deleteByQueryRequest.getSlices()); - - if (SearchRequest.DEFAULT_INDICES_OPTIONS.equals(deleteByQueryRequest.indicesOptions()) == false) { - params = params.withIndicesOptions(deleteByQueryRequest.indicesOptions()); - } - - if (deleteByQueryRequest.isAbortOnVersionConflict() == false) { - params.putParam("conflicts", "proceed"); - } - if (deleteByQueryRequest.getBatchSize() != AbstractBulkByScrollRequest.DEFAULT_SCROLL_SIZE) { - params.putParam("scroll_size", Integer.toString(deleteByQueryRequest.getBatchSize())); - } - if (deleteByQueryRequest.getScrollTime() != AbstractBulkByScrollRequest.DEFAULT_SCROLL_TIMEOUT) { - params.putParam("scroll", deleteByQueryRequest.getScrollTime()); - } - if (deleteByQueryRequest.getMaxDocs() > 0) { - params.putParam("max_docs", Integer.toString(deleteByQueryRequest.getMaxDocs())); - } - request.addParameters(params.asMap()); - request.setEntity(createEntity(deleteByQueryRequest, REQUEST_BODY_CONTENT_TYPE)); - return request; - } - - static Request prepareUpdateByQueryRequest(UpdateByQueryRequest updateByQueryRequest, boolean waitForCompletion) throws IOException { - String endpoint = endpoint(updateByQueryRequest.indices(), "_update_by_query"); - Request request = new Request(HttpPost.METHOD_NAME, endpoint); - Params params = new Params().withRouting(updateByQueryRequest.getRouting()) - .withPipeline(updateByQueryRequest.getPipeline()) - .withRefresh(updateByQueryRequest.isRefresh()) - .withTimeout(updateByQueryRequest.getTimeout()) - .withWaitForActiveShards(updateByQueryRequest.getWaitForActiveShards()) - .withRequestsPerSecond(updateByQueryRequest.getRequestsPerSecond()) - .withWaitForCompletion(waitForCompletion) - .withSlices(updateByQueryRequest.getSlices()); - if (SearchRequest.DEFAULT_INDICES_OPTIONS.equals(updateByQueryRequest.indicesOptions()) == false) { - params = params.withIndicesOptions(updateByQueryRequest.indicesOptions()); - } - if (updateByQueryRequest.isAbortOnVersionConflict() == false) { - params.putParam("conflicts", "proceed"); - } - if (updateByQueryRequest.getBatchSize() != AbstractBulkByScrollRequest.DEFAULT_SCROLL_SIZE) { - params.putParam("scroll_size", Integer.toString(updateByQueryRequest.getBatchSize())); - } - if (updateByQueryRequest.getScrollTime() != AbstractBulkByScrollRequest.DEFAULT_SCROLL_TIMEOUT) { - params.putParam("scroll", updateByQueryRequest.getScrollTime()); - } - if (updateByQueryRequest.getMaxDocs() > 0) { - params.putParam("max_docs", Integer.toString(updateByQueryRequest.getMaxDocs())); - } - request.addParameters(params.asMap()); - request.setEntity(createEntity(updateByQueryRequest, REQUEST_BODY_CONTENT_TYPE)); - return request; - } - - static Request rethrottleReindex(RethrottleRequest rethrottleRequest) { - return rethrottle(rethrottleRequest, "_reindex"); - } - - static Request rethrottleUpdateByQuery(RethrottleRequest rethrottleRequest) { - return rethrottle(rethrottleRequest, "_update_by_query"); - } - - static Request rethrottleDeleteByQuery(RethrottleRequest rethrottleRequest) { - return rethrottle(rethrottleRequest, "_delete_by_query"); - } - - private static Request rethrottle(RethrottleRequest rethrottleRequest, String firstPathPart) { - String endpoint = new EndpointBuilder().addPathPart(firstPathPart) - .addPathPart(rethrottleRequest.getTaskId().toString()) - .addPathPart("_rethrottle") - .build(); - Request request = new Request(HttpPost.METHOD_NAME, endpoint); - Params params = new Params().withRequestsPerSecond(rethrottleRequest.getRequestsPerSecond()); - // we set "group_by" to "none" because this is the response format we can parse back - params.putParam("group_by", "none"); - request.addParameters(params.asMap()); - return request; - } - - static Request putScript(PutStoredScriptRequest putStoredScriptRequest) throws IOException { - String endpoint = new EndpointBuilder().addPathPartAsIs("_scripts").addPathPart(putStoredScriptRequest.id()).build(); - Request request = new Request(HttpPost.METHOD_NAME, endpoint); - Params params = new Params(); - params.withTimeout(putStoredScriptRequest.timeout()); - params.withMasterTimeout(putStoredScriptRequest.masterNodeTimeout()); - if (Strings.hasText(putStoredScriptRequest.context())) { - params.putParam("context", putStoredScriptRequest.context()); - } - request.addParameters(params.asMap()); - request.setEntity(createEntity(putStoredScriptRequest, REQUEST_BODY_CONTENT_TYPE)); - return request; - } - static Request termVectors(TermVectorsRequest tvrequest) throws IOException { String endpoint; if (tvrequest.getType() != null) { @@ -733,32 +528,6 @@ static Request termVectors(TermVectorsRequest tvrequest) throws IOException { return request; } - static Request mtermVectors(MultiTermVectorsRequest mtvrequest) throws IOException { - String endpoint = "_mtermvectors"; - Request request = new Request(HttpGet.METHOD_NAME, endpoint); - request.setEntity(createEntity(mtvrequest, REQUEST_BODY_CONTENT_TYPE)); - return request; - } - - static Request getScript(GetStoredScriptRequest getStoredScriptRequest) { - String endpoint = new EndpointBuilder().addPathPartAsIs("_scripts").addPathPart(getStoredScriptRequest.id()).build(); - Request request = new Request(HttpGet.METHOD_NAME, endpoint); - Params params = new Params(); - params.withMasterTimeout(getStoredScriptRequest.masterNodeTimeout()); - request.addParameters(params.asMap()); - return request; - } - - static Request deleteScript(DeleteStoredScriptRequest deleteStoredScriptRequest) { - String endpoint = new EndpointBuilder().addPathPartAsIs("_scripts").addPathPart(deleteStoredScriptRequest.id()).build(); - Request request = new Request(HttpDelete.METHOD_NAME, endpoint); - Params params = new Params(); - params.withTimeout(deleteStoredScriptRequest.timeout()); - params.withMasterTimeout(deleteStoredScriptRequest.masterNodeTimeout()); - request.addParameters(params.asMap()); - return request; - } - static HttpEntity createEntity(ToXContent toXContent, XContentType xContentType) throws IOException { return createEntity(toXContent, xContentType, ToXContent.EMPTY_PARAMS); } @@ -783,10 +552,6 @@ static String endpoint(String index, String type, String id, String endpoint) { return new EndpointBuilder().addPathPart(index, type, id).addPathPartAsIs(endpoint).build(); } - static String endpoint(String[] indices) { - return new EndpointBuilder().addCommaSeparatedPathParts(indices).build(); - } - static String endpoint(String[] indices, String endpoint) { return new EndpointBuilder().addCommaSeparatedPathParts(indices).addPathPartAsIs(endpoint).build(); } @@ -799,13 +564,6 @@ static String endpoint(String[] indices, String[] types, String endpoint) { .build(); } - static String endpoint(String[] indices, String endpoint, String[] suffixes) { - return new EndpointBuilder().addCommaSeparatedPathParts(indices) - .addPathPartAsIs(endpoint) - .addCommaSeparatedPathParts(suffixes) - .build(); - } - @Deprecated static String endpoint(String[] indices, String endpoint, String type) { return new EndpointBuilder().addCommaSeparatedPathParts(indices).addPathPartAsIs(endpoint).addPathPart(type).build(); @@ -878,10 +636,6 @@ Params withFields(String[] fields) { return this; } - Params withMasterTimeout(TimeValue masterTimeout) { - return putParam("master_timeout", masterTimeout); - } - Params withPipeline(String pipeline) { return putParam("pipeline", pipeline); } @@ -1050,102 +804,9 @@ Params withIgnoreUnavailable(boolean ignoreUnavailable) { return this; } - Params withHuman(boolean human) { - if (human) { - putParam("human", Boolean.toString(human)); - } - return this; - } - - Params withLocal(boolean local) { - if (local) { - putParam("local", Boolean.toString(local)); - } - return this; - } - - Params withIncludeDefaults(boolean includeDefaults) { - if (includeDefaults) { - return putParam("include_defaults", Boolean.TRUE.toString()); - } - return this; - } - - Params withPreserveExisting(boolean preserveExisting) { - if (preserveExisting) { - return putParam("preserve_existing", Boolean.TRUE.toString()); - } - return this; - } - - Params withDetailed(boolean detailed) { - if (detailed) { - return putParam("detailed", Boolean.TRUE.toString()); - } - return this; - } - Params withWaitForCompletion(Boolean waitForCompletion) { return putParam("wait_for_completion", waitForCompletion.toString()); } - - Params withNodes(String[] nodes) { - return withNodes(Arrays.asList(nodes)); - } - - Params withNodes(List nodes) { - if (nodes != null && nodes.size() > 0) { - return putParam("nodes", String.join(",", nodes)); - } - return this; - } - - Params withActions(String[] actions) { - return withActions(Arrays.asList(actions)); - } - - Params withActions(List actions) { - if (actions != null && actions.size() > 0) { - return putParam("actions", String.join(",", actions)); - } - return this; - } - - Params withWaitForStatus(ClusterHealthStatus status) { - if (status != null) { - return putParam("wait_for_status", status.name().toLowerCase(Locale.ROOT)); - } - return this; - } - - Params withWaitForNoRelocatingShards(boolean waitNoRelocatingShards) { - if (waitNoRelocatingShards) { - return putParam("wait_for_no_relocating_shards", Boolean.TRUE.toString()); - } - return this; - } - - Params withWaitForNoInitializingShards(boolean waitNoInitShards) { - if (waitNoInitShards) { - return putParam("wait_for_no_initializing_shards", Boolean.TRUE.toString()); - } - return this; - } - - Params withWaitForNodes(String waitForNodes) { - return putParam("wait_for_nodes", waitForNodes); - } - - Params withLevel(ClusterHealthRequest.Level level) { - return putParam("level", level.name().toLowerCase(Locale.ROOT)); - } - - Params withWaitForEvents(Priority waitForEvents) { - if (waitForEvents != null) { - return putParam("wait_for_events", waitForEvents.name().toLowerCase(Locale.ROOT)); - } - return this; - } } /** @@ -1199,11 +860,6 @@ EndpointBuilder addCommaSeparatedPathParts(String[] parts) { return this; } - EndpointBuilder addCommaSeparatedPathParts(List parts) { - addPathPart(String.join(",", parts)); - return this; - } - EndpointBuilder addPathPartAsIs(String... parts) { for (String part : parts) { if (Strings.hasLength(part)) { diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java index 1ccdb1ae58df..06ff0bdd72a1 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java @@ -63,9 +63,7 @@ import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.core.CheckedFunction; import org.elasticsearch.index.reindex.BulkByScrollResponse; -import org.elasticsearch.index.reindex.DeleteByQueryRequest; import org.elasticsearch.index.reindex.ReindexRequest; -import org.elasticsearch.index.reindex.UpdateByQueryRequest; import org.elasticsearch.plugins.spi.NamedXContentProvider; import org.elasticsearch.rest.RestResponse; import org.elasticsearch.rest.RestStatus; @@ -377,42 +375,6 @@ public final BulkByScrollResponse reindex(ReindexRequest reindexRequest, Request ); } - /** - * Executes a update by query request. - * See - * Update By Query API on elastic.co - * @param updateByQueryRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return the response - */ - public final BulkByScrollResponse updateByQuery(UpdateByQueryRequest updateByQueryRequest, RequestOptions options) throws IOException { - return performRequestAndParseEntity( - updateByQueryRequest, - RequestConverters::updateByQuery, - options, - BulkByScrollResponse::fromXContent, - singleton(409) - ); - } - - /** - * Executes a delete by query request. - * See - * Delete By Query API on elastic.co - * @param deleteByQueryRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return the response - */ - public final BulkByScrollResponse deleteByQuery(DeleteByQueryRequest deleteByQueryRequest, RequestOptions options) throws IOException { - return performRequestAndParseEntity( - deleteByQueryRequest, - RequestConverters::deleteByQuery, - options, - BulkByScrollResponse::fromXContent, - singleton(409) - ); - } - /** * Get the cluster info otherwise provided when sending an HTTP request to '/' * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized @@ -1104,37 +1066,6 @@ public void onFailure(Exception exception) { }; } - final ResponseListener wrapResponseListener404sOptional( - CheckedFunction responseConverter, - ActionListener> actionListener - ) { - return new ResponseListener() { - @Override - public void onSuccess(Response response) { - try { - actionListener.onResponse(Optional.of(responseConverter.apply(response))); - } catch (Exception e) { - IOException ioe = new IOException("Unable to parse response body for " + response, e); - onFailure(ioe); - } - } - - @Override - public void onFailure(Exception exception) { - if (exception instanceof ResponseException responseException) { - Response response = responseException.getResponse(); - if (RestStatus.NOT_FOUND.getStatus() == response.getStatusLine().getStatusCode()) { - actionListener.onResponse(Optional.empty()); - } else { - actionListener.onFailure(parseResponseException(responseException)); - } - } else { - actionListener.onFailure(exception); - } - } - }; - } - /** * Converts a {@link ResponseException} obtained from the low level REST client into an {@link ElasticsearchException}. * If a response body was returned, tries to parse it as an error returned from Elasticsearch. diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/TimedRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/TimedRequest.java deleted file mode 100644 index a3135b9725eb..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/TimedRequest.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -package org.elasticsearch.client; - -import org.elasticsearch.core.TimeValue; - -import static org.elasticsearch.core.TimeValue.timeValueSeconds; - -/** - * A base request for any requests that supply timeouts. - * - * Please note, any requests that use a ackTimeout should set timeout as they - * represent the same backing field on the server. - */ -public abstract class TimedRequest implements Validatable { - - public static final TimeValue DEFAULT_ACK_TIMEOUT = timeValueSeconds(30); - public static final TimeValue DEFAULT_MASTER_NODE_TIMEOUT = TimeValue.timeValueSeconds(30); - - private TimeValue timeout = DEFAULT_ACK_TIMEOUT; - private TimeValue masterTimeout = DEFAULT_MASTER_NODE_TIMEOUT; - - /** - * Sets the timeout to wait for the all the nodes to acknowledge - * @param timeout timeout as a {@link TimeValue} - */ - public void setTimeout(TimeValue timeout) { - this.timeout = timeout; - } - - /** - * Sets the timeout to connect to the master node - * @param masterTimeout timeout as a {@link TimeValue} - */ - public void setMasterTimeout(TimeValue masterTimeout) { - this.masterTimeout = masterTimeout; - } - - /** - * Returns the request timeout - */ - public TimeValue timeout() { - return timeout; - } - - /** - * Returns the timeout for the request to be completed on the master node - */ - public TimeValue masterNodeTimeout() { - return masterTimeout; - } -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/asyncsearch/DeleteAsyncSearchRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/asyncsearch/DeleteAsyncSearchRequest.java deleted file mode 100644 index 0f7e7579e0b5..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/asyncsearch/DeleteAsyncSearchRequest.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client.asyncsearch; - -import org.elasticsearch.client.Validatable; - -import java.util.Objects; - -public class DeleteAsyncSearchRequest implements Validatable { - - private final String id; - - public DeleteAsyncSearchRequest(String id) { - this.id = id; - } - - public String getId() { - return this.id; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - DeleteAsyncSearchRequest request = (DeleteAsyncSearchRequest) o; - return Objects.equals(getId(), request.getId()); - } - - @Override - public int hashCode() { - return Objects.hash(getId()); - } -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/asyncsearch/GetAsyncSearchRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/asyncsearch/GetAsyncSearchRequest.java deleted file mode 100644 index d6c40dbe1a56..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/asyncsearch/GetAsyncSearchRequest.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client.asyncsearch; - -import org.elasticsearch.client.Validatable; -import org.elasticsearch.client.ValidationException; -import org.elasticsearch.core.TimeValue; - -import java.util.Objects; -import java.util.Optional; - -public class GetAsyncSearchRequest implements Validatable { - - private TimeValue waitForCompletion; - private TimeValue keepAlive; - - private final String id; - - public GetAsyncSearchRequest(String id) { - this.id = id; - } - - public String getId() { - return this.id; - } - - public TimeValue getWaitForCompletion() { - return waitForCompletion; - } - - public void setWaitForCompletion(TimeValue waitForCompletion) { - this.waitForCompletion = waitForCompletion; - } - - public TimeValue getKeepAlive() { - return keepAlive; - } - - public void setKeepAlive(TimeValue keepAlive) { - this.keepAlive = keepAlive; - } - - @Override - public Optional validate() { - return Optional.empty(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - GetAsyncSearchRequest request = (GetAsyncSearchRequest) o; - return Objects.equals(getId(), request.getId()) - && Objects.equals(getKeepAlive(), request.getKeepAlive()) - && Objects.equals(getWaitForCompletion(), request.getWaitForCompletion()); - } - - @Override - public int hashCode() { - return Objects.hash(getId(), getKeepAlive(), getWaitForCompletion()); - } -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/asyncsearch/SubmitAsyncSearchRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/asyncsearch/SubmitAsyncSearchRequest.java deleted file mode 100644 index 9c381645d6ff..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/asyncsearch/SubmitAsyncSearchRequest.java +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client.asyncsearch; - -import org.elasticsearch.action.search.SearchRequest; -import org.elasticsearch.action.search.SearchType; -import org.elasticsearch.action.support.IndicesOptions; -import org.elasticsearch.client.Validatable; -import org.elasticsearch.client.ValidationException; -import org.elasticsearch.core.TimeValue; -import org.elasticsearch.search.builder.SearchSourceBuilder; - -import java.util.Objects; -import java.util.Optional; - -/** - * A request to track asynchronously the progress of a search against one or more indices. - */ -public class SubmitAsyncSearchRequest implements Validatable { - - public static long MIN_KEEP_ALIVE = TimeValue.timeValueMinutes(1).millis(); - - private TimeValue waitForCompletionTimeout; - private Boolean keepOnCompletion; - private TimeValue keepAlive; - private final SearchRequest searchRequest; - // The following is optional and will only be sent down with the request if explicitely set by the user - private Integer batchedReduceSize; - - /** - * Creates a new request - */ - public SubmitAsyncSearchRequest(SearchSourceBuilder source, String... indices) { - this.searchRequest = new SearchRequest(indices, source); - } - - /** - * Get the target indices - */ - public String[] getIndices() { - return this.searchRequest.indices(); - } - - /** - * Get the minimum time that the request should wait before returning a partial result (defaults to 1 second). - */ - public TimeValue getWaitForCompletionTimeout() { - return waitForCompletionTimeout; - } - - /** - * Sets the minimum time that the request should wait before returning a partial result (defaults to 1 second). - */ - public void setWaitForCompletionTimeout(TimeValue waitForCompletionTimeout) { - this.waitForCompletionTimeout = waitForCompletionTimeout; - } - - /** - * Returns whether the resource resource should be kept on completion or failure (defaults to false). - */ - public Boolean isKeepOnCompletion() { - return keepOnCompletion; - } - - /** - * Determines if the resource should be kept on completion or failure (defaults to false). - */ - public void setKeepOnCompletion(boolean keepOnCompletion) { - this.keepOnCompletion = keepOnCompletion; - } - - /** - * Get the amount of time after which the result will expire (defaults to 5 days). - */ - public TimeValue getKeepAlive() { - return keepAlive; - } - - /** - * Sets the amount of time after which the result will expire (defaults to 5 days). - */ - public void setKeepAlive(TimeValue keepAlive) { - this.keepAlive = keepAlive; - } - - // setters for request parameters of the wrapped SearchRequest - /** - * Set the routing value to control the shards that the search will be executed on. - * A comma separated list of routing values to control the shards the search will be executed on. - */ - public void setRouting(String routing) { - this.searchRequest.routing(routing); - } - - /** - * Set the routing values to control the shards that the search will be executed on. - */ - public void setRoutings(String... routings) { - this.searchRequest.routing(routings); - } - - /** - * Get the routing value to control the shards that the search will be executed on. - */ - public String getRouting() { - return this.searchRequest.routing(); - } - - /** - * Sets the preference to execute the search. Defaults to randomize across shards. Can be set to - * {@code _local} to prefer local shards or a custom value, which guarantees that the same order - * will be used across different requests. - */ - public void setPreference(String preference) { - this.searchRequest.preference(preference); - } - - /** - * Get the preference to execute the search. - */ - public String getPreference() { - return this.searchRequest.preference(); - } - - /** - * Specifies what type of requested indices to ignore and how to deal with indices wildcard expressions. - */ - public void setIndicesOptions(IndicesOptions indicesOptions) { - this.searchRequest.indicesOptions(indicesOptions); - } - - /** - * Get the indices Options. - */ - public IndicesOptions getIndicesOptions() { - return this.searchRequest.indicesOptions(); - } - - /** - * The search type to execute, defaults to {@link SearchType#DEFAULT}. - */ - public void setSearchType(SearchType searchType) { - this.searchRequest.searchType(searchType); - } - - /** - * Get the search type to execute, defaults to {@link SearchType#DEFAULT}. - */ - public SearchType getSearchType() { - return this.searchRequest.searchType(); - } - - /** - * Sets if this request should allow partial results. (If method is not called, - * will default to the cluster level setting). - */ - public void setAllowPartialSearchResults(boolean allowPartialSearchResults) { - this.searchRequest.allowPartialSearchResults(allowPartialSearchResults); - } - - /** - * Gets if this request should allow partial results. - */ - public Boolean getAllowPartialSearchResults() { - return this.searchRequest.allowPartialSearchResults(); - } - - /** - * Optional. Sets the number of shard results that should be reduced at once on the coordinating node. - * This value should be used as a protection mechanism to reduce the memory overhead per search - * request if the potential number of shards in the request can be large. Defaults to 5. - */ - public void setBatchedReduceSize(int batchedReduceSize) { - this.batchedReduceSize = batchedReduceSize; - } - - /** - * Gets the number of shard results that should be reduced at once on the coordinating node. - * Returns {@code null} if unset. - */ - public Integer getBatchedReduceSize() { - return this.batchedReduceSize; - } - - /** - * Sets if this request should use the request cache or not, assuming that it can (for - * example, if "now" is used, it will never be cached). - * By default (if not set) this is turned on for {@link SubmitAsyncSearchRequest}. - */ - public void setRequestCache(Boolean requestCache) { - this.searchRequest.requestCache(requestCache); - } - - /** - * Gets if this request should use the request cache or not, if set. - * This defaults to `true` on the server side if unset in the client. - */ - public Boolean getRequestCache() { - return this.searchRequest.requestCache(); - } - - /** - * Returns the number of shard requests that should be executed concurrently on a single node. - * The default is {@code 5}. - */ - public int getMaxConcurrentShardRequests() { - return this.searchRequest.getMaxConcurrentShardRequests(); - } - - /** - * Sets the number of shard requests that should be executed concurrently on a single node. - * The default is {@code 5}. - */ - public void setMaxConcurrentShardRequests(int maxConcurrentShardRequests) { - this.searchRequest.setMaxConcurrentShardRequests(maxConcurrentShardRequests); - } - - /** - * Gets if the source of the {@link SearchSourceBuilder} initially used on this request. - */ - public SearchSourceBuilder getSearchSource() { - return this.searchRequest.source(); - } - - @Override - public Optional validate() { - final ValidationException validationException = new ValidationException(); - if (searchRequest.isSuggestOnly()) { - validationException.addValidationError("suggest-only queries are not supported"); - } - if (keepAlive != null && keepAlive.getMillis() < MIN_KEEP_ALIVE) { - validationException.addValidationError("[keep_alive] must be greater than 1 minute, got: " + keepAlive.toString()); - } - if (validationException.validationErrors().isEmpty()) { - return Optional.empty(); - } - return Optional.of(validationException); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - SubmitAsyncSearchRequest request = (SubmitAsyncSearchRequest) o; - return Objects.equals(searchRequest, request.searchRequest) - && Objects.equals(getKeepAlive(), request.getKeepAlive()) - && Objects.equals(getWaitForCompletionTimeout(), request.getWaitForCompletionTimeout()) - && Objects.equals(isKeepOnCompletion(), request.isKeepOnCompletion()); - } - - @Override - public int hashCode() { - return Objects.hash(searchRequest, getKeepAlive(), getWaitForCompletionTimeout(), isKeepOnCompletion()); - } -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoRequest.java deleted file mode 100644 index 7672c1054c04..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -package org.elasticsearch.client.cluster; - -import org.elasticsearch.client.Validatable; - -/** - * The request object used by the Remote cluster info API. - */ -public final class RemoteInfoRequest implements Validatable { - -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoResponse.java deleted file mode 100644 index 9aff43bf3871..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteInfoResponse.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -package org.elasticsearch.client.cluster; - -import org.elasticsearch.xcontent.XContentParser; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; - -/** - * A response to _remote/info API request. - */ -public final class RemoteInfoResponse { - - private List infos; - - RemoteInfoResponse(Collection infos) { - this.infos = List.copyOf(infos); - } - - public List getInfos() { - return infos; - } - - public static RemoteInfoResponse fromXContent(XContentParser parser) throws IOException { - ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); - - List infos = new ArrayList<>(); - - XContentParser.Token token; - while ((token = parser.nextToken()) == XContentParser.Token.FIELD_NAME) { - String clusterAlias = parser.currentName(); - RemoteConnectionInfo info = RemoteConnectionInfo.fromXContent(parser, clusterAlias); - infos.add(info); - } - ensureExpectedToken(XContentParser.Token.END_OBJECT, token, parser); - return new RemoteInfoResponse(infos); - } -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/IndexerJobStats.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/IndexerJobStats.java deleted file mode 100644 index e404f254e17a..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/IndexerJobStats.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client.core; - -import org.elasticsearch.xcontent.ParseField; - -import java.util.Objects; - -public abstract class IndexerJobStats { - public static ParseField NUM_PAGES = new ParseField("pages_processed"); - public static ParseField NUM_INPUT_DOCUMENTS = new ParseField("documents_processed"); - public static ParseField NUM_OUTPUT_DOCUMENTS = new ParseField("documents_indexed"); - public static ParseField NUM_INVOCATIONS = new ParseField("trigger_count"); - public static ParseField INDEX_TIME_IN_MS = new ParseField("index_time_in_ms"); - public static ParseField SEARCH_TIME_IN_MS = new ParseField("search_time_in_ms"); - public static ParseField PROCESSING_TIME_IN_MS = new ParseField("processing_time_in_ms"); - public static ParseField INDEX_TOTAL = new ParseField("index_total"); - public static ParseField SEARCH_TOTAL = new ParseField("search_total"); - public static ParseField PROCESSING_TOTAL = new ParseField("processing_total"); - public static ParseField SEARCH_FAILURES = new ParseField("search_failures"); - public static ParseField INDEX_FAILURES = new ParseField("index_failures"); - - protected final long numPages; - protected final long numInputDocuments; - protected final long numOuputDocuments; - protected final long numInvocations; - protected final long indexTime; - protected final long indexTotal; - protected final long searchTime; - protected final long searchTotal; - protected final long processingTime; - protected final long processingTotal; - protected final long indexFailures; - protected final long searchFailures; - - public IndexerJobStats( - long numPages, - long numInputDocuments, - long numOutputDocuments, - long numInvocations, - long indexTime, - long searchTime, - long processingTime, - long indexTotal, - long searchTotal, - long processingTotal, - long indexFailures, - long searchFailures - ) { - this.numPages = numPages; - this.numInputDocuments = numInputDocuments; - this.numOuputDocuments = numOutputDocuments; - this.numInvocations = numInvocations; - this.indexTime = indexTime; - this.indexTotal = indexTotal; - this.searchTime = searchTime; - this.searchTotal = searchTotal; - this.processingTime = processingTime; - this.processingTotal = processingTotal; - this.indexFailures = indexFailures; - this.searchFailures = searchFailures; - } - - /** - * The number of pages read from the input indices - */ - public long getNumPages() { - return numPages; - } - - /** - * The number of documents read from the input indices - */ - public long getNumDocuments() { - return numInputDocuments; - } - - /** - * Number of times that the job woke up to write documents - */ - public long getNumInvocations() { - return numInvocations; - } - - /** - * Number of documents written - */ - public long getOutputDocuments() { - return numOuputDocuments; - } - - /** - * Number of index failures that have occurred - */ - public long getIndexFailures() { - return indexFailures; - } - - /** - * Number of failures that have occurred - */ - public long getSearchFailures() { - return searchFailures; - } - - /** - * Returns the time spent indexing (cumulative) in milliseconds - */ - public long getIndexTime() { - return indexTime; - } - - /** - * Returns the time spent searching (cumulative) in milliseconds - */ - public long getSearchTime() { - return searchTime; - } - - /** - * Returns the time spent processing (cumulative) in milliseconds - */ - public long getProcessingTime() { - return processingTime; - } - - /** - * Returns the total number of indexing requests that have been processed - * (Note: this is not the number of _documents_ that have been indexed) - */ - public long getIndexTotal() { - return indexTotal; - } - - /** - * Returns the total number of search requests that have been made - */ - public long getSearchTotal() { - return searchTotal; - } - - /** - * Returns the total number of processing runs that have been made - */ - public long getProcessingTotal() { - return processingTotal; - } - - @Override - public boolean equals(Object other) { - if (this == other) { - return true; - } - - if (other instanceof IndexerJobStats == false) { - return false; - } - - IndexerJobStats that = (IndexerJobStats) other; - return Objects.equals(this.numPages, that.numPages) - && Objects.equals(this.numInputDocuments, that.numInputDocuments) - && Objects.equals(this.numOuputDocuments, that.numOuputDocuments) - && Objects.equals(this.numInvocations, that.numInvocations) - && Objects.equals(this.indexTime, that.indexTime) - && Objects.equals(this.searchTime, that.searchTime) - && Objects.equals(this.processingTime, that.processingTime) - && Objects.equals(this.indexFailures, that.indexFailures) - && Objects.equals(this.searchFailures, that.searchFailures) - && Objects.equals(this.searchTotal, that.searchTotal) - && Objects.equals(this.processingTotal, that.processingTotal) - && Objects.equals(this.indexTotal, that.indexTotal); - } - - @Override - public int hashCode() { - return Objects.hash( - numPages, - numInputDocuments, - numOuputDocuments, - numInvocations, - indexTime, - searchTime, - processingTime, - indexFailures, - searchFailures, - searchTotal, - indexTotal, - processingTotal - ); - } - - @Override - public final String toString() { - return "{pages=" - + numPages - + ", input_docs=" - + numInputDocuments - + ", output_docs=" - + numOuputDocuments - + ", invocations=" - + numInvocations - + ", index_failures=" - + indexFailures - + ", search_failures=" - + searchFailures - + ", index_time_in_ms=" - + indexTime - + ", index_total=" - + indexTotal - + ", search_time_in_ms=" - + searchTime - + ", search_total=" - + searchTotal - + ", processing_time_in_ms=" - + processingTime - + ", processing_total=" - + processingTotal - + "}"; - } -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/license/LicenseStatus.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/license/LicenseStatus.java deleted file mode 100644 index 3038b702c84e..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/license/LicenseStatus.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -package org.elasticsearch.client.license; - -/** - * Status of an X-Pack license. - */ -public enum LicenseStatus { - - ACTIVE("active"), - INVALID("invalid"), - EXPIRED("expired"); - - private final String label; - - LicenseStatus(String label) { - this.label = label; - } - - public String label() { - return label; - } - - public static LicenseStatus fromString(String value) { - return switch (value) { - case "active" -> ACTIVE; - case "invalid" -> INVALID; - case "expired" -> EXPIRED; - default -> throw new IllegalArgumentException("unknown license status [" + value + "]"); - }; - } -} From 2b472c919790dc60ba0f64a957cf97b041759901 Mon Sep 17 00:00:00 2001 From: Daniel Mitterdorfer Date: Wed, 30 Nov 2022 17:56:52 +0100 Subject: [PATCH 121/919] Add profiling plugin (#91640) With this commit we add a new plugin that retrieves data for Universal Profiler. This functionality is currently implemented in Kibana in the function `getExecutablesAndStackTraces` which we replicate here for efficiency purposes. --- docs/changelog/91640.yaml | 5 + x-pack/plugin/profiler/build.gradle | 15 + .../xpack/profiler/GetProfilingActionIT.java | 315 ++++++++++++++++ .../internalClusterTest/resources/events.json | 78 ++++ .../resources/executables.json | 32 ++ .../resources/stackframes.json | 41 +++ .../resources/stacktraces.json | 38 ++ .../xpack/profiler/EventsIndex.java | 95 +++++ .../xpack/profiler/GetProfilingAction.java | 18 + .../xpack/profiler/GetProfilingRequest.java | 183 ++++++++++ .../xpack/profiler/GetProfilingResponse.java | 222 ++++++++++++ .../xpack/profiler/ProfilingPlugin.java | 148 ++++++++ .../profiler/RestGetProfilingAction.java | 43 +++ .../xpack/profiler/StackFrame.java | 75 ++++ .../xpack/profiler/StackTrace.java | 231 ++++++++++++ .../profiler/TransportGetProfilingAction.java | 338 ++++++++++++++++++ .../xpack/profiler/EventsIndexTests.java | 48 +++ .../profiler/GetProfilingRequestTests.java | 102 ++++++ .../profiler/GetProfilingResponseTests.java | 62 ++++ .../profiler/RestGetProfilingActionTests.java | 108 ++++++ .../xpack/profiler/StackFrameTests.java | 71 ++++ .../xpack/profiler/StackTraceTests.java | 115 ++++++ .../xpack/security/operator/Constants.java | 1 + 23 files changed, 2384 insertions(+) create mode 100644 docs/changelog/91640.yaml create mode 100644 x-pack/plugin/profiler/build.gradle create mode 100644 x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/GetProfilingActionIT.java create mode 100644 x-pack/plugin/profiler/src/internalClusterTest/resources/events.json create mode 100644 x-pack/plugin/profiler/src/internalClusterTest/resources/executables.json create mode 100644 x-pack/plugin/profiler/src/internalClusterTest/resources/stackframes.json create mode 100644 x-pack/plugin/profiler/src/internalClusterTest/resources/stacktraces.json create mode 100644 x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/EventsIndex.java create mode 100644 x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/GetProfilingAction.java create mode 100644 x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/GetProfilingRequest.java create mode 100644 x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/GetProfilingResponse.java create mode 100644 x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/ProfilingPlugin.java create mode 100644 x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/RestGetProfilingAction.java create mode 100644 x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/StackFrame.java create mode 100644 x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/StackTrace.java create mode 100644 x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/TransportGetProfilingAction.java create mode 100644 x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/EventsIndexTests.java create mode 100644 x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/GetProfilingRequestTests.java create mode 100644 x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/GetProfilingResponseTests.java create mode 100644 x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/RestGetProfilingActionTests.java create mode 100644 x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/StackFrameTests.java create mode 100644 x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/StackTraceTests.java diff --git a/docs/changelog/91640.yaml b/docs/changelog/91640.yaml new file mode 100644 index 000000000000..e581b03d523c --- /dev/null +++ b/docs/changelog/91640.yaml @@ -0,0 +1,5 @@ +pr: 91640 +summary: Add profiling plugin +area: Search +type: enhancement +issues: [] diff --git a/x-pack/plugin/profiler/build.gradle b/x-pack/plugin/profiler/build.gradle new file mode 100644 index 000000000000..80624ec9a181 --- /dev/null +++ b/x-pack/plugin/profiler/build.gradle @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +apply plugin: 'elasticsearch.internal-cluster-test' +apply plugin: 'elasticsearch.internal-es-plugin' + +esplugin { + name 'x-pack-profiling' + description 'The profiler plugin adds support for retrieving data from Universal Profiler.' + classname 'org.elasticsearch.xpack.profiler.ProfilingPlugin' +} diff --git a/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/GetProfilingActionIT.java b/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/GetProfilingActionIT.java new file mode 100644 index 000000000000..0c6038186c91 --- /dev/null +++ b/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/GetProfilingActionIT.java @@ -0,0 +1,315 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.profiler; + +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.logging.log4j.LogManager; +import org.apache.lucene.util.SetOnce; +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; +import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; +import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse; +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.client.Cancellable; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.common.network.NetworkModule; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.plugins.PluginsService; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.script.MockScriptPlugin; +import org.elasticsearch.search.lookup.LeafStoredFieldsLookup; +import org.elasticsearch.tasks.CancellableTask; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.tasks.TaskId; +import org.elasticsearch.tasks.TaskInfo; +import org.elasticsearch.tasks.TaskManager; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.transport.netty4.Netty4Plugin; +import org.elasticsearch.xcontent.XContentType; +import org.junit.Before; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CancellationException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +import static org.elasticsearch.action.support.ActionTestUtils.wrapAsRestResponseListener; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.instanceOf; + +@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 1) +public class GetProfilingActionIT extends ESIntegTestCase { + @Override + protected Collection> nodePlugins() { + return List.of(ProfilingPlugin.class, ScriptedBlockPlugin.class, getTestTransportPlugin()); + } + + @Override + protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) { + return Settings.builder() + .put(super.nodeSettings(nodeOrdinal, otherSettings)) + .put(ProfilingPlugin.PROFILING_ENABLED.getKey(), true) + .put(NetworkModule.TRANSPORT_TYPE_KEY, Netty4Plugin.NETTY_TRANSPORT_NAME) + .put(NetworkModule.HTTP_TYPE_KEY, Netty4Plugin.NETTY_HTTP_TRANSPORT_NAME) + .build(); + } + + @Override + protected boolean addMockHttpTransport() { + return false; // enable http + } + + @Override + protected boolean ignoreExternalCluster() { + return true; + } + + private byte[] read(String resource) throws IOException { + return GetProfilingAction.class.getClassLoader().getResourceAsStream(resource).readAllBytes(); + } + + private void createIndex(String name, String bodyFileName) throws Exception { + client().admin().indices().prepareCreate(name).setSource(read(bodyFileName), XContentType.JSON).execute().get(); + } + + private void indexDoc(String index, String id, Map source) { + IndexResponse indexResponse = client().prepareIndex(index).setId(id).setSource(source).get(); + assertEquals(RestStatus.CREATED, indexResponse.status()); + } + + @Before + public void setupData() throws Exception { + + for (String idx : EventsIndex.indexNames()) { + createIndex(idx, "events.json"); + } + createIndex("profiling-stackframes", "stackframes.json"); + createIndex("profiling-stacktraces", "stacktraces.json"); + createIndex("profiling-executables", "executables.json"); + ensureGreen(); + + // ensure that we have this in every index, so we find an event + for (String idx : EventsIndex.indexNames()) { + indexDoc( + idx, + "QjoLteG7HX3VUUXr-J4kHQ", + Map.of("@timestamp", 1668761065, "Stacktrace.id", "QjoLteG7HX3VUUXr-J4kHQ", "Stacktrace.count", 1) + ); + } + + indexDoc( + "profiling-stacktraces", + "QjoLteG7HX3VUUXr-J4kHQ", + Map.of("Stacktrace.frame.ids", "QCCDqjSg3bMK1C4YRK6TiwAAAAAAEIpf", "Stacktrace.frame.types", "AQI") + ); + indexDoc( + "profiling-stackframes", + "QCCDqjSg3bMK1C4YRK6TiwAAAAAAEIpf", + Map.of("Stackframe.function.name", "_raw_spin_unlock_irqrestore") + ); + indexDoc("profiling-executables", "QCCDqjSg3bMK1C4YRK6Tiw", Map.of("Executable.file.name", "libc.so.6")); + + refresh(); + } + + public void testGetProfilingDataUnfiltered() throws Exception { + GetProfilingRequest request = new GetProfilingRequest(1, null); + GetProfilingResponse response = client().execute(GetProfilingAction.INSTANCE, request).get(); + assertEquals(RestStatus.OK, response.status()); + assertEquals(1, response.getTotalFrames()); + assertNotNull(response.getStackTraces()); + StackTrace stackTrace = response.getStackTraces().get("QjoLteG7HX3VUUXr-J4kHQ"); + assertArrayEquals(new int[] { 1083999 }, stackTrace.addressOrLines); + assertArrayEquals(new String[] { "QCCDqjSg3bMK1C4YRK6Tiw" }, stackTrace.fileIds); + assertArrayEquals(new String[] { "QCCDqjSg3bMK1C4YRK6TiwAAAAAAEIpf" }, stackTrace.frameIds); + assertArrayEquals(new int[] { 2 }, stackTrace.typeIds); + + assertNotNull(response.getStackFrames()); + StackFrame stackFrame = response.getStackFrames().get("QCCDqjSg3bMK1C4YRK6TiwAAAAAAEIpf"); + assertEquals("_raw_spin_unlock_irqrestore", stackFrame.functionName); + assertNotNull(response.getStackTraceEvents()); + assertEquals(1, (int) response.getStackTraceEvents().get("QjoLteG7HX3VUUXr-J4kHQ")); + + assertNotNull(response.getExecutables()); + assertNotNull("libc.so.6", response.getExecutables().get("QCCDqjSg3bMK1C4YRK6Tiw")); + } + + public void testAutomaticCancellation() throws Exception { + Request restRequest = new Request("POST", "/_profiling/stacktraces"); + restRequest.setEntity(new StringEntity(""" + { + "sample_size": 10000, + "query": { + "bool": { + "filter": [ + { + "script": { + "script": { + "lang": "mockscript", + "source": "search_block", + "params": {} + } + } + } + ] + } + } + } + """, ContentType.APPLICATION_JSON.withCharset(StandardCharsets.UTF_8))); + verifyCancellation(GetProfilingAction.NAME, restRequest); + } + + void verifyCancellation(String action, Request restRequest) throws Exception { + Map nodeIdToName = readNodesInfo(); + List plugins = initBlockFactory(); + + PlainActionFuture future = PlainActionFuture.newFuture(); + Cancellable cancellable = getRestClient().performRequestAsync(restRequest, wrapAsRestResponseListener(future)); + + awaitForBlock(plugins); + Collection profilingTasks = collectProfilingRelatedTasks(action); + cancellable.cancel(); + ensureTasksAreCancelled(profilingTasks, nodeIdToName::get); + + disableBlocks(plugins); + expectThrows(CancellationException.class, future::actionGet); + } + + private static Map readNodesInfo() { + Map nodeIdToName = new HashMap<>(); + NodesInfoResponse nodesInfoResponse = client().admin().cluster().prepareNodesInfo().get(); + assertFalse(nodesInfoResponse.hasFailures()); + for (NodeInfo node : nodesInfoResponse.getNodes()) { + nodeIdToName.put(node.getNode().getId(), node.getNode().getName()); + } + return nodeIdToName; + } + + private static Collection collectProfilingRelatedTasks(String transportAction) { + SetOnce profilingTask = new SetOnce<>(); + Map> taskToParent = new HashMap<>(); + ListTasksResponse listTasksResponse = client().admin().cluster().prepareListTasks().get(); + for (TaskInfo task : listTasksResponse.getTasks()) { + TaskId parentTaskId = task.parentTaskId(); + if (parentTaskId != null) { + if (taskToParent.containsKey(parentTaskId) == false) { + taskToParent.put(parentTaskId, new HashSet<>()); + } + taskToParent.get(parentTaskId).add(task.taskId()); + } + if (task.action().equals(transportAction)) { + profilingTask.set(task); + } + } + assertNotNull(profilingTask.get()); + return taskToParent.get(profilingTask.get().taskId()); + } + + private static void ensureTasksAreCancelled(Collection taskIds, Function nodeIdToName) throws Exception { + assertBusy(() -> { + for (TaskId taskId : taskIds) { + String nodeName = nodeIdToName.apply(taskId.getNodeId()); + TaskManager taskManager = internalCluster().getInstance(TransportService.class, nodeName).getTaskManager(); + Task task = taskManager.getTask(taskId.getId()); + assertThat(task, instanceOf(CancellableTask.class)); + assertTrue(((CancellableTask) task).isCancelled()); + } + }); + } + + private static List initBlockFactory() { + List plugins = new ArrayList<>(); + for (PluginsService pluginsService : internalCluster().getDataNodeInstances(PluginsService.class)) { + plugins.addAll(pluginsService.filterPlugins(ScriptedBlockPlugin.class)); + } + for (ScriptedBlockPlugin plugin : plugins) { + plugin.reset(); + plugin.enableBlock(); + // Allow to execute one search and only block starting with the second one. This + // is done so we have at least one child action and can check that all active children + // are cancelled with the parent action. + plugin.setSlack(1); + } + return plugins; + } + + private void awaitForBlock(List plugins) throws Exception { + assertBusy(() -> { + int numberOfBlockedPlugins = 0; + for (ScriptedBlockPlugin plugin : plugins) { + numberOfBlockedPlugins += plugin.hits.get(); + } + logger.info("The plugin blocked on {} shards", numberOfBlockedPlugins); + assertThat(numberOfBlockedPlugins, greaterThan(0)); + }, 10, TimeUnit.SECONDS); + } + + private static void disableBlocks(List plugins) { + for (ScriptedBlockPlugin plugin : plugins) { + plugin.disableBlock(); + } + } + + public static class ScriptedBlockPlugin extends MockScriptPlugin { + static final String SCRIPT_NAME = "search_block"; + + private final AtomicInteger hits = new AtomicInteger(); + + private final AtomicInteger slack = new AtomicInteger(0); + + private final AtomicBoolean shouldBlock = new AtomicBoolean(true); + + void reset() { + hits.set(0); + } + + void disableBlock() { + shouldBlock.set(false); + } + + void enableBlock() { + shouldBlock.set(true); + } + + void setSlack(int slack) { + this.slack.set(slack); + } + + @Override + public Map, Object>> pluginScripts() { + return Collections.singletonMap(SCRIPT_NAME, params -> { + LeafStoredFieldsLookup fieldsLookup = (LeafStoredFieldsLookup) params.get("_fields"); + LogManager.getLogger(GetProfilingActionIT.class).info("Blocking on the document {}", fieldsLookup.get("_id")); + hits.incrementAndGet(); + if (slack.decrementAndGet() < 0) { + try { + waitUntil(() -> shouldBlock.get() == false); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return true; + }); + } + } +} diff --git a/x-pack/plugin/profiler/src/internalClusterTest/resources/events.json b/x-pack/plugin/profiler/src/internalClusterTest/resources/events.json new file mode 100644 index 000000000000..855c754191db --- /dev/null +++ b/x-pack/plugin/profiler/src/internalClusterTest/resources/events.json @@ -0,0 +1,78 @@ +{ + "settings": { + "index": { + "number_of_shards": "4", + "max_result_window": 150000, + "refresh_interval": "10s", + "sort": { + "field": [ + "service.name", + "@timestamp", + "orchestrator.resource.name", + "container.name", + "process.thread.name", + "host.id" + ] + } + }, + "codec": "best_compression" + }, + "mappings": { + "_doc": { + "_source": { + "enabled": false + }, + "properties": { + "ecs.version": { + "type": "keyword", + "index": true + }, + "service.name": { + "type": "keyword" + }, + "@timestamp": { + "type": "date", + "format": "epoch_second" + }, + "host.id": { + "type": "keyword" + }, + "Stacktrace.id": { + "type": "keyword", + "index": false + }, + "orchestrator.resource.name": { + "type": "keyword" + }, + "container.name": { + "type": "keyword" + }, + "process.thread.name": { + "type": "keyword" + }, + "Stacktrace.count": { + "type": "short", + "index": false + }, + "agent.version": { + "type": "keyword" + }, + "host.ip": { + "type": "ip" + }, + "host.ipstring": { + "type": "keyword" + }, + "host.name": { + "type": "keyword" + }, + "os.kernel": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + } + } + } + } +} diff --git a/x-pack/plugin/profiler/src/internalClusterTest/resources/executables.json b/x-pack/plugin/profiler/src/internalClusterTest/resources/executables.json new file mode 100644 index 000000000000..f5db60318bcf --- /dev/null +++ b/x-pack/plugin/profiler/src/internalClusterTest/resources/executables.json @@ -0,0 +1,32 @@ +{ + "settings": { + "index": { + "refresh_interval": "10s" + } + }, + "mappings": { + "_doc": { + "_source": { + "mode": "synthetic" + }, + "properties": { + "ecs.version": { + "type": "keyword", + "index": true + }, + "Executable.build.id": { + "type": "keyword", + "index": true + }, + "Executable.file.name": { + "type": "keyword", + "index": true + }, + "@timestamp": { + "type": "date", + "format": "epoch_second" + } + } + } + } +} diff --git a/x-pack/plugin/profiler/src/internalClusterTest/resources/stackframes.json b/x-pack/plugin/profiler/src/internalClusterTest/resources/stackframes.json new file mode 100644 index 000000000000..179f57214267 --- /dev/null +++ b/x-pack/plugin/profiler/src/internalClusterTest/resources/stackframes.json @@ -0,0 +1,41 @@ +{ + "settings": { + "index": { + "number_of_shards": "16", + "refresh_interval": "10s" + } + }, + "mappings": { + "_doc": { + "_source": { + "mode": "synthetic" + }, + "properties": { + "ecs.version": { + "type": "keyword", + "index": true + }, + "Stackframe.line.number": { + "type": "integer", + "index": false + }, + "Stackframe.file.name": { + "type": "keyword", + "index": false + }, + "Stackframe.source.type": { + "type": "short", + "index": false + }, + "Stackframe.function.name": { + "type": "keyword", + "index": false + }, + "Stackframe.function.offset": { + "type": "integer", + "index": false + } + } + } + } +} diff --git a/x-pack/plugin/profiler/src/internalClusterTest/resources/stacktraces.json b/x-pack/plugin/profiler/src/internalClusterTest/resources/stacktraces.json new file mode 100644 index 000000000000..0b72dccdfe5c --- /dev/null +++ b/x-pack/plugin/profiler/src/internalClusterTest/resources/stacktraces.json @@ -0,0 +1,38 @@ +{ + "settings": { + "index": { + "number_of_shards": "16", + "refresh_interval": "10s", + "sort": { + "field": [ + "Stacktrace.frame.ids" + ] + } + } + }, + "mappings": { + "_doc": { + "_source": { + "mode": "synthetic" + }, + "properties": { + "ecs.version": { + "type": "keyword", + "index": true + }, + "Stacktrace.frame.ids": { + "type": "keyword", + "index": false + }, + "Stacktrace.frame.types": { + "type": "keyword", + "index": false + }, + "@timestamp": { + "type": "date", + "format": "epoch_second" + } + } + } + } +} diff --git a/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/EventsIndex.java b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/EventsIndex.java new file mode 100644 index 000000000000..441a5bbd6cd2 --- /dev/null +++ b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/EventsIndex.java @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.profiler; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +public final class EventsIndex { + private static final String PREFIX = "profiling-events"; + private static final String ALL_EVENTS = PREFIX + "-all"; + + private static final int SAMPLING_FACTOR = 5; + + private static final int MIN_EXPONENT = 1; + + private static final int MAX_EXPONENT = 11; + + public static final EventsIndex FULL_INDEX = new EventsIndex(ALL_EVENTS, 1, 1); + + // Start with counting the results in the index down-sampled by 5^6. + // That is in the middle of our down-sampled indexes. + public static final EventsIndex MEDIUM_DOWNSAMPLED = fromFactorAndExponent(SAMPLING_FACTOR, 6); + + private final String name; + + private final int samplingFactor; + + private final int exponent; + + private EventsIndex(String name, int samplingFactor, int exponent) { + this.name = name; + this.samplingFactor = samplingFactor; + this.exponent = exponent; + } + + public String getName() { + return name; + } + + public int getExponent() { + return exponent; + } + + public double getSampleRate() { + return Math.pow(1.0d / samplingFactor, exponent); + } + + public EventsIndex getResampledIndex(long targetSampleSize, long currentSampleSize) { + return EventsIndex.getSampledIndex(targetSampleSize, currentSampleSize, this.getExponent()); + } + + // Return the index that has between targetSampleSize..targetSampleSize*samplingFactor entries. + // The starting point is the number of entries from the profiling-events-5pow index. + private static EventsIndex getSampledIndex(long targetSampleSize, long sampleCountFromInitialExp, int initialExp) { + if (sampleCountFromInitialExp == 0) { + return FULL_INDEX; + } + int exp = initialExp - (int) Math.round( + Math.log((targetSampleSize * SAMPLING_FACTOR) / (double) sampleCountFromInitialExp) / Math.log(SAMPLING_FACTOR) + ) + 1; + + if (exp < MIN_EXPONENT) { + return FULL_INDEX; + } + if (exp > MAX_EXPONENT) { + exp = MAX_EXPONENT; + } + return fromFactorAndExponent(SAMPLING_FACTOR, exp); + } + + private static EventsIndex fromFactorAndExponent(int factor, int exp) { + return new EventsIndex(indexName(factor, exp), factor, exp); + } + + private static String indexName(int factor, int exp) { + return String.format(Locale.ROOT, "%s-%dpow%02d", PREFIX, factor, exp); + } + + public static Collection indexNames() { + Set names = new HashSet<>(); + names.add(EventsIndex.ALL_EVENTS); + for (int exp = MIN_EXPONENT; exp <= MAX_EXPONENT; exp++) { + names.add(indexName(SAMPLING_FACTOR, exp)); + } + return Collections.unmodifiableSet(names); + } +} diff --git a/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/GetProfilingAction.java b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/GetProfilingAction.java new file mode 100644 index 000000000000..eaec2e4b7739 --- /dev/null +++ b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/GetProfilingAction.java @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.profiler; + +import org.elasticsearch.action.ActionType; + +public final class GetProfilingAction extends ActionType { + public static final GetProfilingAction INSTANCE = new GetProfilingAction(); + public static final String NAME = "indices:data/read/profiling"; + + private GetProfilingAction() { + super(NAME, GetProfilingResponse::new); + } +} diff --git a/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/GetProfilingRequest.java b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/GetProfilingRequest.java new file mode 100644 index 000000000000..13c3b2e38ea4 --- /dev/null +++ b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/GetProfilingRequest.java @@ -0,0 +1,183 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.profiler; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.tasks.CancellableTask; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.tasks.TaskId; +import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import static org.elasticsearch.action.ValidateActions.addValidationError; +import static org.elasticsearch.index.query.AbstractQueryBuilder.parseTopLevelQuery; + +/** + * A request to get profiling details + */ +public class GetProfilingRequest extends ActionRequest implements IndicesRequest { + public static final ParseField QUERY_FIELD = new ParseField("query"); + public static final ParseField SAMPLE_SIZE_FIELD = new ParseField("sample_size"); + + private QueryBuilder query; + + private Integer sampleSize; + + public GetProfilingRequest() { + this(null, null); + } + + public GetProfilingRequest(Integer sampleSize, QueryBuilder query) { + this.sampleSize = sampleSize; + this.query = query; + } + + public GetProfilingRequest(StreamInput in) throws IOException { + this.query = in.readOptionalNamedWriteable(QueryBuilder.class); + this.sampleSize = in.readOptionalInt(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeOptionalNamedWriteable(query); + out.writeOptionalInt(sampleSize); + } + + public Integer getSampleSize() { + return sampleSize; + } + + public QueryBuilder getQuery() { + return query; + } + + public void parseXContent(XContentParser parser) throws IOException { + XContentParser.Token token = parser.currentToken(); + String currentFieldName = null; + if (token != XContentParser.Token.START_OBJECT && (token = parser.nextToken()) != XContentParser.Token.START_OBJECT) { + throw new ParsingException( + parser.getTokenLocation(), + "Expected [" + XContentParser.Token.START_OBJECT + "] but found [" + token + "]", + parser.getTokenLocation() + ); + } + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token.isValue()) { + if (SAMPLE_SIZE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + this.sampleSize = parser.intValue(); + } else { + throw new ParsingException( + parser.getTokenLocation(), + "Unknown key for a " + token + " in [" + currentFieldName + "].", + parser.getTokenLocation() + ); + } + } else if (token == XContentParser.Token.START_OBJECT) { + if (QUERY_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + this.query = parseTopLevelQuery(parser); + } + } else { + throw new ParsingException( + parser.getTokenLocation(), + "Unknown key for a " + token + " in [" + currentFieldName + "].", + parser.getTokenLocation() + ); + } + } + + token = parser.nextToken(); + if (token != null) { + throw new ParsingException(parser.getTokenLocation(), "Unexpected token [" + token + "] found after the main object."); + } + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (sampleSize == null) { + validationException = addValidationError("[" + SAMPLE_SIZE_FIELD.getPreferredName() + "] is mandatory", validationException); + } else if (sampleSize <= 0) { + validationException = addValidationError( + "[" + SAMPLE_SIZE_FIELD.getPreferredName() + "] must be greater or equals than 1, got: " + sampleSize, + validationException + ); + } + return validationException; + } + + @Override + public Task createTask(long id, String type, String action, TaskId parentTaskId, Map headers) { + return new CancellableTask(id, type, action, null, parentTaskId, headers) { + @Override + public String getDescription() { + // generating description lazily since the query could be large + StringBuilder sb = new StringBuilder(); + sb.append("sample_size[").append(sampleSize).append("]"); + if (query == null) { + sb.append(", query[]"); + } else { + sb.append(", query[").append(Strings.toString(query)).append("]"); + } + return sb.toString(); + } + }; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GetProfilingRequest that = (GetProfilingRequest) o; + return Objects.equals(query, that.query) && Objects.equals(sampleSize, that.sampleSize); + } + + @Override + public int hashCode() { + return Objects.hash(query, sampleSize); + } + + @Override + public String[] indices() { + Set indices = new HashSet<>(); + indices.add("profiling-stacktraces"); + indices.add("profiling-stackframes"); + indices.add("profiling-executables"); + indices.addAll(EventsIndex.indexNames()); + return indices.toArray(new String[0]); + } + + @Override + public IndicesOptions indicesOptions() { + return IndicesOptions.STRICT_EXPAND_OPEN; + } + + @Override + public boolean includeDataStreams() { + return true; + } +} diff --git a/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/GetProfilingResponse.java b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/GetProfilingResponse.java new file mode 100644 index 000000000000..46f0f0604915 --- /dev/null +++ b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/GetProfilingResponse.java @@ -0,0 +1,222 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.profiler; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.StatusToXContentObject; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; + +import static org.elasticsearch.rest.RestStatus.OK; + +public class GetProfilingResponse extends ActionResponse implements StatusToXContentObject { + @Nullable + private final Map stackTraces; + @Nullable + private final Map stackFrames; + @Nullable + private final Map executables; + @Nullable + private final Map stackTraceEvents; + private final int totalFrames; + @Nullable + private final Exception error; + + public GetProfilingResponse(StreamInput in) throws IOException { + this.stackTraces = in.readBoolean() + ? in.readMap( + StreamInput::readString, + i -> new StackTrace(i.readIntArray(), i.readStringArray(), i.readStringArray(), i.readIntArray()) + ) + : null; + this.stackFrames = in.readBoolean() + ? in.readMap( + StreamInput::readString, + i -> new StackFrame( + i.readOptionalString(), + i.readOptionalString(), + i.readOptionalInt(), + i.readOptionalInt(), + i.readOptionalInt() + ) + ) + : null; + this.executables = in.readBoolean() ? in.readMap(StreamInput::readString, StreamInput::readString) : null; + this.stackTraceEvents = in.readBoolean() ? in.readMap(StreamInput::readString, StreamInput::readInt) : null; + this.totalFrames = in.readInt(); + this.error = in.readBoolean() ? in.readException() : null; + } + + public GetProfilingResponse( + Map stackTraces, + Map stackFrames, + Map executables, + Map stackTraceEvents, + int totalFrames + ) { + this(stackTraces, stackFrames, executables, stackTraceEvents, totalFrames, null); + } + + public GetProfilingResponse(Exception error) { + this(null, null, null, null, 0, error); + } + + private GetProfilingResponse( + Map stackTraces, + Map stackFrames, + Map executables, + Map stackTraceEvents, + int totalFrames, + Exception error + ) { + this.stackTraces = stackTraces; + this.stackFrames = stackFrames; + this.executables = executables; + this.stackTraceEvents = stackTraceEvents; + this.totalFrames = totalFrames; + this.error = error; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + if (stackTraces != null) { + out.writeBoolean(true); + out.writeMap(stackTraces, StreamOutput::writeString, (o, v) -> { + o.writeIntArray(v.addressOrLines); + o.writeStringArray(v.fileIds); + o.writeStringArray(v.frameIds); + o.writeIntArray(v.typeIds); + }); + } else { + out.writeBoolean(false); + } + if (stackFrames != null) { + out.writeBoolean(true); + out.writeMap(stackFrames, StreamOutput::writeString, (o, v) -> { + o.writeOptionalString(v.fileName); + o.writeOptionalString(v.functionName); + o.writeOptionalInt(v.functionOffset); + o.writeOptionalInt(v.lineNumber); + o.writeOptionalInt(v.sourceType); + }); + } else { + out.writeBoolean(false); + } + if (executables != null) { + out.writeBoolean(true); + out.writeMap(executables, StreamOutput::writeString, StreamOutput::writeString); + } else { + out.writeBoolean(false); + } + if (stackTraceEvents != null) { + out.writeBoolean(true); + out.writeMap(stackTraceEvents, StreamOutput::writeString, StreamOutput::writeInt); + } else { + out.writeBoolean(false); + } + out.writeInt(totalFrames); + if (error != null) { + out.writeBoolean(true); + out.writeException(error); + } else { + out.writeBoolean(false); + } + } + + @Override + public RestStatus status() { + return error != null ? ExceptionsHelper.status(ExceptionsHelper.unwrapCause(error)) : OK; + } + + public Map getStackTraces() { + return stackTraces; + } + + public Map getStackFrames() { + return stackFrames; + } + + public Map getExecutables() { + return executables; + } + + public Map getStackTraceEvents() { + return stackTraceEvents; + } + + public int getTotalFrames() { + return totalFrames; + } + + public Exception getError() { + return error; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + if (stackTraces != null) { + builder.startObject("stack_traces"); + builder.mapContents(stackTraces); + builder.endObject(); + } + if (stackFrames != null) { + builder.startObject("stack_frames"); + builder.mapContents(stackFrames); + builder.endObject(); + } + if (executables != null) { + builder.startObject("executables"); + builder.mapContents(executables); + builder.endObject(); + } + if (stackTraceEvents != null) { + builder.startObject("stack_trace_events"); + builder.mapContents(stackTraceEvents); + builder.endObject(); + } + builder.field("total_frames", totalFrames); + if (error != null) { + builder.startObject("error"); + ElasticsearchException.generateThrowableXContent(builder, params, error); + builder.endObject(); + } + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GetProfilingResponse response = (GetProfilingResponse) o; + return totalFrames == response.totalFrames + && Objects.equals(stackTraces, response.stackTraces) + && Objects.equals(stackFrames, response.stackFrames) + && Objects.equals(executables, response.executables) + && Objects.equals(stackTraceEvents, response.stackTraceEvents) + && Objects.equals(error, response.error); + } + + @Override + public int hashCode() { + return Objects.hash(stackTraces, stackFrames, executables, stackTraceEvents, totalFrames, error); + } +} diff --git a/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/ProfilingPlugin.java b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/ProfilingPlugin.java new file mode 100644 index 000000000000..bc6fb4b9dc1d --- /dev/null +++ b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/ProfilingPlugin.java @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.profiler; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.client.internal.Client; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.cluster.routing.allocation.decider.AllocationDeciders; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.IndexScopedSettings; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsFilter; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.NodeEnvironment; +import org.elasticsearch.plugins.ActionPlugin; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.repositories.RepositoriesService; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestHandler; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.search.aggregations.MultiBucketConsumerService; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.tracing.Tracer; +import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.xcontent.NamedXContentRegistry; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.function.Supplier; + +import static java.util.Collections.singletonList; + +public class ProfilingPlugin extends Plugin implements ActionPlugin { + private static final Logger logger = LogManager.getLogger(ProfilingPlugin.class); + public static final Setting PROFILING_ENABLED = Setting.boolSetting( + "xpack.profiling.enabled", + false, + Setting.Property.NodeScope + ); + private static final int REQUIRED_MAX_BUCKETS = 150_000; + private final Settings settings; + private final boolean enabled; + + public ProfilingPlugin(Settings settings) { + this.settings = settings; + this.enabled = PROFILING_ENABLED.get(settings); + } + + @Override + public Collection createComponents( + Client client, + ClusterService clusterService, + ThreadPool threadPool, + ResourceWatcherService resourceWatcherService, + ScriptService scriptService, + NamedXContentRegistry xContentRegistry, + Environment environment, + NodeEnvironment nodeEnvironment, + NamedWriteableRegistry namedWriteableRegistry, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier repositoriesServiceSupplier, + Tracer tracer, + AllocationDeciders allocationDeciders + ) { + logger.info("Profiling is {}", enabled ? "enabled" : "disabled"); + return super.createComponents( + client, + clusterService, + threadPool, + resourceWatcherService, + scriptService, + xContentRegistry, + environment, + nodeEnvironment, + namedWriteableRegistry, + indexNameExpressionResolver, + repositoriesServiceSupplier, + tracer, + allocationDeciders + ); + } + + @Override + public List getRestHandlers( + final Settings settings, + final RestController restController, + final ClusterSettings clusterSettings, + final IndexScopedSettings indexScopedSettings, + final SettingsFilter settingsFilter, + final IndexNameExpressionResolver indexNameExpressionResolver, + final Supplier nodesInCluster + ) { + if (enabled) { + return singletonList(new RestGetProfilingAction()); + } else { + return Collections.emptyList(); + } + } + + @Override + public List> getSettings() { + return List.of(PROFILING_ENABLED); + } + + @Override + public Settings additionalSettings() { + // workaround until https://github.com/elastic/elasticsearch/issues/91776 is implemented + final Settings.Builder builder = Settings.builder(); + if (enabled) { + if (MultiBucketConsumerService.MAX_BUCKET_SETTING.exists(settings) == false) { + logger.debug("Overriding [{}] to [{}].", MultiBucketConsumerService.MAX_BUCKET_SETTING, REQUIRED_MAX_BUCKETS); + builder.put(MultiBucketConsumerService.MAX_BUCKET_SETTING.getKey(), REQUIRED_MAX_BUCKETS); + } else { + Integer configuredMaxBuckets = MultiBucketConsumerService.MAX_BUCKET_SETTING.get(settings); + if (configuredMaxBuckets != null && configuredMaxBuckets < REQUIRED_MAX_BUCKETS) { + final String message = String.format( + Locale.ROOT, + "Profiling requires [%s] to be set at least to [%d] but was configured to [%d].", + MultiBucketConsumerService.MAX_BUCKET_SETTING.getKey(), + REQUIRED_MAX_BUCKETS, + configuredMaxBuckets + ); + throw new IllegalArgumentException(message); + } + } + } + return builder.build(); + } + + @Override + public List> getActions() { + return List.of(new ActionHandler<>(GetProfilingAction.INSTANCE, TransportGetProfilingAction.class)); + } +} diff --git a/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/RestGetProfilingAction.java b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/RestGetProfilingAction.java new file mode 100644 index 000000000000..3b831f257e71 --- /dev/null +++ b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/RestGetProfilingAction.java @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.profiler; + +import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.action.RestCancellableNodeClient; +import org.elasticsearch.rest.action.RestStatusToXContentListener; + +import java.io.IOException; +import java.util.List; + +import static org.elasticsearch.rest.RestRequest.Method.GET; +import static org.elasticsearch.rest.RestRequest.Method.POST; + +public class RestGetProfilingAction extends BaseRestHandler { + @Override + public List routes() { + return List.of(new Route(GET, "/_profiling/stacktraces"), new Route(POST, "/_profiling/stacktraces")); + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + GetProfilingRequest getProfilingRequest = new GetProfilingRequest(); + request.applyContentParser(getProfilingRequest::parseXContent); + + return channel -> { + RestStatusToXContentListener listener = new RestStatusToXContentListener<>(channel); + RestCancellableNodeClient cancelClient = new RestCancellableNodeClient(client, request.getHttpChannel()); + cancelClient.execute(GetProfilingAction.INSTANCE, getProfilingRequest, listener); + }; + } + + @Override + public String getName() { + return "get_profiling_action"; + } +} diff --git a/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/StackFrame.java b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/StackFrame.java new file mode 100644 index 000000000000..928277fbe7e0 --- /dev/null +++ b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/StackFrame.java @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.profiler; + +import org.elasticsearch.xcontent.ObjectPath; +import org.elasticsearch.xcontent.ToXContentObject; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; + +final class StackFrame implements ToXContentObject { + String fileName; + String functionName; + Integer functionOffset; + Integer lineNumber; + Integer sourceType; + + StackFrame(String fileName, String functionName, Integer functionOffset, Integer lineNumber, Integer sourceType) { + this.fileName = fileName; + this.functionName = functionName; + this.functionOffset = functionOffset; + this.lineNumber = lineNumber; + this.sourceType = sourceType; + } + + public static StackFrame fromSource(Map source) { + return new StackFrame( + ObjectPath.eval("Stackframe.file.name", source), + ObjectPath.eval("Stackframe.function.name", source), + ObjectPath.eval("Stackframe.function.offset", source), + ObjectPath.eval("Stackframe.line.number", source), + ObjectPath.eval("Stackframe.source.type", source) + ); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("file_name", this.fileName); + builder.field("function_name", this.functionName); + builder.field("function_offset", this.functionOffset); + builder.field("line_number", this.lineNumber); + builder.field("source_type", this.sourceType); + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StackFrame that = (StackFrame) o; + return Objects.equals(fileName, that.fileName) + && Objects.equals(functionName, that.functionName) + && Objects.equals(functionOffset, that.functionOffset) + && Objects.equals(lineNumber, that.lineNumber) + && Objects.equals(sourceType, that.sourceType); + } + + @Override + public int hashCode() { + return Objects.hash(fileName, functionName, functionOffset, lineNumber, sourceType); + } +} diff --git a/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/StackTrace.java b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/StackTrace.java new file mode 100644 index 000000000000..22c4c71887dc --- /dev/null +++ b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/StackTrace.java @@ -0,0 +1,231 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.profiler; + +import org.elasticsearch.xcontent.ObjectPath; +import org.elasticsearch.xcontent.ToXContentObject; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; + +final class StackTrace implements ToXContentObject { + int[] addressOrLines; + String[] fileIds; + String[] frameIds; + int[] typeIds; + + StackTrace(int[] addressOrLines, String[] fileIds, String[] frameIds, int[] typeIds) { + this.addressOrLines = addressOrLines; + this.fileIds = fileIds; + this.frameIds = frameIds; + this.typeIds = typeIds; + } + + private static final int BASE64_FRAME_ID_LENGTH = 32; + + private static final String SAFE_BASE64_ENCODER = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234456789-_"; + + // tag::noformat + private static final int[] SAFE_BASE64_DECODER = new int[] { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 0, 0, 0, 0, 63, 0, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, + 45, 46, 47, 48, 49, 50, 51, 0, 0, 0, 0, 0 + }; + // end::noformat + + /** + * + * runLengthDecodeBase64Url decodes a run-length encoding for the base64-encoded input string. + * E.g. the string 'BQADAg' is converted into an int array like [0, 0, 0, 0, 0, 2, 2, 2]. + * The motivating intent for this method is to unpack a base64-encoded run-length encoding + * without using intermediate storage. + * + * This method relies on these assumptions and details: + * - array encoded using run-length and base64 always returns string of length 0, 3, or 6 (mod 8) + * - since original array is composed of int, we ignore Unicode codepoints + * + * @param input A base64-encoded string. + * @param size Decoded length of the input. + * @param capacity Capacity of the underlying array (>= size). + * + * @return Corresponding numbers that are encoded in the input. + */ + // package-private for testing + static int[] runLengthDecodeBase64Url(String input, int size, int capacity) { + int[] output = new int[capacity]; + int multipleOf8 = size / 8; + int remainder = size % 8; + + int n; + int count; + int value; + int i; + int j = 0; + + for (i = 0; i < multipleOf8 * 8; i += 8) { + n = (charCodeAt(input, i) << 26) | (charCodeAt(input, i + 1) << 20) | (charCodeAt(input, i + 2) << 14) | (charCodeAt( + input, + i + 3 + ) << 8) | (charCodeAt(input, i + 4) << 2) | (charCodeAt(input, i + 5) >> 4); + + count = (n >> 24) & 0xff; + value = (n >> 16) & 0xff; + + Arrays.fill(output, j, j + count, value); + j += count; + + count = (n >> 8) & 0xff; + value = n & 0xff; + + Arrays.fill(output, j, j + count, value); + j += count; + + n = ((charCodeAt(input, i + 5) & 0xf) << 12) | (charCodeAt(input, i + 6) << 6) | charCodeAt(input, i + 7); + + count = (n >> 8) & 0xff; + value = n & 0xff; + + Arrays.fill(output, j, j + count, value); + j += count; + } + + if (remainder == 6) { + n = (charCodeAt(input, i) << 26) | (charCodeAt(input, i + 1) << 20) | (charCodeAt(input, i + 2) << 14) | (charCodeAt( + input, + i + 3 + ) << 8) | (charCodeAt(input, i + 4) << 2) | (charCodeAt(input, i + 5) >> 4); + + count = (n >> 24) & 0xff; + value = (n >> 16) & 0xff; + + Arrays.fill(output, j, j + count, value); + j += count; + + count = (n >> 8) & 0xff; + value = n & 0xff; + + Arrays.fill(output, j, j + count, value); + j += count; + } else if (remainder == 3) { + n = (charCodeAt(input, i) << 12) | (charCodeAt(input, i + 1) << 6) | charCodeAt(input, i + 2); + n >>= 2; + + count = (n >> 8) & 0xff; + value = n & 0xff; + + Arrays.fill(output, j, j + count, value); + j += count; + } + return output; + } + + // package-private for testing + static int getAddressFromStackFrameID(String frameID) { + int address = charCodeAt(frameID, 21) & 0xf; + address <<= 6; + address += charCodeAt(frameID, 22); + address <<= 6; + address += charCodeAt(frameID, 23); + address <<= 6; + address += charCodeAt(frameID, 24); + address <<= 6; + address += charCodeAt(frameID, 25); + address <<= 6; + address += charCodeAt(frameID, 26); + address <<= 6; + address += charCodeAt(frameID, 27); + address <<= 6; + address += charCodeAt(frameID, 28); + address <<= 6; + address += charCodeAt(frameID, 29); + address <<= 6; + address += charCodeAt(frameID, 30); + address <<= 6; + address += charCodeAt(frameID, 31); + return address; + } + + private static int charCodeAt(String input, int i) { + return SAFE_BASE64_DECODER[input.charAt(i) & 0x7f]; + } + + // package-private for testing + static String getFileIDFromStackFrameID(String frameID) { + return frameID.substring(0, 21) + SAFE_BASE64_ENCODER.charAt(frameID.charAt(21) & 0x30); + } + + public static StackTrace fromSource(Map source) { + String inputFrameIDs = ObjectPath.eval("Stacktrace.frame.ids", source); + String inputFrameTypes = ObjectPath.eval("Stacktrace.frame.types", source); + int countsFrameIDs = inputFrameIDs.length() / BASE64_FRAME_ID_LENGTH; + + String[] fileIDs = new String[countsFrameIDs]; + String[] frameIDs = new String[countsFrameIDs]; + int[] addressOrLines = new int[countsFrameIDs]; + + // Step 1: Convert the base64-encoded frameID list into two separate + // lists (frame IDs and file IDs), both of which are also base64-encoded. + // + // To get the frame ID, we grab the next 32 bytes. + // + // To get the file ID, we grab the first 22 bytes of the frame ID. + // However, since the file ID is base64-encoded using 21.33 bytes + // (16 * 4 / 3), then the 22 bytes have an extra 4 bits from the + // address (see diagram in definition of EncodedStackTrace). + for (int i = 0, pos = 0; i < countsFrameIDs; i++, pos += BASE64_FRAME_ID_LENGTH) { + String frameID = inputFrameIDs.substring(pos, pos + BASE64_FRAME_ID_LENGTH); + frameIDs[i] = frameID; + fileIDs[i] = getFileIDFromStackFrameID(frameID); + addressOrLines[i] = getAddressFromStackFrameID(frameID); + } + + // Step 2: Convert the run-length byte encoding into a list of uint8s. + int[] typeIDs = runLengthDecodeBase64Url(inputFrameTypes, inputFrameTypes.length(), countsFrameIDs); + + return new StackTrace(addressOrLines, fileIDs, frameIDs, typeIDs); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.array("address_or_lines", this.addressOrLines); + builder.array("file_ids", this.fileIds); + builder.array("frame_ids", this.frameIds); + builder.array("type_ids", this.typeIds); + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StackTrace that = (StackTrace) o; + return Arrays.equals(addressOrLines, that.addressOrLines) + && Arrays.equals(fileIds, that.fileIds) + && Arrays.equals(frameIds, that.frameIds) + && Arrays.equals(typeIds, that.typeIds); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(addressOrLines); + result = 31 * result + Arrays.hashCode(fileIds); + result = 31 * result + Arrays.hashCode(frameIds); + result = 31 * result + Arrays.hashCode(typeIds); + return result; + } +} diff --git a/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/TransportGetProfilingAction.java b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/TransportGetProfilingAction.java new file mode 100644 index 000000000000..fa9c9f1fd15f --- /dev/null +++ b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/TransportGetProfilingAction.java @@ -0,0 +1,338 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.profiler; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.get.MultiGetItemResponse; +import org.elasticsearch.action.get.MultiGetResponse; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.client.internal.Client; +import org.elasticsearch.client.internal.ParentTaskAssigningClient; +import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.util.Maps; +import org.elasticsearch.search.aggregations.bucket.terms.StringTerms; +import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; +import org.elasticsearch.search.aggregations.metrics.Sum; +import org.elasticsearch.search.aggregations.metrics.SumAggregationBuilder; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xcontent.ObjectPath; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +public class TransportGetProfilingAction extends HandledTransportAction { + private final NodeClient nodeClient; + private final TransportService transportService; + + @Inject + public TransportGetProfilingAction(TransportService transportService, ActionFilters actionFilters, NodeClient nodeClient) { + super(GetProfilingAction.NAME, transportService, actionFilters, GetProfilingRequest::new); + this.nodeClient = nodeClient; + this.transportService = transportService; + } + + @Override + protected void doExecute(Task submitTask, GetProfilingRequest request, ActionListener submitListener) { + Client client = new ParentTaskAssigningClient(this.nodeClient, transportService.getLocalNode(), submitTask); + EventsIndex mediumDownsampled = EventsIndex.MEDIUM_DOWNSAMPLED; + client.prepareSearch(mediumDownsampled.getName()) + .setSize(0) + .setQuery(request.getQuery()) + .setTrackTotalHits(true) + .execute(new ActionListener<>() { + @Override + public void onResponse(SearchResponse searchResponse) { + long sampleCount = searchResponse.getHits().getTotalHits().value; + EventsIndex resampledIndex = mediumDownsampled.getResampledIndex(request.getSampleSize(), sampleCount); + searchEventGroupByStackTrace(client, request, resampledIndex, submitListener); + } + + @Override + public void onFailure(Exception e) { + submitListener.onFailure(e); + } + }); + } + + private void searchEventGroupByStackTrace( + Client client, + GetProfilingRequest request, + EventsIndex eventsIndex, + ActionListener submitListener + ) { + GetProfilingResponseBuilder responseBuilder = new GetProfilingResponseBuilder(); + client.prepareSearch(eventsIndex.getName()) + .setTrackTotalHits(false) + .setQuery(request.getQuery()) + .addAggregation( + new TermsAggregationBuilder("group_by") + // 'size' should be max 100k, but might be slightly more. Better be on the safe side. + .size(150_000) + .field("Stacktrace.id") + // 'execution_hint: map' skips the slow building of ordinals that we don't need. + // Especially with high cardinality fields, this makes aggregations really slow. + .executionHint("map") + .subAggregation(new SumAggregationBuilder("count").field("Stacktrace.count")) + ) + .addAggregation(new SumAggregationBuilder("total_count").field("Stacktrace.count")) + .execute(new ActionListener<>() { + @Override + public void onResponse(SearchResponse searchResponse) { + Sum totalCountAgg = searchResponse.getAggregations().get("total_count"); + long totalCount = Math.round(totalCountAgg.value()); + Resampler resampler = new Resampler(request, eventsIndex.getSampleRate(), totalCount); + StringTerms stacktraces = searchResponse.getAggregations().get("group_by"); + Map stackTraceEvents = Maps.newHashMapWithExpectedSize(stacktraces.getBuckets().size()); + for (StringTerms.Bucket bucket : stacktraces.getBuckets()) { + Sum count = bucket.getAggregations().get("count"); + int finalCount = resampler.adjustSampleCount((int) count.value()); + if (finalCount > 0) { + stackTraceEvents.put(bucket.getKeyAsString(), finalCount); + } + } + if (stackTraceEvents.isEmpty() == false) { + responseBuilder.setStackTraceEvents(stackTraceEvents); + retrieveStackTraces(client, responseBuilder, submitListener); + } else { + submitListener.onResponse(responseBuilder.build()); + } + } + + @Override + public void onFailure(Exception e) { + submitListener.onFailure(e); + } + }); + } + + private void retrieveStackTraces( + Client client, + GetProfilingResponseBuilder responseBuilder, + ActionListener submitListener + ) { + client.prepareMultiGet() + .addIds("profiling-stacktraces", responseBuilder.getStackTraceEvents().keySet()) + .setRealtime(true) + .execute(new ActionListener<>() { + @Override + public void onResponse(MultiGetResponse multiGetItemResponses) { + Map stackTracePerId = new HashMap<>(); + Set stackFrameIds = new HashSet<>(); + Set executableIds = new HashSet<>(); + int totalFrames = 0; + for (MultiGetItemResponse trace : multiGetItemResponses) { + if (trace.isFailed() == false && trace.getResponse().isExists()) { + String id = trace.getId(); + StackTrace stacktrace = StackTrace.fromSource(trace.getResponse().getSource()); + stackTracePerId.put(id, stacktrace); + totalFrames += stacktrace.frameIds.length; + stackFrameIds.addAll(Arrays.asList(stacktrace.frameIds)); + executableIds.addAll(Arrays.asList(stacktrace.fileIds)); + } + } + responseBuilder.setStackTraces(stackTracePerId); + responseBuilder.setTotalFrames(totalFrames); + retrieveStackTraceDetails(client, responseBuilder, stackFrameIds, executableIds, submitListener); + } + + @Override + public void onFailure(Exception e) { + submitListener.onFailure(e); + } + }); + } + + private void retrieveStackTraceDetails( + Client client, + GetProfilingResponseBuilder responseBuilder, + Set stackFrameIds, + Set executableIds, + ActionListener submitListener + ) { + + DetailsHandler handler = new DetailsHandler(responseBuilder, submitListener); + + if (stackFrameIds.isEmpty()) { + handler.onStackFramesResponse(new MultiGetResponse(new MultiGetItemResponse[0])); + } else { + client.prepareMultiGet().addIds("profiling-stackframes", stackFrameIds).setRealtime(true).execute(new ActionListener<>() { + @Override + public void onResponse(MultiGetResponse multiGetItemResponses) { + handler.onStackFramesResponse(multiGetItemResponses); + } + + @Override + public void onFailure(Exception e) { + submitListener.onFailure(e); + } + }); + } + // no data dependency - we can do this concurrently + if (executableIds.isEmpty()) { + handler.onExecutableDetailsResponse(new MultiGetResponse(new MultiGetItemResponse[0])); + } else { + client.prepareMultiGet().addIds("profiling-executables", executableIds).setRealtime(true).execute(new ActionListener<>() { + @Override + public void onResponse(MultiGetResponse multiGetItemResponses) { + handler.onExecutableDetailsResponse(multiGetItemResponses); + } + + @Override + public void onFailure(Exception e) { + submitListener.onFailure(e); + } + }); + } + } + + private static class Resampler { + private final boolean requiresResampling; + + private final Random r; + + private final double sampleRate; + + private final double p; + + Resampler(GetProfilingRequest request, double sampleRate, long totalCount) { + // Manually reduce sample count if totalCount exceeds sampleSize by 10%. + if (totalCount > request.getSampleSize() * 1.1) { + this.requiresResampling = true; + // Make the RNG predictable to get reproducible results. + this.r = new Random(request.hashCode()); + this.sampleRate = sampleRate; + this.p = (double) request.getSampleSize() / totalCount; + } else { + this.requiresResampling = false; + this.r = null; + this.sampleRate = sampleRate; + this.p = 1.0d; + } + } + + public int adjustSampleCount(int originalCount) { + if (requiresResampling) { + int newCount = 0; + for (int i = 0; i < originalCount; i++) { + if (r.nextDouble() < p) { + newCount++; + } + } + if (newCount > 0) { + // Adjust the sample counts from down-sampled to fully sampled. + // Be aware that downsampling drops entries from stackTraceEvents, so that + // the sum of the upscaled count values is less that totalCount. + return (int) Math.floor(newCount / (sampleRate * p)); + } else { + return 0; + } + } else { + return originalCount; + } + } + } + + /** + * Collects stack trace details which are retrieved concurrently and sends a response only when all details are known. + */ + private static class DetailsHandler { + private final GetProfilingResponseBuilder builder; + private final ActionListener submitListener; + private volatile Map executables; + private volatile Map stackFrames; + + private DetailsHandler(GetProfilingResponseBuilder builder, ActionListener submitListener) { + this.builder = builder; + this.submitListener = submitListener; + } + + public void onStackFramesResponse(MultiGetResponse multiGetItemResponses) { + Map stackFrames = new HashMap<>(); + for (MultiGetItemResponse frame : multiGetItemResponses) { + if (frame.isFailed() == false && frame.getResponse().isExists()) { + stackFrames.put(frame.getId(), StackFrame.fromSource(frame.getResponse().getSource())); + } + } + // publish to object state only when completely done, otherwise mayFinish() could run twice + this.stackFrames = stackFrames; + mayFinish(); + } + + public void onExecutableDetailsResponse(MultiGetResponse multiGetItemResponses) { + Map executables = new HashMap<>(); + for (MultiGetItemResponse executable : multiGetItemResponses) { + if (executable.isFailed() == false && executable.getResponse().isExists()) { + executables.put(executable.getId(), ObjectPath.eval("Executable.file.name", executable.getResponse().getSource())); + } + } + // publish to object state only when completely done, otherwise mayFinish() could run twice + this.executables = executables; + mayFinish(); + } + + public void mayFinish() { + if (executables != null && stackFrames != null) { + builder.setExecutables(executables); + builder.setStackFrames(stackFrames); + submitListener.onResponse(builder.build()); + } + } + } + + private static class GetProfilingResponseBuilder { + private Map stackTraces; + private int totalFrames; + private Map stackFrames; + private Map executables; + private Map stackTraceEvents; + private Exception error; + + public void setStackTraces(Map stackTraces) { + this.stackTraces = stackTraces; + } + + public void setTotalFrames(int totalFrames) { + this.totalFrames = totalFrames; + } + + public void setStackFrames(Map stackFrames) { + this.stackFrames = stackFrames; + } + + public void setExecutables(Map executables) { + this.executables = executables; + } + + public void setStackTraceEvents(Map stackTraceEvents) { + this.stackTraceEvents = stackTraceEvents; + } + + public Map getStackTraceEvents() { + return stackTraceEvents; + } + + public void setError(Exception error) { + this.error = error; + } + + public GetProfilingResponse build() { + if (error != null) { + return new GetProfilingResponse(error); + } else { + return new GetProfilingResponse(stackTraces, stackFrames, executables, stackTraceEvents, totalFrames); + } + } + } +} diff --git a/x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/EventsIndexTests.java b/x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/EventsIndexTests.java new file mode 100644 index 000000000000..b653fecfe37f --- /dev/null +++ b/x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/EventsIndexTests.java @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.profiler; + +import org.elasticsearch.test.ESTestCase; + +public class EventsIndexTests extends ESTestCase { + public void testFullIndex() { + EventsIndex idx = EventsIndex.FULL_INDEX; + assertEquals("profiling-events-all", idx.getName()); + assertEquals(1.0d, idx.getSampleRate(), 1e-3); + } + + public void testResampledIndexSameSize() { + EventsIndex resampledIndex = EventsIndex.MEDIUM_DOWNSAMPLED.getResampledIndex(100, 100); + assertEquals("profiling-events-5pow06", resampledIndex.getName()); + assertEquals(Math.pow(1.0d / 5.0d, 6.0d), resampledIndex.getSampleRate(), 1e-9); + } + + public void testResampledIndexDifferentSizes() { + assertResampledIndex("profiling-events-5pow01", Math.pow(5.0d, 5)); + assertResampledIndex("profiling-events-5pow02", Math.pow(5.0d, 4)); + assertResampledIndex("profiling-events-5pow03", Math.pow(5.0d, 3)); + + assertResampledIndex("profiling-events-5pow04", Math.pow(5.0d, 2)); + assertResampledIndex("profiling-events-5pow05", Math.pow(5.0d, 1)); + + assertResampledIndex("profiling-events-5pow06", Math.pow(5.0d, 0)); + assertResampledIndex("profiling-events-5pow07", Math.pow(5.0d, -1)); + assertResampledIndex("profiling-events-5pow08", Math.pow(5.0d, -2)); + + assertResampledIndex("profiling-events-5pow09", Math.pow(5.0d, -3)); + assertResampledIndex("profiling-events-5pow10", Math.pow(5.0d, -4)); + assertResampledIndex("profiling-events-5pow11", Math.pow(5.0d, -5)); + } + + private void assertResampledIndex(String expectedName, double ratio) { + long currentSampleSize = 10_000_000L; + long targetSampleSize = (long) (currentSampleSize * ratio); + EventsIndex e = EventsIndex.MEDIUM_DOWNSAMPLED; + assertEquals(expectedName, e.getResampledIndex(targetSampleSize, currentSampleSize).getName()); + } +} diff --git a/x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/GetProfilingRequestTests.java b/x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/GetProfilingRequestTests.java new file mode 100644 index 000000000000..d4394b2b1926 --- /dev/null +++ b/x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/GetProfilingRequestTests.java @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.profiler; + +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.RangeQueryBuilder; +import org.elasticsearch.search.SearchModule; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.NamedXContentRegistry; +import org.elasticsearch.xcontent.XContentFactory; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; + +import static java.util.Collections.emptyList; + +public class GetProfilingRequestTests extends ESTestCase { + public void testSerialization() throws IOException { + Integer sampleSize = randomBoolean() ? randomIntBetween(0, Integer.MAX_VALUE) : null; + QueryBuilder query = randomBoolean() ? new BoolQueryBuilder() : null; + + GetProfilingRequest request = new GetProfilingRequest(sampleSize, query); + try (BytesStreamOutput out = new BytesStreamOutput()) { + request.writeTo(out); + try (NamedWriteableAwareStreamInput in = new NamedWriteableAwareStreamInput(out.bytes().streamInput(), writableRegistry())) { + GetProfilingRequest deserialized = new GetProfilingRequest(in); + assertEquals(sampleSize, deserialized.getSampleSize()); + assertEquals(query, deserialized.getQuery()); + } + } + } + + public void testParseValidXContent() throws IOException { + try (XContentParser content = createParser(XContentFactory.jsonBuilder() + //tag::noformat + .startObject() + .field("sample_size", 500) + .startObject("query") + .startObject("range") + .startObject("@timestamp") + .field("gte", "2022-10-05") + .endObject() + .endObject() + .endObject() + .endObject() + //end::noformat + )) { + + GetProfilingRequest profilingRequest = new GetProfilingRequest(); + profilingRequest.parseXContent(content); + + assertEquals(Integer.valueOf(500), profilingRequest.getSampleSize()); + // a basic check suffices here + assertEquals("@timestamp", ((RangeQueryBuilder) profilingRequest.getQuery()).fieldName()); + } + } + + public void testParseXContentUnrecognizedField() throws IOException { + try (XContentParser content = createParser(XContentFactory.jsonBuilder() + //tag::noformat + .startObject() + // should be sample_size + .field("sample-size", 500) + .startObject("query") + .startObject("range") + .startObject("@timestamp") + .field("gte", "2022-10-05") + .endObject() + .endObject() + .endObject() + .endObject() + //end::noformat + )) { + + GetProfilingRequest profilingRequest = new GetProfilingRequest(); + ParsingException ex = expectThrows(ParsingException.class, () -> profilingRequest.parseXContent(content)); + assertEquals("Unknown key for a VALUE_NUMBER in [sample-size].", ex.getMessage()); + } + } + + @Override + protected NamedXContentRegistry xContentRegistry() { + // to register the query parser + return new NamedXContentRegistry(new SearchModule(Settings.EMPTY, emptyList()).getNamedXContents()); + } + + @Override + protected NamedWriteableRegistry writableRegistry() { + return new NamedWriteableRegistry(new SearchModule(Settings.EMPTY, emptyList()).getNamedWriteables()); + } +} diff --git a/x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/GetProfilingResponseTests.java b/x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/GetProfilingResponseTests.java new file mode 100644 index 000000000000..a0613be3ab42 --- /dev/null +++ b/x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/GetProfilingResponseTests.java @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.profiler; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; + +import java.util.Map; +import java.util.function.Supplier; + +public class GetProfilingResponseTests extends AbstractWireSerializingTestCase { + private T randomNullable(Supplier v) { + return randomBoolean() ? v.get() : null; + } + + private T randomNullable(T v) { + return randomBoolean() ? v : null; + } + + @Override + protected GetProfilingResponse createTestInstance() { + int totalFrames = randomIntBetween(1, 100); + + Map stackTraces = randomNullable( + Map.of( + "QjoLteG7HX3VUUXr-J4kHQ", + new StackTrace( + new int[] { 1083999 }, + new String[] { "QCCDqjSg3bMK1C4YRK6Tiw" }, + new String[] { "QCCDqjSg3bMK1C4YRK6TiwAAAAAAEIpf" }, + new int[] { 2 } + ) + ) + ); + Map stackFrames = randomNullable( + Map.of( + "QCCDqjSg3bMK1C4YRK6TiwAAAAAAEIpf", + new StackFrame( + randomNullable(() -> randomAlphaOfLength(20)), + randomNullable(() -> randomAlphaOfLength(20)), + randomNullable(() -> randomIntBetween(1, Integer.MAX_VALUE)), + randomNullable(() -> randomIntBetween(1, 30_000)), + randomNullable(() -> randomIntBetween(1, 10)) + ) + ) + ); + Map executables = randomNullable(Map.of("QCCDqjSg3bMK1C4YRK6Tiw", "libc.so.6")); + Map stackTraceEvents = randomNullable(Map.of(randomAlphaOfLength(12), randomIntBetween(1, 200))); + + return new GetProfilingResponse(stackTraces, stackFrames, executables, stackTraceEvents, totalFrames); + } + + @Override + protected Writeable.Reader instanceReader() { + return GetProfilingResponse::new; + } +} diff --git a/x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/RestGetProfilingActionTests.java b/x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/RestGetProfilingActionTests.java new file mode 100644 index 000000000000..d9a3532b451f --- /dev/null +++ b/x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/RestGetProfilingActionTests.java @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.profiler; + +import org.apache.lucene.util.SetOnce; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.search.SearchModule; +import org.elasticsearch.test.rest.FakeRestRequest; +import org.elasticsearch.test.rest.RestActionTestCase; +import org.elasticsearch.xcontent.NamedXContentRegistry; +import org.elasticsearch.xcontent.XContentType; +import org.junit.Before; + +import java.util.Collections; + +import static java.util.Collections.emptyList; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class RestGetProfilingActionTests extends RestActionTestCase { + @Before + public void setUpAction() { + controller().registerHandler(new RestGetProfilingAction()); + } + + public void testPrepareEmptyRequest() { + SetOnce executeCalled = new SetOnce<>(); + verifyingClient.setExecuteLocallyVerifier((actionType, request) -> { + assertThat(request, instanceOf(GetProfilingRequest.class)); + GetProfilingRequest profilingRequest = (GetProfilingRequest) request; + assertThat(profilingRequest.getSampleSize(), nullValue()); + assertThat(profilingRequest.getQuery(), nullValue()); + executeCalled.set(true); + return new GetProfilingResponse( + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap(), + 0 + ); + }); + RestRequest profilingRequest = new FakeRestRequest.Builder(xContentRegistry()).withMethod(RestRequest.Method.POST) + .withPath("/_profiling/stacktraces") + .withContent(new BytesArray("{}"), XContentType.JSON) + .build(); + dispatchRequest(profilingRequest); + assertThat(executeCalled.get(), equalTo(true)); + } + + public void testPrepareParameterizedRequest() { + SetOnce executeCalled = new SetOnce<>(); + verifyingClient.setExecuteLocallyVerifier((actionType, request) -> { + assertThat(request, instanceOf(GetProfilingRequest.class)); + GetProfilingRequest profilingRequest = (GetProfilingRequest) request; + assertThat(profilingRequest.getSampleSize(), is(10000)); + assertThat(profilingRequest.getQuery(), notNullValue(QueryBuilder.class)); + executeCalled.set(true); + return new GetProfilingResponse( + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap(), + 0 + ); + }); + RestRequest profilingRequest = new FakeRestRequest.Builder(xContentRegistry()).withMethod(RestRequest.Method.POST) + .withPath("/_profiling/stacktraces") + .withContent(new BytesArray(""" + { + "sample_size": 10000, + "query": { + "bool": { + "filter": [ + { + "range": { + "@timestamp": { + "gte": "2022-10-05", + "lt": "2022-12-05" + } + } + } + ] + } + } + } + """), XContentType.JSON) + .build(); + dispatchRequest(profilingRequest); + assertThat(executeCalled.get(), equalTo(true)); + } + + @Override + protected NamedXContentRegistry xContentRegistry() { + // to register the query parser + return new NamedXContentRegistry(new SearchModule(Settings.EMPTY, emptyList()).getNamedXContents()); + } +} diff --git a/x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/StackFrameTests.java b/x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/StackFrameTests.java new file mode 100644 index 000000000000..36cfe24bd001 --- /dev/null +++ b/x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/StackFrameTests.java @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.profiler; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.EqualsHashCodeTestUtils; +import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentFactory; +import org.elasticsearch.xcontent.XContentType; + +import java.io.IOException; +import java.util.Map; + +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent; + +public class StackFrameTests extends ESTestCase { + public void testCreateFromSource() { + // tag::noformat + StackFrame frame = StackFrame.fromSource( + Map.of("Stackframe", Map.of( + "file", Map.of("name", "Main.java"), + "function", Map.of( + "name", "helloWorld", + "offset", 31733 + ), + "line", Map.of("number", 22), + "source", Map.of("type", 3)) + ) + ); + // end::noformat + assertEquals(Integer.valueOf(3), frame.sourceType); + assertEquals("Main.java", frame.fileName); + assertEquals("helloWorld", frame.functionName); + assertEquals(Integer.valueOf(31733), frame.functionOffset); + assertEquals(Integer.valueOf(22), frame.lineNumber); + } + + public void testToXContent() throws IOException { + XContentType contentType = randomFrom(XContentType.values()); + XContentBuilder expectedRequest = XContentFactory.contentBuilder(contentType) + .startObject() + .field("file_name", "Main.java") + .field("function_name", "helloWorld") + .field("function_offset", 31733) + .field("line_number", 22) + .field("source_type", 3) + .endObject(); + + XContentBuilder actualRequest = XContentFactory.contentBuilder(contentType); + StackFrame stackTrace = new StackFrame("Main.java", "helloWorld", 31733, 22, 3); + stackTrace.toXContent(actualRequest, ToXContent.EMPTY_PARAMS); + + assertToXContentEquivalent(BytesReference.bytes(expectedRequest), BytesReference.bytes(actualRequest), contentType); + } + + public void testEquality() { + StackFrame frame = new StackFrame("Main.java", "helloWorld", 31733, 22, 3); + EqualsHashCodeTestUtils.checkEqualsAndHashCode( + frame, + (o -> new StackFrame(o.fileName, o.functionName, o.functionOffset, o.lineNumber, o.sourceType)) + ); + + } +} diff --git a/x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/StackTraceTests.java b/x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/StackTraceTests.java new file mode 100644 index 000000000000..30a525811478 --- /dev/null +++ b/x-pack/plugin/profiler/src/test/java/org/elasticsearch/xpack/profiler/StackTraceTests.java @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.profiler; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.EqualsHashCodeTestUtils; +import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentFactory; +import org.elasticsearch.xcontent.XContentType; + +import java.io.IOException; +import java.util.Map; + +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent; + +public class StackTraceTests extends ESTestCase { + public void testDecodeFrameId() { + String frameId = "AAAAAAAAAAUAAAAAAAAB3gAAAAAAD67u"; + // base64 encoded representation of the tuple (5, 478) + assertEquals("AAAAAAAAAAUAAAAAAAAB3g", StackTrace.getFileIDFromStackFrameID(frameId)); + assertEquals(1027822, StackTrace.getAddressFromStackFrameID(frameId)); + } + + public void testRunlengthDecodeUniqueValues() { + // 0 - 9 (reversed) + String encodedFrameTypes = "AQkBCAEHAQYBBQEEAQMBAgEBAQA"; + int[] actual = StackTrace.runLengthDecodeBase64Url(encodedFrameTypes, encodedFrameTypes.length(), 10); + assertArrayEquals(new int[] { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 }, actual); + } + + public void testRunlengthDecodeSingleValue() { + // "4", repeated ten times + String encodedFrameTypes = "CgQ"; + int[] actual = StackTrace.runLengthDecodeBase64Url(encodedFrameTypes, encodedFrameTypes.length(), 10); + assertArrayEquals(new int[] { 4, 4, 4, 4, 4, 4, 4, 4, 4, 4 }, actual); + } + + public void testRunlengthDecodeFillsGap() { + // "2", repeated three times + String encodedFrameTypes = "AwI"; + int[] actual = StackTrace.runLengthDecodeBase64Url(encodedFrameTypes, encodedFrameTypes.length(), 5); + // zeroes should be appended for the last two values which are not present in the encoded representation. + assertArrayEquals(new int[] { 2, 2, 2, 0, 0 }, actual); + } + + public void testRunlengthDecodeMixedValue() { + // 4 + String encodedFrameTypes = "BQADAg"; + int[] actual = StackTrace.runLengthDecodeBase64Url(encodedFrameTypes, encodedFrameTypes.length(), 8); + assertArrayEquals(new int[] { 0, 0, 0, 0, 0, 2, 2, 2 }, actual); + } + + public void testCreateFromSource() { + String ids = "AAAAAAAAAAUAAAAAAAAB3gAAAAAAD67u"; + String types = "AQI"; + // tag::noformat + StackTrace stackTrace = StackTrace.fromSource( + Map.of("Stacktrace", + Map.of("frame", + Map.of( + "ids", ids, + "types", types) + ) + ) + ); + // end::noformat + assertArrayEquals(new String[] { "AAAAAAAAAAUAAAAAAAAB3gAAAAAAD67u" }, stackTrace.frameIds); + assertArrayEquals(new String[] { "AAAAAAAAAAUAAAAAAAAB3g" }, stackTrace.fileIds); + assertArrayEquals(new int[] { 1027822 }, stackTrace.addressOrLines); + assertArrayEquals(new int[] { 2 }, stackTrace.typeIds); + } + + public void testToXContent() throws IOException { + XContentType contentType = randomFrom(XContentType.values()); + XContentBuilder expectedRequest = XContentFactory.contentBuilder(contentType) + .startObject() + .array("address_or_lines", new int[] { 1027822 }) + .array("file_ids", "AAAAAAAAAAUAAAAAAAAB3g") + .array("frame_ids", "AAAAAAAAAAUAAAAAAAAB3gAAAAAAD67u") + .array("type_ids", new int[] { 2 }) + .endObject(); + + XContentBuilder actualRequest = XContentFactory.contentBuilder(contentType); + StackTrace stackTrace = new StackTrace( + new int[] { 1027822 }, + new String[] { "AAAAAAAAAAUAAAAAAAAB3g" }, + new String[] { "AAAAAAAAAAUAAAAAAAAB3gAAAAAAD67u" }, + new int[] { 2 } + ); + stackTrace.toXContent(actualRequest, ToXContent.EMPTY_PARAMS); + + assertToXContentEquivalent(BytesReference.bytes(expectedRequest), BytesReference.bytes(actualRequest), contentType); + } + + public void testEquality() { + StackTrace stackTrace = new StackTrace( + new int[] { 1027822 }, + new String[] { "AAAAAAAAAAUAAAAAAAAB3g" }, + new String[] { "AAAAAAAAAAUAAAAAAAAB3gAAAAAAD67u" }, + new int[] { 2 } + ); + + EqualsHashCodeTestUtils.checkEqualsAndHashCode( + stackTrace, + (o -> new StackTrace(o.addressOrLines.clone(), o.fileIds.clone(), o.frameIds.clone(), o.typeIds.clone())) + ); + } +} diff --git a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java index 57d3a3aec972..65037651cf4c 100644 --- a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java +++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java @@ -437,6 +437,7 @@ public class Constants { "indices:data/read/mtv", "indices:data/read/mtv[shard]", "indices:data/read/open_point_in_time", + "indices:data/read/profiling", "indices:data/read/rank_eval", "indices:data/read/scroll", "indices:data/read/scroll/clear", From 4c9b50df104092be6f54c2881878efac29da1f68 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 30 Nov 2022 17:30:42 +0000 Subject: [PATCH 122/919] Clean up on exception while chunking XContent (#92024) If serializing a chunk throws an exception, we must clean up the chunk before propagating the exception. --- docs/changelog/92024.yaml | 5 +++ .../rest/ChunkedRestResponseBody.java | 37 +++++++++++++------ 2 files changed, 30 insertions(+), 12 deletions(-) create mode 100644 docs/changelog/92024.yaml diff --git a/docs/changelog/92024.yaml b/docs/changelog/92024.yaml new file mode 100644 index 000000000000..df0021a5cb14 --- /dev/null +++ b/docs/changelog/92024.yaml @@ -0,0 +1,5 @@ +pr: 92024 +summary: Clean up on exception while chunking XContent +area: Network +type: bug +issues: [] diff --git a/server/src/main/java/org/elasticsearch/rest/ChunkedRestResponseBody.java b/server/src/main/java/org/elasticsearch/rest/ChunkedRestResponseBody.java index f9c5aadc1f56..b6583a22b8aa 100644 --- a/server/src/main/java/org/elasticsearch/rest/ChunkedRestResponseBody.java +++ b/server/src/main/java/org/elasticsearch/rest/ChunkedRestResponseBody.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.recycler.Recycler; import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.core.IOUtils; +import org.elasticsearch.core.Releasables; import org.elasticsearch.core.Streams; import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentBuilder; @@ -92,20 +93,32 @@ public boolean isDone() { @Override public ReleasableBytesReference encodeChunk(int sizeHint, Recycler recycler) throws IOException { - final RecyclerBytesStreamOutput chunkStream = new RecyclerBytesStreamOutput(recycler); - assert this.target == null; - this.target = chunkStream; - while (serialization.hasNext()) { - serialization.next().toXContent(builder, params); - if (chunkStream.size() >= sizeHint) { - break; + try { + final RecyclerBytesStreamOutput chunkStream = new RecyclerBytesStreamOutput(recycler); + assert target == null; + target = chunkStream; + while (serialization.hasNext()) { + serialization.next().toXContent(builder, params); + if (chunkStream.size() >= sizeHint) { + break; + } + } + if (serialization.hasNext() == false) { + builder.close(); + } + final var result = new ReleasableBytesReference( + chunkStream.bytes(), + () -> Releasables.closeExpectNoException(chunkStream) + ); + target = null; + return result; + } finally { + if (target != null) { + assert false : "failure encoding chunk"; + IOUtils.closeWhileHandlingException(target); + target = null; } } - if (serialization.hasNext() == false) { - builder.close(); - } - this.target = null; - return new ReleasableBytesReference(chunkStream.bytes(), () -> IOUtils.closeWhileHandlingException(chunkStream)); } @Override From 2b8fde68905cd5a74d0e8be82173aed1ff165b79 Mon Sep 17 00:00:00 2001 From: Joe Gallo Date: Wed, 30 Nov 2022 13:34:01 -0500 Subject: [PATCH 123/919] Remove unused methods and classes from HLRC (#92030) --- .../client/RequestConverters.java | 337 ------------- .../client/RestHighLevelClient.java | 471 ------------------ .../client/RethrottleRequest.java | 66 --- .../client/cluster/ProxyModeInfo.java | 72 --- .../client/cluster/RemoteConnectionInfo.java | 127 ----- .../client/cluster/SniffModeInfo.java | 65 --- .../client/core/BroadcastResponse.java | 174 ------- .../client/core/CountRequest.java | 249 --------- .../client/core/CountResponse.java | 230 --------- .../client/core/GetSourceRequest.java | 115 ----- .../client/core/GetSourceResponse.java | 36 -- .../client/core/MainRequest.java | 13 - .../client/core/MultiTermVectorsRequest.java | 66 --- .../client/core/MultiTermVectorsResponse.java | 66 --- .../elasticsearch/client/core/PageParams.java | 89 ---- .../client/core/TermVectorsRequest.java | 284 ----------- .../client/core/TermVectorsResponse.java | 468 ----------------- 17 files changed, 2928 deletions(-) delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/RethrottleRequest.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/ProxyModeInfo.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteConnectionInfo.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/SniffModeInfo.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/core/BroadcastResponse.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/core/CountRequest.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/core/CountResponse.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/core/GetSourceRequest.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/core/GetSourceResponse.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/core/MainRequest.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/core/MultiTermVectorsRequest.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/core/MultiTermVectorsResponse.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/core/PageParams.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/core/TermVectorsRequest.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/core/TermVectorsResponse.java diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java index 6d2b06e1d1c3..fca1e5d29efa 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java @@ -9,9 +9,6 @@ package org.elasticsearch.client; import org.apache.http.HttpEntity; -import org.apache.http.client.methods.HttpDelete; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpHead; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.entity.ContentType; @@ -19,41 +16,26 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.action.DocWriteRequest; import org.elasticsearch.action.bulk.BulkRequest; -import org.elasticsearch.action.delete.DeleteRequest; -import org.elasticsearch.action.explain.ExplainRequest; -import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest; -import org.elasticsearch.action.get.GetRequest; -import org.elasticsearch.action.get.MultiGetRequest; import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.action.search.MultiSearchRequest; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchScrollRequest; import org.elasticsearch.action.support.ActiveShardCount; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; import org.elasticsearch.action.update.UpdateRequest; -import org.elasticsearch.client.core.CountRequest; -import org.elasticsearch.client.core.GetSourceRequest; -import org.elasticsearch.client.core.TermVectorsRequest; -import org.elasticsearch.client.internal.Requests; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.lucene.uid.Versions; -import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.TimeValue; import org.elasticsearch.index.VersionType; -import org.elasticsearch.index.reindex.AbstractBulkByScrollRequest; -import org.elasticsearch.index.reindex.ReindexRequest; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.rest.action.search.RestSearchAction; -import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.xcontent.DeprecationHandler; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.ToXContent; -import org.elasticsearch.xcontent.XContent; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentType; @@ -75,27 +57,6 @@ private RequestConverters() { // Contains only status utility methods } - static Request delete(DeleteRequest deleteRequest) { - String endpoint = endpoint(deleteRequest.index(), deleteRequest.id()); - Request request = new Request(HttpDelete.METHOD_NAME, endpoint); - - Params parameters = new Params(); - parameters.withRouting(deleteRequest.routing()); - parameters.withTimeout(deleteRequest.timeout()); - parameters.withVersion(deleteRequest.version()); - parameters.withVersionType(deleteRequest.versionType()); - parameters.withIfSeqNo(deleteRequest.ifSeqNo()); - parameters.withIfPrimaryTerm(deleteRequest.ifPrimaryTerm()); - parameters.withRefreshPolicy(deleteRequest.getRefreshPolicy()); - parameters.withWaitForActiveShards(deleteRequest.waitForActiveShards()); - request.addParameters(parameters.asMap()); - return request; - } - - static Request info() { - return new Request(HttpGet.METHOD_NAME, "/"); - } - static Request bulk(BulkRequest bulkRequest) throws IOException { Request request = new Request(HttpPost.METHOD_NAME, "/_bulk"); @@ -229,64 +190,6 @@ static Request bulk(BulkRequest bulkRequest) throws IOException { return request; } - static Request exists(GetRequest getRequest) { - return getStyleRequest(HttpHead.METHOD_NAME, getRequest); - } - - static Request get(GetRequest getRequest) { - return getStyleRequest(HttpGet.METHOD_NAME, getRequest); - } - - private static Request getStyleRequest(String method, GetRequest getRequest) { - Request request = new Request(method, endpoint(getRequest.index(), getRequest.id())); - - Params parameters = new Params(); - parameters.withPreference(getRequest.preference()); - parameters.withRouting(getRequest.routing()); - parameters.withRefresh(getRequest.refresh()); - parameters.withRealtime(getRequest.realtime()); - parameters.withStoredFields(getRequest.storedFields()); - parameters.withVersion(getRequest.version()); - parameters.withVersionType(getRequest.versionType()); - parameters.withFetchSourceContext(getRequest.fetchSourceContext()); - request.addParameters(parameters.asMap()); - return request; - } - - static Request sourceExists(GetSourceRequest getSourceRequest) { - return sourceRequest(getSourceRequest, HttpHead.METHOD_NAME); - } - - static Request getSource(GetSourceRequest getSourceRequest) { - return sourceRequest(getSourceRequest, HttpGet.METHOD_NAME); - } - - private static Request sourceRequest(GetSourceRequest getSourceRequest, String httpMethodName) { - Params parameters = new Params(); - parameters.withPreference(getSourceRequest.preference()); - parameters.withRouting(getSourceRequest.routing()); - parameters.withRefresh(getSourceRequest.refresh()); - parameters.withRealtime(getSourceRequest.realtime()); - parameters.withFetchSourceContext(getSourceRequest.fetchSourceContext()); - - String endpoint = endpoint(getSourceRequest.index(), "_source", getSourceRequest.id()); - Request request = new Request(httpMethodName, endpoint); - request.addParameters(parameters.asMap()); - return request; - } - - static Request multiGet(MultiGetRequest multiGetRequest) throws IOException { - Request request = new Request(HttpPost.METHOD_NAME, "/_mget"); - - Params parameters = new Params(); - parameters.withPreference(multiGetRequest.preference()); - parameters.withRealtime(multiGetRequest.realtime()); - parameters.withRefresh(multiGetRequest.refresh()); - request.addParameters(parameters.asMap()); - request.setEntity(createEntity(multiGetRequest, REQUEST_BODY_CONTENT_TYPE)); - return request; - } - static Request index(IndexRequest indexRequest) { String method = Strings.hasLength(indexRequest.id()) ? HttpPut.METHOD_NAME : HttpPost.METHOD_NAME; @@ -318,53 +221,6 @@ static Request index(IndexRequest indexRequest) { return request; } - static Request update(UpdateRequest updateRequest) throws IOException { - String endpoint = endpoint(updateRequest.index(), "_update", updateRequest.id()); - Request request = new Request(HttpPost.METHOD_NAME, endpoint); - - Params parameters = new Params(); - parameters.withRouting(updateRequest.routing()); - parameters.withTimeout(updateRequest.timeout()); - parameters.withRefreshPolicy(updateRequest.getRefreshPolicy()); - parameters.withWaitForActiveShards(updateRequest.waitForActiveShards()); - parameters.withDocAsUpsert(updateRequest.docAsUpsert()); - parameters.withFetchSourceContext(updateRequest.fetchSource()); - parameters.withRetryOnConflict(updateRequest.retryOnConflict()); - parameters.withVersion(updateRequest.version()); - parameters.withVersionType(updateRequest.versionType()); - parameters.withRequireAlias(updateRequest.isRequireAlias()); - - // The Java API allows update requests with different content types - // set for the partial document and the upsert document. This client - // only accepts update requests that have the same content types set - // for both doc and upsert. - XContentType xContentType = null; - if (updateRequest.doc() != null) { - xContentType = updateRequest.doc().getContentType(); - } - if (updateRequest.upsertRequest() != null) { - XContentType upsertContentType = updateRequest.upsertRequest().getContentType(); - if ((xContentType != null) && (xContentType != upsertContentType)) { - throw new IllegalStateException( - "Update request cannot have different content types for doc [" - + xContentType - + "]" - + " and upsert [" - + upsertContentType - + "] documents" - ); - } else { - xContentType = upsertContentType; - } - } - if (xContentType == null) { - xContentType = Requests.INDEX_CONTENT_TYPE; - } - request.addParameters(parameters.asMap()); - request.setEntity(createEntity(updateRequest, xContentType)); - return request; - } - /** * Convert a {@linkplain SearchRequest} into a {@linkplain Request}. * @param searchRequest the request to convert @@ -418,116 +274,6 @@ static Request searchScroll(SearchScrollRequest searchScrollRequest) throws IOEx return request; } - static Request multiSearch(MultiSearchRequest multiSearchRequest) throws IOException { - Request request = new Request(HttpPost.METHOD_NAME, "/_msearch"); - - Params params = new Params(); - params.putParam(RestSearchAction.TYPED_KEYS_PARAM, "true"); - if (multiSearchRequest.maxConcurrentSearchRequests() != MultiSearchRequest.MAX_CONCURRENT_SEARCH_REQUESTS_DEFAULT) { - params.putParam("max_concurrent_searches", Integer.toString(multiSearchRequest.maxConcurrentSearchRequests())); - } - - XContent xContent = REQUEST_BODY_CONTENT_TYPE.xContent(); - byte[] source = MultiSearchRequest.writeMultiLineFormat(multiSearchRequest, xContent); - request.addParameters(params.asMap()); - request.setEntity(new NByteArrayEntity(source, createContentType(xContent.type()))); - return request; - } - - static Request count(CountRequest countRequest) throws IOException { - Request request = new Request(HttpPost.METHOD_NAME, endpoint(countRequest.indices(), countRequest.types(), "_count")); - Params params = new Params(); - params.withRouting(countRequest.routing()); - params.withPreference(countRequest.preference()); - params.withIndicesOptions(countRequest.indicesOptions()); - if (countRequest.terminateAfter() != 0) { - params.withTerminateAfter(countRequest.terminateAfter()); - } - if (countRequest.minScore() != null) { - params.putParam("min_score", String.valueOf(countRequest.minScore())); - } - request.addParameters(params.asMap()); - request.setEntity(createEntity(countRequest, REQUEST_BODY_CONTENT_TYPE)); - return request; - } - - static Request explain(ExplainRequest explainRequest) throws IOException { - String endpoint = endpoint(explainRequest.index(), "_explain", explainRequest.id()); - Request request = new Request(HttpGet.METHOD_NAME, endpoint); - - Params params = new Params(); - params.withStoredFields(explainRequest.storedFields()); - params.withFetchSourceContext(explainRequest.fetchSourceContext()); - params.withRouting(explainRequest.routing()); - params.withPreference(explainRequest.preference()); - request.addParameters(params.asMap()); - request.setEntity(createEntity(explainRequest, REQUEST_BODY_CONTENT_TYPE)); - return request; - } - - static Request fieldCaps(FieldCapabilitiesRequest fieldCapabilitiesRequest) throws IOException { - String methodName = fieldCapabilitiesRequest.indexFilter() != null ? HttpPost.METHOD_NAME : HttpGet.METHOD_NAME; - Request request = new Request(methodName, endpoint(fieldCapabilitiesRequest.indices(), "_field_caps")); - - Params params = new Params(); - params.withFields(fieldCapabilitiesRequest.fields()); - if (FieldCapabilitiesRequest.DEFAULT_INDICES_OPTIONS.equals(fieldCapabilitiesRequest.indicesOptions()) == false) { - params.withIndicesOptions(fieldCapabilitiesRequest.indicesOptions()); - } - request.addParameters(params.asMap()); - if (fieldCapabilitiesRequest.indexFilter() != null) { - request.setEntity(createEntity(fieldCapabilitiesRequest, REQUEST_BODY_CONTENT_TYPE)); - } - return request; - } - - static Request reindex(ReindexRequest reindexRequest) throws IOException { - return prepareReindexRequest(reindexRequest, true); - } - - private static Request prepareReindexRequest(ReindexRequest reindexRequest, boolean waitForCompletion) throws IOException { - String endpoint = new EndpointBuilder().addPathPart("_reindex").build(); - Request request = new Request(HttpPost.METHOD_NAME, endpoint); - Params params = new Params().withWaitForCompletion(waitForCompletion) - .withRefresh(reindexRequest.isRefresh()) - .withTimeout(reindexRequest.getTimeout()) - .withWaitForActiveShards(reindexRequest.getWaitForActiveShards()) - .withRequestsPerSecond(reindexRequest.getRequestsPerSecond()) - .withSlices(reindexRequest.getSlices()) - .withRequireAlias(reindexRequest.getDestination().isRequireAlias()); - - if (reindexRequest.getScrollTime() != null) { - params.putParam("scroll", reindexRequest.getScrollTime()); - } - - request.addParameters(params.asMap()); - request.setEntity(createEntity(reindexRequest, REQUEST_BODY_CONTENT_TYPE)); - return request; - } - - static Request termVectors(TermVectorsRequest tvrequest) throws IOException { - String endpoint; - if (tvrequest.getType() != null) { - endpoint = new EndpointBuilder().addPathPart(tvrequest.getIndex(), tvrequest.getType(), tvrequest.getId()) - .addPathPartAsIs("_termvectors") - .build(); - } else { - endpoint = new EndpointBuilder().addPathPart(tvrequest.getIndex()) - .addPathPartAsIs("_termvectors") - .addPathPart(tvrequest.getId()) - .build(); - } - - Request request = new Request(HttpGet.METHOD_NAME, endpoint); - Params params = new Params(); - params.withRouting(tvrequest.getRouting()); - params.withPreference(tvrequest.getPreference()); - params.withRealtime(tvrequest.getRealtime()); - request.addParameters(params.asMap()); - request.setEntity(createEntity(tvrequest, REQUEST_BODY_CONTENT_TYPE)); - return request; - } - static HttpEntity createEntity(ToXContent toXContent, XContentType xContentType) throws IOException { return createEntity(toXContent, xContentType, ToXContent.EMPTY_PARAMS); } @@ -607,35 +353,6 @@ Map asMap() { return parameters; } - Params withDocAsUpsert(boolean docAsUpsert) { - if (docAsUpsert) { - return putParam("doc_as_upsert", Boolean.TRUE.toString()); - } - return this; - } - - Params withFetchSourceContext(FetchSourceContext fetchSourceContext) { - if (fetchSourceContext != null) { - if (fetchSourceContext.fetchSource() == false) { - putParam("_source", Boolean.FALSE.toString()); - } - if (CollectionUtils.isEmpty(fetchSourceContext.includes()) == false) { - putParam("_source_includes", String.join(",", fetchSourceContext.includes())); - } - if (CollectionUtils.isEmpty(fetchSourceContext.excludes()) == false) { - putParam("_source_excludes", String.join(",", fetchSourceContext.excludes())); - } - } - return this; - } - - Params withFields(String[] fields) { - if (CollectionUtils.isEmpty(fields) == false) { - return putParam("fields", String.join(",", fields)); - } - return this; - } - Params withPipeline(String pipeline) { return putParam("pipeline", pipeline); } @@ -664,20 +381,6 @@ Params withAllowPartialResults(boolean allowPartialSearchResults) { return putParam("allow_partial_search_results", Boolean.toString(allowPartialSearchResults)); } - Params withRealtime(boolean realtime) { - if (realtime == false) { - return putParam("realtime", Boolean.FALSE.toString()); - } - return this; - } - - Params withRefresh(boolean refresh) { - if (refresh) { - return withRefreshPolicy(RefreshPolicy.IMMEDIATE); - } - return this; - } - Params withRefreshPolicy(RefreshPolicy refreshPolicy) { if (refreshPolicy != RefreshPolicy.NONE) { return putParam("refresh", refreshPolicy.getValue()); @@ -685,46 +388,10 @@ Params withRefreshPolicy(RefreshPolicy refreshPolicy) { return this; } - Params withRequestsPerSecond(float requestsPerSecond) { - // the default in AbstractBulkByScrollRequest is Float.POSITIVE_INFINITY, - // but we don't want to add that to the URL parameters, instead we use -1 - if (Float.isFinite(requestsPerSecond)) { - return putParam(RethrottleRequest.REQUEST_PER_SECOND_PARAMETER, Float.toString(requestsPerSecond)); - } else { - return putParam(RethrottleRequest.REQUEST_PER_SECOND_PARAMETER, "-1"); - } - } - - Params withRetryOnConflict(int retryOnConflict) { - if (retryOnConflict > 0) { - return putParam("retry_on_conflict", String.valueOf(retryOnConflict)); - } - return this; - } - Params withRouting(String routing) { return putParam("routing", routing); } - Params withSlices(int slices) { - if (slices == 0) { - // translate to "auto" value in rest request so the receiving end doesn't throw error - return putParam("slices", AbstractBulkByScrollRequest.AUTO_SLICES_VALUE); - } - return putParam("slices", String.valueOf(slices)); - } - - Params withStoredFields(String[] storedFields) { - if (CollectionUtils.isEmpty(storedFields) == false) { - return putParam("stored_fields", String.join(",", storedFields)); - } - return this; - } - - Params withTerminateAfter(int terminateAfter) { - return putParam("terminate_after", String.valueOf(terminateAfter)); - } - Params withTimeout(TimeValue timeout) { return putParam("timeout", timeout); } @@ -803,10 +470,6 @@ Params withIgnoreUnavailable(boolean ignoreUnavailable) { putParam("ignore_unavailable", Boolean.toString(ignoreUnavailable)); return this; } - - Params withWaitForCompletion(Boolean waitForCompletion) { - return putParam("wait_for_completion", waitForCompletion.toString()); - } } /** diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java index 06ff0bdd72a1..9b8e92b65981 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java @@ -19,25 +19,11 @@ import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.bulk.BulkResponse; -import org.elasticsearch.action.delete.DeleteRequest; -import org.elasticsearch.action.delete.DeleteResponse; -import org.elasticsearch.action.explain.ExplainRequest; -import org.elasticsearch.action.explain.ExplainResponse; -import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest; -import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse; -import org.elasticsearch.action.get.GetRequest; -import org.elasticsearch.action.get.GetResponse; -import org.elasticsearch.action.get.MultiGetRequest; -import org.elasticsearch.action.get.MultiGetResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.index.IndexResponse; -import org.elasticsearch.action.search.MultiSearchRequest; -import org.elasticsearch.action.search.MultiSearchResponse; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchScrollRequest; -import org.elasticsearch.action.update.UpdateRequest; -import org.elasticsearch.action.update.UpdateResponse; import org.elasticsearch.aggregations.bucket.adjacency.AdjacencyMatrixAggregationBuilder; import org.elasticsearch.aggregations.bucket.adjacency.ParsedAdjacencyMatrix; import org.elasticsearch.aggregations.bucket.histogram.AutoDateHistogramAggregationBuilder; @@ -49,21 +35,12 @@ import org.elasticsearch.client.analytics.ParsedTopMetrics; import org.elasticsearch.client.analytics.StringStatsAggregationBuilder; import org.elasticsearch.client.analytics.TopMetricsAggregationBuilder; -import org.elasticsearch.client.core.CountRequest; -import org.elasticsearch.client.core.CountResponse; -import org.elasticsearch.client.core.GetSourceRequest; -import org.elasticsearch.client.core.GetSourceResponse; -import org.elasticsearch.client.core.MainRequest; import org.elasticsearch.client.core.MainResponse; -import org.elasticsearch.client.core.TermVectorsRequest; -import org.elasticsearch.client.core.TermVectorsResponse; import org.elasticsearch.common.Strings; import org.elasticsearch.common.util.concurrent.FutureUtils; import org.elasticsearch.common.util.concurrent.ListenableFuture; import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.core.CheckedFunction; -import org.elasticsearch.index.reindex.BulkByScrollResponse; -import org.elasticsearch.index.reindex.ReindexRequest; import org.elasticsearch.plugins.spi.NamedXContentProvider; import org.elasticsearch.rest.RestResponse; import org.elasticsearch.rest.RestStatus; @@ -196,7 +173,6 @@ import java.util.stream.Stream; import static java.util.Collections.emptySet; -import static java.util.Collections.singleton; import static java.util.stream.Collectors.toList; /** @@ -358,187 +334,6 @@ public final Cancellable bulkAsync(BulkRequest bulkRequest, RequestOptions optio ); } - /** - * Executes a reindex request. - * See Reindex API on elastic.co - * @param reindexRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return the response - */ - public final BulkByScrollResponse reindex(ReindexRequest reindexRequest, RequestOptions options) throws IOException { - return performRequestAndParseEntity( - reindexRequest, - RequestConverters::reindex, - options, - BulkByScrollResponse::fromXContent, - singleton(409) - ); - } - - /** - * Get the cluster info otherwise provided when sending an HTTP request to '/' - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return the response - */ - public final MainResponse info(RequestOptions options) throws IOException { - return performRequestAndParseEntity( - new MainRequest(), - (request) -> RequestConverters.info(), - options, - MainResponse::fromXContent, - emptySet() - ); - } - - /** - * Retrieves a document by id using the Get API. - * See Get API on elastic.co - * @param getRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return the response - */ - public final GetResponse get(GetRequest getRequest, RequestOptions options) throws IOException { - return performRequestAndParseEntity(getRequest, RequestConverters::get, options, GetResponse::fromXContent, singleton(404)); - } - - /** - * Retrieves multiple documents by id using the Multi Get API. - * See Multi Get API on elastic.co - * @param multiGetRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return the response - * @deprecated use {@link #mget(MultiGetRequest, RequestOptions)} instead - */ - @Deprecated - public final MultiGetResponse multiGet(MultiGetRequest multiGetRequest, RequestOptions options) throws IOException { - return mget(multiGetRequest, options); - } - - /** - * Retrieves multiple documents by id using the Multi Get API. - * See Multi Get API on elastic.co - * @param multiGetRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return the response - */ - public final MultiGetResponse mget(MultiGetRequest multiGetRequest, RequestOptions options) throws IOException { - return performRequestAndParseEntity( - multiGetRequest, - RequestConverters::multiGet, - options, - MultiGetResponse::fromXContent, - singleton(404) - ); - } - - /** - * Asynchronously retrieves multiple documents by id using the Multi Get API. - * See Multi Get API on elastic.co - * @param multiGetRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @param listener the listener to be notified upon request completion - * @deprecated use {@link #mgetAsync(MultiGetRequest, RequestOptions, ActionListener)} instead - * @return cancellable that may be used to cancel the request - */ - @Deprecated - public final Cancellable multiGetAsync( - MultiGetRequest multiGetRequest, - RequestOptions options, - ActionListener listener - ) { - return mgetAsync(multiGetRequest, options, listener); - } - - /** - * Asynchronously retrieves multiple documents by id using the Multi Get API. - * See Multi Get API on elastic.co - * @param multiGetRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @param listener the listener to be notified upon request completion - * @return cancellable that may be used to cancel the request - */ - public final Cancellable mgetAsync(MultiGetRequest multiGetRequest, RequestOptions options, ActionListener listener) { - return performRequestAsyncAndParseEntity( - multiGetRequest, - RequestConverters::multiGet, - options, - MultiGetResponse::fromXContent, - listener, - singleton(404) - ); - } - - /** - * Checks for the existence of a document. Returns true if it exists, false otherwise. - * See Get API on elastic.co - * @param getRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return true if the document exists, false otherwise - */ - public final boolean exists(GetRequest getRequest, RequestOptions options) throws IOException { - return performRequest(getRequest, RequestConverters::exists, options, RestHighLevelClient::convertExistsResponse, emptySet()); - } - - /** - * Checks for the existence of a document with a "_source" field. Returns true if it exists, false otherwise. - * See Source exists API - * on elastic.co - * @param getRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return true if the document and _source field exists, false otherwise - */ - @Deprecated - public boolean existsSource(GetRequest getRequest, RequestOptions options) throws IOException { - GetSourceRequest getSourceRequest = GetSourceRequest.from(getRequest); - return performRequest( - getSourceRequest, - RequestConverters::sourceExists, - options, - RestHighLevelClient::convertExistsResponse, - emptySet() - ); - } - - /** - * Asynchronously checks for the existence of a document with a "_source" field. Returns true if it exists, false otherwise. - * See Source exists API - * on elastic.co - * @param getRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @param listener the listener to be notified upon request completion - * @return cancellable that may be used to cancel the request - */ - @Deprecated - public final Cancellable existsSourceAsync(GetRequest getRequest, RequestOptions options, ActionListener listener) { - GetSourceRequest getSourceRequest = GetSourceRequest.from(getRequest); - return performRequestAsync( - getSourceRequest, - RequestConverters::sourceExists, - options, - RestHighLevelClient::convertExistsResponse, - listener, - emptySet() - ); - } - - /** - * Retrieves the source field only of a document using GetSource API. - * See Get Source API - * on elastic.co - * @param getSourceRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return the response - */ - public GetSourceResponse getSource(GetSourceRequest getSourceRequest, RequestOptions options) throws IOException { - return performRequestAndParseEntity( - getSourceRequest, - RequestConverters::getSource, - options, - GetSourceResponse::fromXContent, - emptySet() - ); - } - /** * Index a document using the Index API. * See Index API on elastic.co @@ -550,45 +345,6 @@ public final IndexResponse index(IndexRequest indexRequest, RequestOptions optio return performRequestAndParseEntity(indexRequest, RequestConverters::index, options, IndexResponse::fromXContent, emptySet()); } - /** - * Executes a count request using the Count API. - * See Count API on elastic.co - * @param countRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return the response - */ - public final CountResponse count(CountRequest countRequest, RequestOptions options) throws IOException { - return performRequestAndParseEntity(countRequest, RequestConverters::count, options, CountResponse::fromXContent, emptySet()); - } - - /** - * Updates a document using the Update API. - * See Update API on elastic.co - * @param updateRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return the response - */ - public final UpdateResponse update(UpdateRequest updateRequest, RequestOptions options) throws IOException { - return performRequestAndParseEntity(updateRequest, RequestConverters::update, options, UpdateResponse::fromXContent, emptySet()); - } - - /** - * Deletes a document by id using the Delete API. - * See Delete API on elastic.co - * @param deleteRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return the response - */ - public final DeleteResponse delete(DeleteRequest deleteRequest, RequestOptions options) throws IOException { - return performRequestAndParseEntity( - deleteRequest, - RequestConverters::delete, - options, - DeleteResponse::fromXContent, - singleton(404) - ); - } - /** * Executes a search request using the Search API. * See Search API on elastic.co @@ -625,96 +381,6 @@ public final Cancellable searchAsync(SearchRequest searchRequest, RequestOptions ); } - /** - * Executes a multi search using the msearch API. - * See Multi search API on - * elastic.co - * @param multiSearchRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return the response - * @deprecated use {@link #msearch(MultiSearchRequest, RequestOptions)} instead - */ - @Deprecated - public final MultiSearchResponse multiSearch(MultiSearchRequest multiSearchRequest, RequestOptions options) throws IOException { - return msearch(multiSearchRequest, options); - } - - /** - * Executes a multi search using the msearch API. - * See Multi search API on - * elastic.co - * @param multiSearchRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return the response - */ - public final MultiSearchResponse msearch(MultiSearchRequest multiSearchRequest, RequestOptions options) throws IOException { - return performRequestAndParseEntity( - multiSearchRequest, - RequestConverters::multiSearch, - options, - MultiSearchResponse::fromXContext, - emptySet() - ); - } - - /** - * Asynchronously executes a multi search using the msearch API. - * See Multi search API on - * elastic.co - * @param searchRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @param listener the listener to be notified upon request completion - * @deprecated use {@link #msearchAsync(MultiSearchRequest, RequestOptions, ActionListener)} instead - * @return cancellable that may be used to cancel the request - */ - @Deprecated - public final Cancellable multiSearchAsync( - MultiSearchRequest searchRequest, - RequestOptions options, - ActionListener listener - ) { - return msearchAsync(searchRequest, options, listener); - } - - /** - * Asynchronously executes a multi search using the msearch API. - * See Multi search API on - * elastic.co - * @param searchRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @param listener the listener to be notified upon request completion - * @return cancellable that may be used to cancel the request - */ - public final Cancellable msearchAsync( - MultiSearchRequest searchRequest, - RequestOptions options, - ActionListener listener - ) { - return performRequestAsyncAndParseEntity( - searchRequest, - RequestConverters::multiSearch, - options, - MultiSearchResponse::fromXContext, - listener, - emptySet() - ); - } - - /** - * Executes a search using the Search Scroll API. - * See Search - * Scroll API on elastic.co - * @param searchScrollRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return the response - * @deprecated use {@link #scroll(SearchScrollRequest, RequestOptions)} instead - */ - @Deprecated - public final SearchResponse searchScroll(SearchScrollRequest searchScrollRequest, RequestOptions options) throws IOException { - return scroll(searchScrollRequest, options); - } - /** * Executes a search using the Search Scroll API. * See Search - * Scroll API on elastic.co - * @param searchScrollRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @param listener the listener to be notified upon request completion - * @deprecated use {@link #scrollAsync(SearchScrollRequest, RequestOptions, ActionListener)} instead - * @return cancellable that may be used to cancel the request - */ - @Deprecated - public final Cancellable searchScrollAsync( - SearchScrollRequest searchScrollRequest, - RequestOptions options, - ActionListener listener - ) { - return scrollAsync(searchScrollRequest, options, listener); - } - - /** - * Asynchronously executes a search using the Search Scroll API. - * See Search - * Scroll API on elastic.co - * @param searchScrollRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @param listener the listener to be notified upon request completion - * @return cancellable that may be used to cancel the request - */ - public final Cancellable scrollAsync( - SearchScrollRequest searchScrollRequest, - RequestOptions options, - ActionListener listener - ) { - return performRequestAsyncAndParseEntity( - searchScrollRequest, - RequestConverters::searchScroll, - options, - SearchResponse::fromXContent, - listener, - emptySet() - ); - } - - /** - * Executes a request using the Explain API. - * See Explain API on elastic.co - * @param explainRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return the response - */ - public final ExplainResponse explain(ExplainRequest explainRequest, RequestOptions options) throws IOException { - return performRequest(explainRequest, RequestConverters::explain, options, response -> { - CheckedFunction entityParser = parser -> ExplainResponse.fromXContent( - parser, - convertExistsResponse(response) - ); - return parseEntity(response.getEntity(), entityParser); - }, singleton(404)); - } - - /** - * Calls the Term Vectors API - * - * See Term Vectors API on - * elastic.co - * - * @param request the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - */ - public final TermVectorsResponse termvectors(TermVectorsRequest request, RequestOptions options) throws IOException { - return performRequestAndParseEntity( - request, - RequestConverters::termVectors, - options, - TermVectorsResponse::fromXContent, - emptySet() - ); - } - - /** - * Executes a request using the Field Capabilities API. - * See Field Capabilities API - * on elastic.co. - * @param fieldCapabilitiesRequest the request - * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return the response - */ - public final FieldCapabilitiesResponse fieldCaps(FieldCapabilitiesRequest fieldCapabilitiesRequest, RequestOptions options) - throws IOException { - return performRequestAndParseEntity( - fieldCapabilitiesRequest, - RequestConverters::fieldCaps, - options, - FieldCapabilitiesResponse::fromXContent, - emptySet() - ); - } - /** * @deprecated If creating a new HLRC ReST API call, consider creating new actions instead of reusing server actions. The Validation * layer has been added to the ReST client, and requests should extend {@link Validatable} instead of {@link ActionRequest}. @@ -849,19 +415,6 @@ protected final Resp performRequestAndParseEnt return performRequest(request, requestConverter, options, response -> parseEntity(response.getEntity(), entityParser), ignores); } - /** - * Defines a helper method for performing a request and then parsing the returned entity using the provided entityParser. - */ - protected final Resp performRequestAndParseEntity( - Req request, - CheckedFunction requestConverter, - RequestOptions options, - CheckedFunction entityParser, - Set ignores - ) throws IOException { - return performRequest(request, requestConverter, options, response -> parseEntity(response.getEntity(), entityParser), ignores); - } - /** * @deprecated If creating a new HLRC ReST API call, consider creating new actions instead of reusing server actions. The Validation * layer has been added to the ReST client, and requests should extend {@link Validatable} instead of {@link ActionRequest}. @@ -981,26 +534,6 @@ protected final Cancellable performRequestAsyn return internalPerformRequestAsync(request, requestConverter, options, responseConverter, listener, ignores); } - /** - * Defines a helper method for asynchronously performing a request. - * @return Cancellable instance that may be used to cancel the request - */ - protected final Cancellable performRequestAsync( - Req request, - CheckedFunction requestConverter, - RequestOptions options, - CheckedFunction responseConverter, - ActionListener listener, - Set ignores - ) { - Optional validationException = request.validate(); - if (validationException != null && validationException.isPresent()) { - listener.onFailure(validationException.get()); - return Cancellable.NO_OP; - } - return internalPerformRequestAsync(request, requestConverter, options, responseConverter, listener, ignores); - } - /** * Provides common functionality for asynchronously performing a request. * @return Cancellable instance that may be used to cancel the request @@ -1110,10 +643,6 @@ protected final Resp parseEntity(final HttpEntity entity, final CheckedFu } } - protected static boolean convertExistsResponse(Response response) { - return response.getStatusLine().getStatusCode() == 200; - } - private enum EntityType { JSON() { @Override diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RethrottleRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RethrottleRequest.java deleted file mode 100644 index a518d74b14c2..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RethrottleRequest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client; - -import org.elasticsearch.tasks.TaskId; - -import java.util.Objects; - -/** - * A request changing throttling of a task. - */ -public class RethrottleRequest implements Validatable { - - static final String REQUEST_PER_SECOND_PARAMETER = "requests_per_second"; - - private final TaskId taskId; - private final float requestsPerSecond; - - /** - * Create a new {@link RethrottleRequest} which disables any throttling for the given taskId. - * @param taskId the task for which throttling will be disabled - */ - public RethrottleRequest(TaskId taskId) { - this.taskId = taskId; - this.requestsPerSecond = Float.POSITIVE_INFINITY; - } - - /** - * Create a new {@link RethrottleRequest} which changes the throttling for the given taskId. - * @param taskId the task that throttling changes will be applied to - * @param requestsPerSecond the number of requests per second that the task should perform. This needs to be a positive value. - */ - public RethrottleRequest(TaskId taskId, float requestsPerSecond) { - Objects.requireNonNull(taskId, "taskId cannot be null"); - if (requestsPerSecond <= 0) { - throw new IllegalArgumentException("requestsPerSecond needs to be positive value but was [" + requestsPerSecond + "]"); - } - this.taskId = taskId; - this.requestsPerSecond = requestsPerSecond; - } - - /** - * @return the task Id - */ - public TaskId getTaskId() { - return taskId; - } - - /** - * @return the requests per seconds value - */ - public float getRequestsPerSecond() { - return requestsPerSecond; - } - - @Override - public String toString() { - return "RethrottleRequest: taskID = " + taskId + "; reqestsPerSecond = " + requestsPerSecond; - } -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/ProxyModeInfo.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/ProxyModeInfo.java deleted file mode 100644 index 0e55c232cf3f..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/ProxyModeInfo.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client.cluster; - -import java.util.Objects; - -public class ProxyModeInfo implements RemoteConnectionInfo.ModeInfo { - static final String NAME = "proxy"; - static final String PROXY_ADDRESS = "proxy_address"; - static final String SERVER_NAME = "server_name"; - static final String NUM_PROXY_SOCKETS_CONNECTED = "num_proxy_sockets_connected"; - static final String MAX_PROXY_SOCKET_CONNECTIONS = "max_proxy_socket_connections"; - private final String address; - private final String serverName; - private final int maxSocketConnections; - private final int numSocketsConnected; - - ProxyModeInfo(String address, String serverName, int maxSocketConnections, int numSocketsConnected) { - this.address = address; - this.serverName = serverName; - this.maxSocketConnections = maxSocketConnections; - this.numSocketsConnected = numSocketsConnected; - } - - @Override - public boolean isConnected() { - return numSocketsConnected > 0; - } - - @Override - public String modeName() { - return NAME; - } - - public String getAddress() { - return address; - } - - public String getServerName() { - return serverName; - } - - public int getMaxSocketConnections() { - return maxSocketConnections; - } - - public int getNumSocketsConnected() { - return numSocketsConnected; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ProxyModeInfo otherProxy = (ProxyModeInfo) o; - return maxSocketConnections == otherProxy.maxSocketConnections - && numSocketsConnected == otherProxy.numSocketsConnected - && Objects.equals(address, otherProxy.address) - && Objects.equals(serverName, otherProxy.serverName); - } - - @Override - public int hashCode() { - return Objects.hash(address, serverName, maxSocketConnections, numSocketsConnected); - } -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteConnectionInfo.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteConnectionInfo.java deleted file mode 100644 index f5069d7771d7..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/RemoteConnectionInfo.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client.cluster; - -import org.elasticsearch.xcontent.ConstructingObjectParser; -import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.XContentParser; - -import java.io.IOException; -import java.util.List; -import java.util.Objects; - -import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; -import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; - -/** - * This class encapsulates all remote cluster information to be rendered on - * {@code _remote/info} requests. - */ -public final class RemoteConnectionInfo { - private static final String CONNECTED = "connected"; - private static final String MODE = "mode"; - private static final String INITIAL_CONNECT_TIMEOUT = "initial_connect_timeout"; - private static final String SKIP_UNAVAILABLE = "skip_unavailable"; - - @SuppressWarnings("unchecked") - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - "RemoteConnectionInfoObjectParser", - false, - (args, clusterAlias) -> { - String mode = (String) args[1]; - ModeInfo modeInfo; - if (mode.equals(ProxyModeInfo.NAME)) { - modeInfo = new ProxyModeInfo((String) args[4], (String) args[5], (int) args[6], (int) args[7]); - } else if (mode.equals(SniffModeInfo.NAME)) { - modeInfo = new SniffModeInfo((List) args[8], (int) args[9], (int) args[10]); - } else { - throw new IllegalArgumentException("mode cannot be " + mode); - } - return new RemoteConnectionInfo(clusterAlias, modeInfo, (String) args[2], (boolean) args[3]); - } - ); - - static { - PARSER.declareBoolean(constructorArg(), new ParseField(CONNECTED)); - PARSER.declareString(constructorArg(), new ParseField(MODE)); - PARSER.declareString(constructorArg(), new ParseField(INITIAL_CONNECT_TIMEOUT)); - PARSER.declareBoolean(constructorArg(), new ParseField(SKIP_UNAVAILABLE)); - - PARSER.declareString(optionalConstructorArg(), new ParseField(ProxyModeInfo.PROXY_ADDRESS)); - PARSER.declareString(optionalConstructorArg(), new ParseField(ProxyModeInfo.SERVER_NAME)); - PARSER.declareInt(optionalConstructorArg(), new ParseField(ProxyModeInfo.MAX_PROXY_SOCKET_CONNECTIONS)); - PARSER.declareInt(optionalConstructorArg(), new ParseField(ProxyModeInfo.NUM_PROXY_SOCKETS_CONNECTED)); - - PARSER.declareStringArray(optionalConstructorArg(), new ParseField(SniffModeInfo.SEEDS)); - PARSER.declareInt(optionalConstructorArg(), new ParseField(SniffModeInfo.MAX_CONNECTIONS_PER_CLUSTER)); - PARSER.declareInt(optionalConstructorArg(), new ParseField(SniffModeInfo.NUM_NODES_CONNECTED)); - } - - private final ModeInfo modeInfo; - // TODO: deprecate and remove this field in favor of initialConnectionTimeout field that is of type TimeValue. - // When rest api versioning exists then change org.elasticsearch.transport.RemoteConnectionInfo to properly serialize - // the initialConnectionTimeout field so that we can properly parse initialConnectionTimeout as TimeValue - private final String initialConnectionTimeoutString; - private final String clusterAlias; - private final boolean skipUnavailable; - - RemoteConnectionInfo(String clusterAlias, ModeInfo modeInfo, String initialConnectionTimeoutString, boolean skipUnavailable) { - this.clusterAlias = clusterAlias; - this.modeInfo = modeInfo; - this.initialConnectionTimeoutString = initialConnectionTimeoutString; - this.skipUnavailable = skipUnavailable; - } - - public boolean isConnected() { - return modeInfo.isConnected(); - } - - public String getClusterAlias() { - return clusterAlias; - } - - public ModeInfo getModeInfo() { - return modeInfo; - } - - public String getInitialConnectionTimeoutString() { - return initialConnectionTimeoutString; - } - - public boolean isSkipUnavailable() { - return skipUnavailable; - } - - public static RemoteConnectionInfo fromXContent(XContentParser parser, String clusterAlias) throws IOException { - return PARSER.parse(parser, clusterAlias); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - RemoteConnectionInfo that = (RemoteConnectionInfo) o; - return skipUnavailable == that.skipUnavailable - && Objects.equals(modeInfo, that.modeInfo) - && Objects.equals(initialConnectionTimeoutString, that.initialConnectionTimeoutString) - && Objects.equals(clusterAlias, that.clusterAlias); - } - - @Override - public int hashCode() { - return Objects.hash(modeInfo, initialConnectionTimeoutString, clusterAlias, skipUnavailable); - } - - public interface ModeInfo { - - boolean isConnected(); - - String modeName(); - } -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/SniffModeInfo.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/SniffModeInfo.java deleted file mode 100644 index e08509dd14b6..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/cluster/SniffModeInfo.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client.cluster; - -import java.util.List; -import java.util.Objects; - -public class SniffModeInfo implements RemoteConnectionInfo.ModeInfo { - public static final String NAME = "sniff"; - static final String SEEDS = "seeds"; - static final String NUM_NODES_CONNECTED = "num_nodes_connected"; - static final String MAX_CONNECTIONS_PER_CLUSTER = "max_connections_per_cluster"; - final List seedNodes; - final int maxConnectionsPerCluster; - final int numNodesConnected; - - SniffModeInfo(List seedNodes, int maxConnectionsPerCluster, int numNodesConnected) { - this.seedNodes = seedNodes; - this.maxConnectionsPerCluster = maxConnectionsPerCluster; - this.numNodesConnected = numNodesConnected; - } - - @Override - public boolean isConnected() { - return numNodesConnected > 0; - } - - @Override - public String modeName() { - return NAME; - } - - public List getSeedNodes() { - return seedNodes; - } - - public int getMaxConnectionsPerCluster() { - return maxConnectionsPerCluster; - } - - public int getNumNodesConnected() { - return numNodesConnected; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - SniffModeInfo sniff = (SniffModeInfo) o; - return maxConnectionsPerCluster == sniff.maxConnectionsPerCluster - && numNodesConnected == sniff.numNodesConnected - && Objects.equals(seedNodes, sniff.seedNodes); - } - - @Override - public int hashCode() { - return Objects.hash(seedNodes, maxConnectionsPerCluster, numNodesConnected); - } -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/BroadcastResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/BroadcastResponse.java deleted file mode 100644 index ef3b35a0896d..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/BroadcastResponse.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client.core; - -import org.elasticsearch.action.support.DefaultShardOperationFailedException; -import org.elasticsearch.xcontent.ConstructingObjectParser; -import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.XContentParser; - -import java.io.IOException; -import java.util.Collection; -import java.util.Collections; -import java.util.Objects; - -/** - * Represents a response to a request that is broadcast to a collection of shards. - */ -public class BroadcastResponse { - - private final Shards shards; - - /** - * Represents the shard-level summary of the response execution. - * - * @return the shard-level response summary - */ - public Shards shards() { - return shards; - } - - protected BroadcastResponse(final Shards shards) { - this.shards = Objects.requireNonNull(shards); - } - - private static final ParseField SHARDS_FIELD = new ParseField("_shards"); - - static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - "broadcast_response", - a -> new BroadcastResponse((Shards) a[0]) - ); - - static { - declareShardsField(PARSER); - } - - /** - * Parses a broadcast response. - * - * @param parser the parser - * @return a broadcast response parsed from the specified parser - * @throws IOException if an I/O exception occurs parsing the response - */ - public static BroadcastResponse fromXContent(final XContentParser parser) throws IOException { - return PARSER.parse(parser, null); - } - - protected static void declareShardsField(ConstructingObjectParser parser) { - parser.declareObject(ConstructingObjectParser.constructorArg(), Shards.SHARDS_PARSER, SHARDS_FIELD); - } - - /** - * Represents the results of a collection of shards on which a request was executed against. - */ - public static class Shards { - - private final int total; - - /** - * The total number of shards on which a request was executed against. - * - * @return the total number of shards - */ - public int total() { - return total; - } - - private final int successful; - - /** - * The number of successful shards on which a request was executed against. - * - * @return the number of successful shards - */ - public int successful() { - return successful; - } - - private final int skipped; - - /** - * The number of shards skipped by the request. - * - * @return the number of skipped shards - */ - public int skipped() { - return skipped; - } - - private final int failed; - - /** - * The number of shards on which a request failed to be executed against. - * - * @return the number of failed shards - */ - public int failed() { - return failed; - } - - private final Collection failures; - - /** - * The failures corresponding to the shards on which a request failed to be executed against. Note that the number of failures might - * not match {@link #failed()} as some responses group together shard failures. - * - * @return the failures - */ - public Collection failures() { - return failures; - } - - Shards( - final int total, - final int successful, - final int skipped, - final int failed, - final Collection failures - ) { - this.total = total; - this.successful = successful; - this.skipped = skipped; - this.failed = failed; - this.failures = Collections.unmodifiableCollection(Objects.requireNonNull(failures)); - } - - private static final ParseField TOTAL_FIELD = new ParseField("total"); - private static final ParseField SUCCESSFUL_FIELD = new ParseField("successful"); - private static final ParseField SKIPPED_FIELD = new ParseField("skipped"); - private static final ParseField FAILED_FIELD = new ParseField("failed"); - private static final ParseField FAILURES_FIELD = new ParseField("failures"); - - @SuppressWarnings("unchecked") - static final ConstructingObjectParser SHARDS_PARSER = new ConstructingObjectParser<>( - "shards", - a -> new Shards( - (int) a[0], // total - (int) a[1], // successful - a[2] == null ? 0 : (int) a[2], // skipped - (int) a[3], // failed - a[4] == null ? Collections.emptyList() : (Collection) a[4] - ) - ); // failures - - static { - SHARDS_PARSER.declareInt(ConstructingObjectParser.constructorArg(), TOTAL_FIELD); - SHARDS_PARSER.declareInt(ConstructingObjectParser.constructorArg(), SUCCESSFUL_FIELD); - SHARDS_PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), SKIPPED_FIELD); - SHARDS_PARSER.declareInt(ConstructingObjectParser.constructorArg(), FAILED_FIELD); - SHARDS_PARSER.declareObjectArray( - ConstructingObjectParser.optionalConstructorArg(), - DefaultShardOperationFailedException.PARSER, - FAILURES_FIELD - ); - } - - } - -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/CountRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/CountRequest.java deleted file mode 100644 index 0899eb03311d..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/CountRequest.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client.core; - -import org.elasticsearch.action.support.IndicesOptions; -import org.elasticsearch.client.Validatable; -import org.elasticsearch.common.Strings; -import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.search.builder.SearchSourceBuilder; -import org.elasticsearch.search.internal.SearchContext; -import org.elasticsearch.xcontent.ToXContentObject; -import org.elasticsearch.xcontent.XContentBuilder; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Objects; - -/** - * Encapsulates a request to _count API against one, several or all indices. - */ -public final class CountRequest implements Validatable, ToXContentObject { - - private String[] indices = Strings.EMPTY_ARRAY; - private String[] types = Strings.EMPTY_ARRAY; - private String routing; - private String preference; - private QueryBuilder query; - private IndicesOptions indicesOptions; - private int terminateAfter = SearchContext.DEFAULT_TERMINATE_AFTER; - private Float minScore; - - public CountRequest() {} - - /** - * Constructs a new count request against the indices. No indices provided here means that count will execute on all indices. - */ - public CountRequest(String... indices) { - indices(indices); - } - - /** - * Constructs a new search request against the provided indices with the given search source. - * - * @deprecated The count api only supports a query. Use {@link #CountRequest(String[], QueryBuilder)} instead. - */ - @Deprecated - public CountRequest(String[] indices, SearchSourceBuilder searchSourceBuilder) { - indices(indices); - this.query = Objects.requireNonNull(searchSourceBuilder, "source must not be null").query(); - } - - /** - * Constructs a new search request against the provided indices with the given query. - */ - public CountRequest(String[] indices, QueryBuilder query) { - indices(indices); - this.query = Objects.requireNonNull(query, "query must not be null"); - ; - } - - /** - * Sets the indices the count will be executed on. - */ - public CountRequest indices(String... indices) { - Objects.requireNonNull(indices, "indices must not be null"); - for (String index : indices) { - Objects.requireNonNull(index, "index must not be null"); - } - this.indices = indices; - return this; - } - - /** - * The source of the count request. - * - * @deprecated The count api only supports a query. Use {@link #query(QueryBuilder)} instead. - */ - @Deprecated - public CountRequest source(SearchSourceBuilder searchSourceBuilder) { - this.query = Objects.requireNonNull(searchSourceBuilder, "source must not be null").query(); - return this; - } - - /** - * Sets the query to execute for this count request. - */ - public CountRequest query(QueryBuilder query) { - this.query = Objects.requireNonNull(query, "query must not be null"); - return this; - } - - /** - * The document types to execute the count against. Defaults to be executed against all types. - * - * @deprecated Types are in the process of being removed. Instead of using a type, prefer to - * filter on a field on the document. - */ - @Deprecated - public CountRequest types(String... types) { - Objects.requireNonNull(types, "types must not be null"); - for (String type : types) { - Objects.requireNonNull(type, "type must not be null"); - } - this.types = types; - return this; - } - - /** - * The routing values to control the shards that the search will be executed on. - */ - public CountRequest routing(String routing) { - this.routing = routing; - return this; - } - - /** - * A comma separated list of routing values to control the shards the count will be executed on. - */ - public CountRequest routing(String... routings) { - this.routing = Strings.arrayToCommaDelimitedString(routings); - return this; - } - - /** - * Returns the indices options used to resolve indices. They tell for instance whether a single index is accepted, whether an empty - * array will be converted to _all, and how wildcards will be expanded if needed. - * - * @see org.elasticsearch.action.support.IndicesOptions - */ - public CountRequest indicesOptions(IndicesOptions indicesOptions) { - this.indicesOptions = Objects.requireNonNull(indicesOptions, "indicesOptions must not be null"); - return this; - } - - /** - * Sets the preference to execute the count. Defaults to randomize across shards. Can be set to {@code _local} to prefer local shards - * or a custom value, which guarantees that the same order will be used across different requests. - */ - public CountRequest preference(String preference) { - this.preference = preference; - return this; - } - - public IndicesOptions indicesOptions() { - return this.indicesOptions; - } - - public String routing() { - return this.routing; - } - - public String preference() { - return this.preference; - } - - public String[] indices() { - return Arrays.copyOf(this.indices, this.indices.length); - } - - public Float minScore() { - return minScore; - } - - public CountRequest minScore(Float minScore) { - this.minScore = minScore; - return this; - } - - public int terminateAfter() { - return this.terminateAfter; - } - - public CountRequest terminateAfter(int terminateAfter) { - if (terminateAfter < 0) { - throw new IllegalArgumentException("terminateAfter must be > 0"); - } - this.terminateAfter = terminateAfter; - return this; - } - - /** - * @deprecated Types are in the process of being removed. Instead of using a type, prefer to - * filter on a field on the document. - */ - @Deprecated - public String[] types() { - return Arrays.copyOf(this.types, this.types.length); - } - - /** - * @return the source builder - * @deprecated The count api only supports a query. Use {@link #query()} instead. - */ - @Deprecated - public SearchSourceBuilder source() { - return new SearchSourceBuilder().query(query); - } - - /** - * @return The provided query to execute with the count request or - * null if no query was provided. - */ - public QueryBuilder query() { - return query; - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - if (query != null) { - builder.field("query", query); - } - builder.endObject(); - return builder; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - CountRequest that = (CountRequest) o; - return Objects.equals(indicesOptions, that.indicesOptions) - && Arrays.equals(indices, that.indices) - && Arrays.equals(types, that.types) - && Objects.equals(routing, that.routing) - && Objects.equals(preference, that.preference) - && Objects.equals(terminateAfter, that.terminateAfter) - && Objects.equals(minScore, that.minScore) - && Objects.equals(query, that.query); - } - - @Override - public int hashCode() { - int result = Objects.hash(indicesOptions, routing, preference, terminateAfter, minScore, query); - result = 31 * result + Arrays.hashCode(indices); - result = 31 * result + Arrays.hashCode(types); - return result; - } -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/CountResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/CountResponse.java deleted file mode 100644 index c19245c4d09f..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/CountResponse.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client.core; - -import org.elasticsearch.action.search.ShardSearchFailure; -import org.elasticsearch.rest.RestStatus; -import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.XContentParser; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; - -/** - * A response to _count API request. - */ -public final class CountResponse { - - static final ParseField COUNT = new ParseField("count"); - static final ParseField TERMINATED_EARLY = new ParseField("terminated_early"); - static final ParseField SHARDS = new ParseField("_shards"); - - private final long count; - private final Boolean terminatedEarly; - private final ShardStats shardStats; - - public CountResponse(long count, Boolean terminatedEarly, ShardStats shardStats) { - this.count = count; - this.terminatedEarly = terminatedEarly; - this.shardStats = shardStats; - } - - public ShardStats getShardStats() { - return shardStats; - } - - /** - * Number of documents matching request. - */ - public long getCount() { - return count; - } - - /** - * The total number of shards the search was executed on. - */ - public int getTotalShards() { - return shardStats.totalShards; - } - - /** - * The successful number of shards the search was executed on. - */ - public int getSuccessfulShards() { - return shardStats.successfulShards; - } - - /** - * The number of shards skipped due to pre-filtering - */ - public int getSkippedShards() { - return shardStats.skippedShards; - } - - /** - * The failed number of shards the search was executed on. - */ - public int getFailedShards() { - return shardStats.shardFailures.length; - } - - /** - * The failures that occurred during the search. - */ - public ShardSearchFailure[] getShardFailures() { - return shardStats.shardFailures; - } - - public RestStatus status() { - return RestStatus.status(shardStats.successfulShards, shardStats.totalShards, shardStats.shardFailures); - } - - public static CountResponse fromXContent(XContentParser parser) throws IOException { - ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); - parser.nextToken(); - ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.currentToken(), parser); - String currentName = parser.currentName(); - Boolean terminatedEarly = null; - long count = 0; - ShardStats shardStats = new ShardStats(-1, -1, 0, ShardSearchFailure.EMPTY_ARRAY); - - for (XContentParser.Token token = parser.nextToken(); token != XContentParser.Token.END_OBJECT; token = parser.nextToken()) { - if (token == XContentParser.Token.FIELD_NAME) { - currentName = parser.currentName(); - } else if (token.isValue()) { - if (COUNT.match(currentName, parser.getDeprecationHandler())) { - count = parser.longValue(); - } else if (TERMINATED_EARLY.match(currentName, parser.getDeprecationHandler())) { - terminatedEarly = parser.booleanValue(); - } else { - parser.skipChildren(); - } - } else if (token == XContentParser.Token.START_OBJECT) { - if (SHARDS.match(currentName, parser.getDeprecationHandler())) { - shardStats = ShardStats.fromXContent(parser); - } else { - parser.skipChildren(); - } - } - } - return new CountResponse(count, terminatedEarly, shardStats); - } - - @Override - public String toString() { - String s = "{" - + "count=" - + count - + (isTerminatedEarly() != null ? ", terminatedEarly=" + terminatedEarly : "") - + ", " - + shardStats - + '}'; - return s; - } - - public Boolean isTerminatedEarly() { - return terminatedEarly; - } - - /** - * Encapsulates _shards section of count api response. - */ - public static final class ShardStats { - - static final ParseField FAILED = new ParseField("failed"); - static final ParseField SKIPPED = new ParseField("skipped"); - static final ParseField TOTAL = new ParseField("total"); - static final ParseField SUCCESSFUL = new ParseField("successful"); - static final ParseField FAILURES = new ParseField("failures"); - - private final int successfulShards; - private final int totalShards; - private final int skippedShards; - private final ShardSearchFailure[] shardFailures; - - public ShardStats(int successfulShards, int totalShards, int skippedShards, ShardSearchFailure[] shardFailures) { - this.successfulShards = successfulShards; - this.totalShards = totalShards; - this.skippedShards = skippedShards; - this.shardFailures = Arrays.copyOf(shardFailures, shardFailures.length); - } - - public int getSuccessfulShards() { - return successfulShards; - } - - public int getTotalShards() { - return totalShards; - } - - public int getSkippedShards() { - return skippedShards; - } - - public ShardSearchFailure[] getShardFailures() { - return Arrays.copyOf(shardFailures, shardFailures.length, ShardSearchFailure[].class); - } - - static ShardStats fromXContent(XContentParser parser) throws IOException { - int successfulShards = -1; - int totalShards = -1; - int skippedShards = 0; // BWC @see org.elasticsearch.action.search.SearchResponse - List failures = new ArrayList<>(); - XContentParser.Token token; - String currentName = parser.currentName(); - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - currentName = parser.currentName(); - } else if (token.isValue()) { - if (FAILED.match(currentName, parser.getDeprecationHandler())) { - parser.intValue(); - } else if (SKIPPED.match(currentName, parser.getDeprecationHandler())) { - skippedShards = parser.intValue(); - } else if (TOTAL.match(currentName, parser.getDeprecationHandler())) { - totalShards = parser.intValue(); - } else if (SUCCESSFUL.match(currentName, parser.getDeprecationHandler())) { - successfulShards = parser.intValue(); - } else { - parser.skipChildren(); - } - } else if (token == XContentParser.Token.START_ARRAY) { - if (FAILURES.match(currentName, parser.getDeprecationHandler())) { - while ((parser.nextToken()) != XContentParser.Token.END_ARRAY) { - failures.add(ShardSearchFailure.fromXContent(parser)); - } - } else { - parser.skipChildren(); - } - } else { - parser.skipChildren(); - } - } - return new ShardStats(successfulShards, totalShards, skippedShards, failures.toArray(new ShardSearchFailure[failures.size()])); - } - - @Override - public String toString() { - return "_shards : {" - + "total=" - + totalShards - + ", successful=" - + successfulShards - + ", skipped=" - + skippedShards - + ", failed=" - + (shardFailures != null && shardFailures.length > 0 ? shardFailures.length : 0) - + (shardFailures != null && shardFailures.length > 0 ? ", failures: " + Arrays.asList(shardFailures) : "") - + '}'; - } - } -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/GetSourceRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/GetSourceRequest.java deleted file mode 100644 index 6e26457a27a5..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/GetSourceRequest.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client.core; - -import org.elasticsearch.action.get.GetRequest; -import org.elasticsearch.client.Validatable; -import org.elasticsearch.search.fetch.subphase.FetchSourceContext; - -public final class GetSourceRequest implements Validatable { - private String routing; - private String preference; - - private boolean refresh = false; - private boolean realtime = true; - - private FetchSourceContext fetchSourceContext; - - private final String index; - private final String id; - - public GetSourceRequest(String index, String id) { - this.index = index; - this.id = id; - } - - public static GetSourceRequest from(GetRequest getRequest) { - return new GetSourceRequest(getRequest.index(), getRequest.id()).routing(getRequest.routing()) - .preference(getRequest.preference()) - .refresh(getRequest.refresh()) - .realtime(getRequest.realtime()) - .fetchSourceContext(getRequest.fetchSourceContext()); - } - - /** - * Controls the shard routing of the request. Using this value to hash the shard - * and not the id. - */ - public GetSourceRequest routing(String routing) { - if (routing != null && routing.length() == 0) { - this.routing = null; - } else { - this.routing = routing; - } - return this; - } - - /** - * Sets the preference to execute the search. Defaults to randomize across shards. Can be set to - * {@code _local} to prefer local shards or a custom value, which guarantees that the same order - * will be used across different requests. - */ - public GetSourceRequest preference(String preference) { - this.preference = preference; - return this; - } - - /** - * Should a refresh be executed before this get operation causing the operation to - * return the latest value. Note, heavy get should not set this to {@code true}. Defaults - * to {@code false}. - */ - public GetSourceRequest refresh(boolean refresh) { - this.refresh = refresh; - return this; - } - - public GetSourceRequest realtime(boolean realtime) { - this.realtime = realtime; - return this; - } - - /** - * Allows setting the {@link FetchSourceContext} for this request, controlling if and how _source should be returned. - * Note, the {@code fetchSource} field of the context must be set to {@code true}. - */ - - public GetSourceRequest fetchSourceContext(FetchSourceContext context) { - this.fetchSourceContext = context; - return this; - } - - public String index() { - return index; - } - - public String id() { - return id; - } - - public String routing() { - return routing; - } - - public String preference() { - return preference; - } - - public boolean refresh() { - return refresh; - } - - public boolean realtime() { - return realtime; - } - - public FetchSourceContext fetchSourceContext() { - return fetchSourceContext; - } -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/GetSourceResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/GetSourceResponse.java deleted file mode 100644 index 45469cf1d1fb..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/GetSourceResponse.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client.core; - -import org.elasticsearch.xcontent.XContentParser; - -import java.io.IOException; -import java.util.Map; - -public final class GetSourceResponse { - - private final Map source; - - public GetSourceResponse(Map source) { - this.source = source; - } - - public static GetSourceResponse fromXContent(XContentParser parser) throws IOException { - return new GetSourceResponse(parser.map()); - } - - public Map getSource() { - return this.source; - } - - @Override - public String toString() { - return source.toString(); - } -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MainRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MainRequest.java deleted file mode 100644 index 592d98674372..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MainRequest.java +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client.core; - -import org.elasticsearch.client.Validatable; - -public class MainRequest implements Validatable {} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MultiTermVectorsRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MultiTermVectorsRequest.java deleted file mode 100644 index cdd98bb12a56..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MultiTermVectorsRequest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client.core; - -import org.elasticsearch.client.Validatable; -import org.elasticsearch.xcontent.ToXContentObject; -import org.elasticsearch.xcontent.XContentBuilder; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import static org.elasticsearch.client.core.TermVectorsRequest.createFromTemplate; - -public class MultiTermVectorsRequest implements ToXContentObject, Validatable { - - private List requests = new ArrayList<>(); - - /** - * Constructs an empty MultiTermVectorsRequest - * After that use {@code add} method to add individual {@code TermVectorsRequest} to it. - */ - public MultiTermVectorsRequest() {} - - /** - * Constructs a MultiTermVectorsRequest from the given document ids - * and a template {@code TermVectorsRequest}. - * Used when individual requests share the same index, type and other settings. - * @param ids - ids of documents for which term vectors are requested - * @param template - a template {@code TermVectorsRequest} that allows to set all - * settings only once for all requests. - */ - public MultiTermVectorsRequest(String[] ids, TermVectorsRequest template) { - for (String id : ids) { - TermVectorsRequest request = createFromTemplate(template, id); - requests.add(request); - } - } - - /** - * Adds another {@code TermVectorsRequest} to this {@code MultiTermVectorsRequest} - * @param request - {@code TermVectorsRequest} to add - */ - public void add(TermVectorsRequest request) { - requests.add(request); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.startArray("docs"); - for (TermVectorsRequest request : requests) { - request.toXContent(builder, params); - } - builder.endArray(); - builder.endObject(); - return builder; - } - -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MultiTermVectorsResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MultiTermVectorsResponse.java deleted file mode 100644 index 3f836d714433..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/MultiTermVectorsResponse.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client.core; - -import org.elasticsearch.xcontent.ConstructingObjectParser; -import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.XContentParser; - -import java.util.List; -import java.util.Objects; - -import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; - -public class MultiTermVectorsResponse { - private final List responses; - - public MultiTermVectorsResponse(List responses) { - this.responses = responses; - } - - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - "multi_term_vectors", - true, - args -> { - // as the response comes from server, we are sure that args[0] will be a list of TermVectorsResponse - @SuppressWarnings("unchecked") - List termVectorsResponsesList = (List) args[0]; - return new MultiTermVectorsResponse(termVectorsResponsesList); - } - ); - - static { - PARSER.declareObjectArray(constructorArg(), (p, c) -> TermVectorsResponse.fromXContent(p), new ParseField("docs")); - } - - public static MultiTermVectorsResponse fromXContent(XContentParser parser) { - return PARSER.apply(parser, null); - } - - /** - * Returns the list of {@code TermVectorsResponse} for this {@code MultiTermVectorsResponse} - */ - public List getTermVectorsResponses() { - return responses; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if ((obj instanceof MultiTermVectorsResponse) == false) return false; - MultiTermVectorsResponse other = (MultiTermVectorsResponse) obj; - return Objects.equals(responses, other.responses); - } - - @Override - public int hashCode() { - return Objects.hash(responses); - } - -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/PageParams.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/PageParams.java deleted file mode 100644 index c41e17e5d1ed..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/PageParams.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -package org.elasticsearch.client.core; - -import org.elasticsearch.core.Nullable; -import org.elasticsearch.xcontent.ConstructingObjectParser; -import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.ToXContentObject; -import org.elasticsearch.xcontent.XContentBuilder; - -import java.io.IOException; -import java.util.Objects; - -/** - * Paging parameters for GET requests - */ -public class PageParams implements ToXContentObject { - - public static final ParseField PAGE = new ParseField("page"); - public static final ParseField FROM = new ParseField("from"); - public static final ParseField SIZE = new ParseField("size"); - - public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - PAGE.getPreferredName(), - a -> new PageParams((Integer) a[0], (Integer) a[1]) - ); - - static { - PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), FROM); - PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), SIZE); - } - - private final Integer from; - private final Integer size; - - /** - * Constructs paging parameters - * @param from skips the specified number of items. When {@code null} the default value will be used. - * @param size specifies the maximum number of items to obtain. When {@code null} the default value will be used. - */ - public PageParams(@Nullable Integer from, @Nullable Integer size) { - this.from = from; - this.size = size; - } - - public Integer getFrom() { - return from; - } - - public Integer getSize() { - return size; - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - if (from != null) { - builder.field(FROM.getPreferredName(), from); - } - if (size != null) { - builder.field(SIZE.getPreferredName(), size); - } - builder.endObject(); - return builder; - } - - @Override - public int hashCode() { - return Objects.hash(from, size); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - PageParams other = (PageParams) obj; - return Objects.equals(from, other.from) && Objects.equals(size, other.size); - } - -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/TermVectorsRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/TermVectorsRequest.java deleted file mode 100644 index af27dae1c04c..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/TermVectorsRequest.java +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client.core; - -import org.elasticsearch.client.Validatable; -import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.core.Nullable; -import org.elasticsearch.xcontent.ToXContentObject; -import org.elasticsearch.xcontent.XContentBuilder; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Map; - -public class TermVectorsRequest implements ToXContentObject, Validatable { - - private final String index; - @Nullable - private final String type; - private String id = null; - private XContentBuilder docBuilder = null; - - private String routing = null; - private String preference = null; - private boolean realtime = true; - private String[] fields = null; - private boolean requestPositions = true; - private boolean requestPayloads = true; - private boolean requestOffsets = true; - private boolean requestFieldStatistics = true; - private boolean requestTermStatistics = false; - private Map perFieldAnalyzer = null; - private Map filterSettings = null; - - /** - * Constructs TermVectorRequest for the given document - * - * @param index - index of the document - * @param docId - id of the document - */ - public TermVectorsRequest(String index, String docId) { - this.index = index; - this.type = null; - this.id = docId; - } - - /** - * Constructs TermVectorRequest for the given document - * - * @param index - index of the document - * @param type - type of the document - * @param docId - id of the document - * - * @deprecated Types are in the process of being removed, use - * {@link #TermVectorsRequest(String, String)} instead. - */ - @Deprecated - public TermVectorsRequest(String index, String type, String docId) { - this.index = index; - this.type = type; - this.id = docId; - } - - /** - * Constructs TermVectorRequest for an artificial document - * - * @param index - index of the document - * @param docBuilder - an artificial document - */ - public TermVectorsRequest(String index, XContentBuilder docBuilder) { - this.index = index; - this.type = null; - this.docBuilder = docBuilder; - } - - /** - * Constructs TermVectorRequest for an artificial document - * @param index - index of the document - * @param type - type of the document - * @param docBuilder - an artificial document - * - * @deprecated Types are in the process of being removed, use - * {@link TermVectorsRequest(String, XContentBuilder)} instead. - */ - @Deprecated - public TermVectorsRequest(String index, String type, XContentBuilder docBuilder) { - this.index = index; - this.type = type; - this.docBuilder = docBuilder; - } - - /** - * Constructs a new TermVectorRequest from a template - * using the provided document id - * @param template - a term vector request served as a template - * @param id - id of the requested document - */ - static TermVectorsRequest createFromTemplate(TermVectorsRequest template, String id) { - TermVectorsRequest request = new TermVectorsRequest(template.getIndex(), template.getType(), id); - request.realtime = template.getRealtime(); - request.requestPositions = template.requestPositions; - request.requestPayloads = template.requestPayloads; - request.requestOffsets = template.requestOffsets; - request.requestFieldStatistics = template.requestFieldStatistics; - request.requestTermStatistics = template.requestTermStatistics; - if (template.routing != null) request.setRouting(template.getRouting()); - if (template.preference != null) request.setPreference(template.getPreference()); - if (template.fields != null) request.setFields(template.getFields()); - if (template.perFieldAnalyzer != null) request.setPerFieldAnalyzer(template.perFieldAnalyzer); - if (template.filterSettings != null) request.setFilterSettings(template.filterSettings); - return request; - } - - /** - * Returns the index of the request - */ - public String getIndex() { - return index; - } - - /** - * Returns the type of the request - * - * @deprecated Types are in the process of being removed. - */ - @Deprecated - public String getType() { - return type; - } - - /** - * Returns the id of the request - * can be NULL if there is no document ID - */ - public String getId() { - return id; - } - - /** - * Sets the fields for which term vectors information should be retrieved - */ - public void setFields(String... fields) { - this.fields = fields; - } - - public String[] getFields() { - return fields; - } - - /** - * Sets whether to request term positions - */ - public void setPositions(boolean positions) { - this.requestPositions = positions; - } - - /** - * Sets whether to request term payloads - */ - public void setPayloads(boolean payloads) { - this.requestPayloads = payloads; - } - - /** - * Sets whether to request term offsets - */ - public void setOffsets(boolean offsets) { - this.requestOffsets = offsets; - } - - /** - * Sets whether to request field statistics - */ - public void setFieldStatistics(boolean fieldStatistics) { - this.requestFieldStatistics = fieldStatistics; - } - - /** - * Sets whether to request term statistics - */ - public void setTermStatistics(boolean termStatistics) { - this.requestTermStatistics = termStatistics; - } - - /** - * Sets different analyzers than the one at the fields - */ - public void setPerFieldAnalyzer(Map perFieldAnalyzer) { - this.perFieldAnalyzer = perFieldAnalyzer; - } - - /** - * Sets conditions for terms filtering - */ - public void setFilterSettings(Map filterSettings) { - this.filterSettings = filterSettings; - } - - /** - * Sets a routing to route a request to a particular shard - */ - public void setRouting(String routing) { - this.routing = routing; - } - - public String getRouting() { - return routing; - } - - /** - * Set a preference of which shard copies to execute the request - */ - public void setPreference(String preference) { - this.preference = preference; - } - - public String getPreference() { - return preference; - } - - /** - * Sets if the request should be realtime or near-realtime - */ - public void setRealtime(boolean realtime) { - this.realtime = realtime; - } - - /** - * Returns if the request is realtime(true) or near-realtime(false) - */ - public boolean getRealtime() { - return realtime; - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field("_index", index); - if (type != null) { - builder.field("_type", type); - } - if (id != null) builder.field("_id", id); - if (fields != null) builder.field("fields", fields); - // set values only when different from defaults - if (requestPositions == false) builder.field("positions", false); - if (requestPayloads == false) builder.field("payloads", false); - if (requestOffsets == false) builder.field("offsets", false); - if (requestFieldStatistics == false) builder.field("field_statistics", false); - if (requestTermStatistics) builder.field("term_statistics", true); - if (perFieldAnalyzer != null) builder.field("per_field_analyzer", perFieldAnalyzer); - - if (docBuilder != null) { - BytesReference doc = BytesReference.bytes(docBuilder); - try (InputStream stream = doc.streamInput()) { - builder.rawField("doc", stream, docBuilder.contentType()); - } - } - - if (filterSettings != null) { - builder.startObject("filter"); - String[] filterSettingNames = { - "max_num_terms", - "min_term_freq", - "max_term_freq", - "min_doc_freq", - "max_doc_freq", - "min_word_length", - "max_word_length" }; - for (String settingName : filterSettingNames) { - if (filterSettings.containsKey(settingName)) builder.field(settingName, filterSettings.get(settingName)); - } - builder.endObject(); - } - builder.endObject(); - return builder; - } - -} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/TermVectorsResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/core/TermVectorsResponse.java deleted file mode 100644 index 89764f639e1a..000000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/core/TermVectorsResponse.java +++ /dev/null @@ -1,468 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.client.core; - -import org.elasticsearch.core.Nullable; -import org.elasticsearch.xcontent.ConstructingObjectParser; -import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.XContentParser; - -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; - -import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; -import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; - -public class TermVectorsResponse { - private final String index; - private final String id; - private final long docVersion; - private final boolean found; - private final long tookInMillis; - private final List termVectorList; - - public TermVectorsResponse(String index, String id, long version, boolean found, long tookInMillis, List termVectorList) { - this.index = index; - this.id = id; - this.docVersion = version; - this.found = found; - this.tookInMillis = tookInMillis; - this.termVectorList = termVectorList; - } - - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - "term_vectors", - true, - args -> { - // as the response comes from server, we are sure that args[5] will be a list of TermVector - @SuppressWarnings("unchecked") - List termVectorList = (List) args[5]; - if (termVectorList != null) { - Collections.sort(termVectorList, Comparator.comparing(TermVector::getFieldName)); - } - return new TermVectorsResponse( - (String) args[0], - (String) args[1], - (long) args[2], - (boolean) args[3], - (long) args[4], - termVectorList - ); - } - ); - - static { - PARSER.declareString(constructorArg(), new ParseField("_index")); - PARSER.declareString(optionalConstructorArg(), new ParseField("_id")); - PARSER.declareLong(constructorArg(), new ParseField("_version")); - PARSER.declareBoolean(constructorArg(), new ParseField("found")); - PARSER.declareLong(constructorArg(), new ParseField("took")); - PARSER.declareNamedObjects( - optionalConstructorArg(), - (p, c, fieldName) -> TermVector.fromXContent(p, fieldName), - new ParseField("term_vectors") - ); - } - - public static TermVectorsResponse fromXContent(XContentParser parser) { - return PARSER.apply(parser, null); - } - - /** - * Returns the index for the response - */ - public String getIndex() { - return index; - } - - /** - * Returns the id of the request - * can be NULL if there is no document ID - */ - public String getId() { - return id; - } - - /** - * Returns if the document is found - * always true for artificial documents - */ - public boolean getFound() { - return found; - } - - /** - * Returns the document version - */ - public long getDocVersion() { - return docVersion; - } - - /** - * Returns the time that a request took in milliseconds - */ - public long getTookInMillis() { - return tookInMillis; - } - - /** - * Returns the list of term vectors - */ - public List getTermVectorsList() { - return termVectorList; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if ((obj instanceof TermVectorsResponse) == false) return false; - TermVectorsResponse other = (TermVectorsResponse) obj; - return index.equals(other.index) - && Objects.equals(id, other.id) - && docVersion == other.docVersion - && found == other.found - && tookInMillis == other.tookInMillis - && Objects.equals(termVectorList, other.termVectorList); - } - - @Override - public int hashCode() { - return Objects.hash(index, id, docVersion, found, tookInMillis, termVectorList); - } - - public static final class TermVector { - - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - "term_vector", - true, - (args, ctxFieldName) -> { - // as the response comes from server, we are sure that args[1] will be a list of Term - @SuppressWarnings("unchecked") - List terms = (List) args[1]; - if (terms != null) { - Collections.sort(terms, Comparator.comparing(Term::getTerm)); - } - return new TermVector(ctxFieldName, (FieldStatistics) args[0], terms); - } - ); - - static { - PARSER.declareObject(optionalConstructorArg(), (p, c) -> FieldStatistics.fromXContent(p), new ParseField("field_statistics")); - PARSER.declareNamedObjects(optionalConstructorArg(), (p, c, term) -> Term.fromXContent(p, term), new ParseField("terms")); - } - - private final String fieldName; - @Nullable - private final FieldStatistics fieldStatistics; - @Nullable - private final List terms; - - public TermVector(String fieldName, FieldStatistics fieldStatistics, List terms) { - this.fieldName = fieldName; - this.fieldStatistics = fieldStatistics; - this.terms = terms; - } - - public static TermVector fromXContent(XContentParser parser, String fieldName) { - return PARSER.apply(parser, fieldName); - } - - /** - * Returns the field name of the current term vector - */ - public String getFieldName() { - return fieldName; - } - - /** - * Returns the list of terms for the current term vector - */ - public List getTerms() { - return terms; - } - - /** - * Returns the field statistics for the current field - */ - public FieldStatistics getFieldStatistics() { - return fieldStatistics; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if ((obj instanceof TermVector) == false) return false; - TermVector other = (TermVector) obj; - return fieldName.equals(other.fieldName) - && Objects.equals(fieldStatistics, other.fieldStatistics) - && Objects.equals(terms, other.terms); - } - - @Override - public int hashCode() { - return Objects.hash(fieldName, fieldStatistics, terms); - } - - // Class containing a general field statistics for the field - public static final class FieldStatistics { - - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - "field_statistics", - true, - args -> { return new FieldStatistics((long) args[0], (int) args[1], (long) args[2]); } - ); - - static { - PARSER.declareLong(constructorArg(), new ParseField("sum_doc_freq")); - PARSER.declareInt(constructorArg(), new ParseField("doc_count")); - PARSER.declareLong(constructorArg(), new ParseField("sum_ttf")); - } - private final long sumDocFreq; - private final int docCount; - private final long sumTotalTermFreq; - - public FieldStatistics(long sumDocFreq, int docCount, long sumTotalTermFreq) { - this.sumDocFreq = sumDocFreq; - this.docCount = docCount; - this.sumTotalTermFreq = sumTotalTermFreq; - } - - public static FieldStatistics fromXContent(XContentParser parser) { - return PARSER.apply(parser, null); - } - - /* - * Returns how many documents this field contains - */ - public int getDocCount() { - return docCount; - } - - /** - * Returns the sum of document frequencies for all terms in this field - */ - public long getSumDocFreq() { - return sumDocFreq; - } - - /** - * Returns the sum of total term frequencies of all terms in this field - */ - public long getSumTotalTermFreq() { - return sumTotalTermFreq; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if ((obj instanceof FieldStatistics) == false) return false; - FieldStatistics other = (FieldStatistics) obj; - return docCount == other.docCount && sumDocFreq == other.sumDocFreq && sumTotalTermFreq == other.sumTotalTermFreq; - } - - @Override - public int hashCode() { - return Objects.hash(docCount, sumDocFreq, sumTotalTermFreq); - } - } - - public static final class Term { - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - "token", - true, - (args, ctxTerm) -> { - // as the response comes from server, we are sure that args[4] will be a list of Token - @SuppressWarnings("unchecked") - List tokens = (List) args[4]; - if (tokens != null) { - Collections.sort( - tokens, - Comparator.comparing(Token::getPosition, Comparator.nullsFirst(Integer::compareTo)) - .thenComparing(Token::getStartOffset, Comparator.nullsFirst(Integer::compareTo)) - .thenComparing(Token::getEndOffset, Comparator.nullsFirst(Integer::compareTo)) - ); - } - return new Term(ctxTerm, (int) args[0], (Integer) args[1], (Long) args[2], (Float) args[3], tokens); - } - ); - static { - PARSER.declareInt(constructorArg(), new ParseField("term_freq")); - PARSER.declareInt(optionalConstructorArg(), new ParseField("doc_freq")); - PARSER.declareLong(optionalConstructorArg(), new ParseField("ttf")); - PARSER.declareFloat(optionalConstructorArg(), new ParseField("score")); - PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> Token.fromXContent(p), new ParseField("tokens")); - } - - private final String term; - private final int termFreq; - @Nullable - private final Integer docFreq; - @Nullable - private final Long totalTermFreq; - @Nullable - private final Float score; - @Nullable - private final List tokens; - - public Term(String term, int termFreq, Integer docFreq, Long totalTermFreq, Float score, List tokens) { - this.term = term; - this.termFreq = termFreq; - this.docFreq = docFreq; - this.totalTermFreq = totalTermFreq; - this.score = score; - this.tokens = tokens; - } - - public static Term fromXContent(XContentParser parser, String term) { - return PARSER.apply(parser, term); - } - - /** - * Returns the string representation of the term - */ - public String getTerm() { - return term; - } - - /** - * Returns term frequency - the number of times this term occurs in the current document - */ - public int getTermFreq() { - return termFreq; - } - - /** - * Returns document frequency - the number of documents in the index that contain this term - */ - public Integer getDocFreq() { - return docFreq; - } - - /** - * Returns total term frequency - the number of times this term occurs across all documents - */ - public Long getTotalTermFreq() { - return totalTermFreq; - } - - /** - * Returns tf-idf score, if the request used some form of terms filtering - */ - public Float getScore() { - return score; - } - - /** - * Returns a list of tokens for the term - */ - public List getTokens() { - return tokens; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if ((obj instanceof Term) == false) return false; - Term other = (Term) obj; - return term.equals(other.term) - && termFreq == other.termFreq - && Objects.equals(docFreq, other.docFreq) - && Objects.equals(totalTermFreq, other.totalTermFreq) - && Objects.equals(score, other.score) - && Objects.equals(tokens, other.tokens); - } - - @Override - public int hashCode() { - return Objects.hash(term, termFreq, docFreq, totalTermFreq, score, tokens); - } - } - - public static final class Token { - - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - "token", - true, - args -> { return new Token((Integer) args[0], (Integer) args[1], (Integer) args[2], (String) args[3]); } - ); - static { - PARSER.declareInt(optionalConstructorArg(), new ParseField("start_offset")); - PARSER.declareInt(optionalConstructorArg(), new ParseField("end_offset")); - PARSER.declareInt(optionalConstructorArg(), new ParseField("position")); - PARSER.declareString(optionalConstructorArg(), new ParseField("payload")); - } - - @Nullable - private final Integer startOffset; - @Nullable - private final Integer endOffset; - @Nullable - private final Integer position; - @Nullable - private final String payload; - - public Token(Integer startOffset, Integer endOffset, Integer position, String payload) { - this.startOffset = startOffset; - this.endOffset = endOffset; - this.position = position; - this.payload = payload; - } - - public static Token fromXContent(XContentParser parser) { - return PARSER.apply(parser, null); - } - - /** - * Returns the start offset of the token in the document's field - */ - public Integer getStartOffset() { - return startOffset; - } - - /** - * Returns the end offset of the token in the document's field - */ - public Integer getEndOffset() { - return endOffset; - } - - /** - * Returns the position of the token in the document's field - */ - public Integer getPosition() { - return position; - } - - /** - * Returns the payload of the token or null if the payload doesn't exist - */ - public String getPayload() { - return payload; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if ((obj instanceof Token) == false) return false; - Token other = (Token) obj; - return Objects.equals(startOffset, other.startOffset) - && Objects.equals(endOffset, other.endOffset) - && Objects.equals(position, other.position) - && Objects.equals(payload, other.payload); - } - - @Override - public int hashCode() { - return Objects.hash(startOffset, endOffset, position, payload); - } - } - } -} From 91d69d928eb9d89e5d6130a89cdb3a3e7f499a6e Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Thu, 1 Dec 2022 01:09:06 -0800 Subject: [PATCH 124/919] Revert "Chunked encoding for RestGetIndicesAction (#92016)" (#92033) This reverts commit 75de8f85cf41374fcdc5da3a36f86f9098b81951 due to https://github.com/elastic/elasticsearch/issues/92032 and https://github.com/elastic/kibana/issues/146758 --- .../rest/Netty4HeadBodyIsEmptyIT.java | 8 +- .../elasticsearch/action/ActionModule.java | 2 +- .../admin/indices/get/GetIndexResponse.java | 95 +++++++++---------- .../admin/indices/RestGetIndicesAction.java | 14 ++- .../indices/get/GetIndexResponseTests.java | 19 ---- .../indices/RestGetIndicesActionTests.java | 5 +- 6 files changed, 66 insertions(+), 77 deletions(-) diff --git a/modules/transport-netty4/src/javaRestTest/java/org/elasticsearch/rest/Netty4HeadBodyIsEmptyIT.java b/modules/transport-netty4/src/javaRestTest/java/org/elasticsearch/rest/Netty4HeadBodyIsEmptyIT.java index e705c4303687..b1e2a6d7fdd1 100644 --- a/modules/transport-netty4/src/javaRestTest/java/org/elasticsearch/rest/Netty4HeadBodyIsEmptyIT.java +++ b/modules/transport-netty4/src/javaRestTest/java/org/elasticsearch/rest/Netty4HeadBodyIsEmptyIT.java @@ -25,7 +25,6 @@ import static org.elasticsearch.rest.RestStatus.OK; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.nullValue; public class Netty4HeadBodyIsEmptyIT extends ESRestTestCase { public void testHeadRoot() throws IOException { @@ -60,8 +59,8 @@ public void testDocumentExists() throws IOException { public void testIndexExists() throws IOException { createTestDoc(); - headTestCase("/test", emptyMap(), nullValue(Integer.class)); - headTestCase("/test", singletonMap("pretty", "true"), nullValue(Integer.class)); + headTestCase("/test", emptyMap(), greaterThan(0)); + headTestCase("/test", singletonMap("pretty", "true"), greaterThan(0)); } public void testAliasExists() throws IOException { @@ -178,8 +177,7 @@ private void headTestCase( request.setOptions(expectWarnings(expectedWarnings)); Response response = client().performRequest(request); assertEquals(expectedStatusCode, response.getStatusLine().getStatusCode()); - final var contentLength = response.getHeader("Content-Length"); - assertThat(contentLength == null ? null : Integer.valueOf(contentLength), matcher); + assertThat(Integer.valueOf(response.getHeader("Content-Length")), matcher); assertNull("HEAD requests shouldn't have a response body but " + url + " did", response.getEntity()); } diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index 59d055e27415..40ee8e4db171 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -768,7 +768,7 @@ public void initRestHandlers(Supplier nodesInCluster) { registerHandler.accept(new RestResetFeatureStateAction()); registerHandler.accept(new RestGetFeatureUpgradeStatusAction()); registerHandler.accept(new RestPostFeatureUpgradeAction()); - registerHandler.accept(new RestGetIndicesAction()); + registerHandler.accept(new RestGetIndicesAction(threadPool)); registerHandler.accept(new RestIndicesStatsAction()); registerHandler.accept(new RestIndicesSegmentsAction()); registerHandler.accept(new RestIndicesShardStoresAction()); diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/get/GetIndexResponse.java b/server/src/main/java/org/elasticsearch/action/admin/indices/get/GetIndexResponse.java index 1e96b950c7a1..4f8f24ea5b72 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/get/GetIndexResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/get/GetIndexResponse.java @@ -13,18 +13,16 @@ import org.elasticsearch.cluster.metadata.AliasMetadata; import org.elasticsearch.cluster.metadata.MappingMetadata; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.core.RestApiVersion; import org.elasticsearch.index.mapper.MapperService; -import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xcontent.ToXContentObject; +import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; import java.util.Arrays; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -35,7 +33,7 @@ /** * A response for a get index action. */ -public class GetIndexResponse extends ActionResponse implements ChunkedToXContent { +public class GetIndexResponse extends ActionResponse implements ToXContentObject { private Map mappings = Map.of(); private Map> aliases = Map.of(); @@ -180,58 +178,59 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public Iterator toXContentChunked(ToXContent.Params ignored) { - return Iterators.concat( - Iterators.single((builder, params) -> builder.startObject()), - Arrays.stream(indices).map(index -> (builder, params) -> { + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + { + for (final String index : indices) { builder.startObject(index); - - builder.startObject("aliases"); - List indexAliases = aliases.get(index); - if (indexAliases != null) { - for (final AliasMetadata alias : indexAliases) { - AliasMetadata.Builder.toXContent(alias, builder, params); + { + builder.startObject("aliases"); + List indexAliases = aliases.get(index); + if (indexAliases != null) { + for (final AliasMetadata alias : indexAliases) { + AliasMetadata.Builder.toXContent(alias, builder, params); + } } - } - builder.endObject(); + builder.endObject(); - MappingMetadata indexMappings = mappings.get(index); - if (indexMappings == null) { - builder.startObject("mappings").endObject(); - } else { - if (builder.getRestApiVersion() == RestApiVersion.V_7 - && params.paramAsBoolean(INCLUDE_TYPE_NAME_PARAMETER, DEFAULT_INCLUDE_TYPE_NAME_POLICY)) { - builder.startObject("mappings"); - builder.field(MapperService.SINGLE_MAPPING_NAME, indexMappings.sourceAsMap()); - builder.endObject(); + MappingMetadata indexMappings = mappings.get(index); + if (indexMappings == null) { + builder.startObject("mappings").endObject(); } else { - builder.field("mappings", indexMappings.sourceAsMap()); + if (builder.getRestApiVersion() == RestApiVersion.V_7 + && params.paramAsBoolean(INCLUDE_TYPE_NAME_PARAMETER, DEFAULT_INCLUDE_TYPE_NAME_POLICY)) { + builder.startObject("mappings"); + builder.field(MapperService.SINGLE_MAPPING_NAME, indexMappings.sourceAsMap()); + builder.endObject(); + } else { + builder.field("mappings", indexMappings.sourceAsMap()); + } } - } - builder.startObject("settings"); - Settings indexSettings = settings.get(index); - if (indexSettings != null) { - indexSettings.toXContent(builder, params); - } - builder.endObject(); - - Settings defaultIndexSettings = defaultSettings.get(index); - if (defaultIndexSettings != null && defaultIndexSettings.isEmpty() == false) { - builder.startObject("defaults"); - defaultIndexSettings.toXContent(builder, params); + builder.startObject("settings"); + Settings indexSettings = settings.get(index); + if (indexSettings != null) { + indexSettings.toXContent(builder, params); + } builder.endObject(); - } - String dataStream = dataStreams.get(index); - if (dataStream != null) { - builder.field("data_stream", dataStream); - } + Settings defaultIndexSettings = defaultSettings.get(index); + if (defaultIndexSettings != null && defaultIndexSettings.isEmpty() == false) { + builder.startObject("defaults"); + defaultIndexSettings.toXContent(builder, params); + builder.endObject(); + } - return builder.endObject(); - }).iterator(), - Iterators.single((builder, params) -> builder.endObject()) - ); + String dataStream = dataStreams.get(index); + if (dataStream != null) { + builder.field("data_stream", dataStream); + } + } + builder.endObject(); + } + } + builder.endObject(); + return builder; } @Override diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesAction.java index 46a686d6e74b..11c725695aa1 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesAction.java @@ -17,8 +17,9 @@ import org.elasticsearch.core.RestApiVersion; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.action.DispatchingRestToXContentListener; import org.elasticsearch.rest.action.RestCancellableNodeClient; -import org.elasticsearch.rest.action.RestChunkedToXContentListener; +import org.elasticsearch.threadpool.ThreadPool; import java.io.IOException; import java.util.List; @@ -38,6 +39,12 @@ public class RestGetIndicesAction extends BaseRestHandler { private static final Set COMPATIBLE_RESPONSE_PARAMS = addToCopy(Settings.FORMAT_PARAMS, INCLUDE_TYPE_NAME_PARAMETER); + private final ThreadPool threadPool; + + public RestGetIndicesAction(ThreadPool threadPool) { + this.threadPool = threadPool; + } + @Override public List routes() { return List.of(new Route(GET, "/{index}"), new Route(HEAD, "/{index}")); @@ -69,7 +76,10 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC final var httpChannel = request.getHttpChannel(); return channel -> new RestCancellableNodeClient(client, httpChannel).admin() .indices() - .getIndex(getIndexRequest, new RestChunkedToXContentListener<>(channel)); + .getIndex( + getIndexRequest, + new DispatchingRestToXContentListener<>(threadPool.executor(ThreadPool.Names.MANAGEMENT), channel, request) + ); } /** diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/get/GetIndexResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/get/GetIndexResponseTests.java index 85ab886542a8..3f6f0baaabf2 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/get/GetIndexResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/get/GetIndexResponseTests.java @@ -18,9 +18,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.RandomCreateIndexGenerator; import org.elasticsearch.test.AbstractWireSerializingTestCase; -import org.elasticsearch.xcontent.ToXContent; -import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -29,9 +27,6 @@ import java.util.Locale; import java.util.Map; -import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; -import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; - public class GetIndexResponseTests extends AbstractWireSerializingTestCase { @Override @@ -78,18 +73,4 @@ protected GetIndexResponse createTestInstance() { } return new GetIndexResponse(indices, mappings, aliases, settings, defaultSettings, dataStreams); } - - public void testChunking() throws IOException { - final var response = createTestInstance(); - - try (var builder = jsonBuilder()) { - int chunkCount = 0; - final var iterator = response.toXContentChunked(EMPTY_PARAMS); - while (iterator.hasNext()) { - iterator.next().toXContent(builder, ToXContent.EMPTY_PARAMS); - chunkCount += 1; - } - assertEquals(response.getIndices().length + 2, chunkCount); - } // closing the builder verifies that the XContent is well-formed - } } diff --git a/server/src/test/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesActionTests.java b/server/src/test/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesActionTests.java index 5885c6f8c988..164fc38d15c4 100644 --- a/server/src/test/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesActionTests.java +++ b/server/src/test/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesActionTests.java @@ -9,6 +9,7 @@ package org.elasticsearch.rest.action.admin.indices; import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.common.util.concurrent.DeterministicTaskQueue; import org.elasticsearch.core.RestApiVersion; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.test.ESTestCase; @@ -36,7 +37,7 @@ public void testIncludeTypeNamesWarning() throws IOException { Map.of("Content-Type", contentTypeHeader, "Accept", contentTypeHeader) ).withMethod(RestRequest.Method.GET).withPath("/some_index").withParams(params).build(); - RestGetIndicesAction handler = new RestGetIndicesAction(); + RestGetIndicesAction handler = new RestGetIndicesAction(new DeterministicTaskQueue().getThreadPool()); handler.prepareRequest(request, mock(NodeClient.class)); assertCriticalWarnings(RestGetIndicesAction.TYPES_DEPRECATION_MESSAGE); @@ -57,7 +58,7 @@ public void testIncludeTypeNamesWarningExists() throws IOException { Map.of("Content-Type", contentTypeHeader, "Accept", contentTypeHeader) ).withMethod(RestRequest.Method.HEAD).withPath("/some_index").withParams(params).build(); - RestGetIndicesAction handler = new RestGetIndicesAction(); + RestGetIndicesAction handler = new RestGetIndicesAction(new DeterministicTaskQueue().getThreadPool()); handler.prepareRequest(request, mock(NodeClient.class)); } } From 635a4fe2bd6bd88ea0122602f6fe68e6f72fbb3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Thu, 1 Dec 2022 11:22:12 +0100 Subject: [PATCH 125/919] Mark empty `_terms_enum` results due to DLS as incomplete (#91720) Today `_terms_enum` returns empty results for indices with document level security. Elasticsearch should return some hint in case the user hits empty results due to DLS limitation so the caller (ie. Kibana) can fall back to other strategies or notify the user with some appropriate error message. This changes the behaviour of the NodeTransportHandler so that it returns a NodeTermsEnumResponse with an error indication. The resulting API response will flag the enum as "incomplete" and list the error in the shard errors section. Clients can choose to react to this in the appropriate way. Closes #88321 --- docs/changelog/91720.yaml | 6 ++++ .../action/TransportTermsEnumAction.java | 15 ++++++++- .../test/terms_enum/10_basic.yml | 31 +++++++++++++------ 3 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 docs/changelog/91720.yaml diff --git a/docs/changelog/91720.yaml b/docs/changelog/91720.yaml new file mode 100644 index 000000000000..4994330c8aaa --- /dev/null +++ b/docs/changelog/91720.yaml @@ -0,0 +1,6 @@ +pr: 91720 +summary: Mark empty `_terms_enum` results due to DLS as incomplete +area: Search +type: enhancement +issues: + - 88321 diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/termsenum/action/TransportTermsEnumAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/termsenum/action/TransportTermsEnumAction.java index 42729bebec94..c4a587814a54 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/termsenum/action/TransportTermsEnumAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/termsenum/action/TransportTermsEnumAction.java @@ -715,7 +715,20 @@ private void asyncNodeOperation(NodeTermsEnumRequest request, Task task, ActionL ThreadContext threadContext = transportService.getThreadPool().getThreadContext(); final XPackLicenseState frozenLicenseState = licenseState.copyCurrentLicenseState(); for (ShardId shardId : request.shardIds().toArray(new ShardId[0])) { - if (canAccess(shardId, request, frozenLicenseState, threadContext) == false || canMatchShard(shardId, request) == false) { + if (canAccess(shardId, request, frozenLicenseState, threadContext) == false) { + listener.onResponse( + new NodeTermsEnumResponse( + request.nodeId(), + Collections.emptyList(), + "cannot execute [_terms_enum] request on index [" + + shardId.getIndexName() + + "] due to " + + "DLS/FLS security restrictions.", + false + ) + ); + } + if (canMatchShard(shardId, request) == false) { // Permission denied or can't match, remove shardID from request request.remove(shardId); } diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/terms_enum/10_basic.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/terms_enum/10_basic.yml index 359e14b935f8..a592768d8336 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/terms_enum/10_basic.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/terms_enum/10_basic.yml @@ -549,18 +549,25 @@ teardown: - length: { terms: 1 } - do: - headers: { Authorization: "Basic ZGxzX3NvbWVfdXNlcjp4LXBhY2stdGVzdC1wYXNzd29yZA==" } # dls_some_user sees selected docs + headers: { Authorization: "Basic ZGxzX3NvbWVfdXNlcjp4LXBhY2stdGVzdC1wYXNzd29yZA==" } # dls_some_user doesn't see all docs terms_enum: - index: test_security - body: {"field": "foo", "string":"b"} - - length: {terms: 0} + index: test_security + body: { "field": "foo", "string": "b" } + + - length: { terms: 0 } + - match: { complete: false } + - match: { _shards.failed: 1 } + - match: { _shards.failures.0.reason.type: "broadcast_shard_operation_failed_exception" } + - match: { _shards.failures.0.reason.reason: "cannot execute [_terms_enum] request on index [test_security] due to DLS/FLS security restrictions." } - do: headers: { Authorization: "Basic ZmxzX3VzZXI6eC1wYWNrLXRlc3QtcGFzc3dvcmQ=" } # fls_user can't see field terms_enum: - index: test_security - body: {"field": "foo", "string":"b"} - - length: {terms: 0} + index: test_security + body: { "field": "foo", "string": "b" } + - length: { terms: 0 } + - match: { complete: true } + - match: { _shards.failed: 0 } --- "Test security with API keys": @@ -612,7 +619,7 @@ teardown: } } - match: { name: "dls_all_user_bad_key" } - - set: { encoded: login_creds} + - set: { encoded: login_creds } - do: headers: Authorization: ApiKey ${login_creds} # dls_all_user bad API key sees selected docs @@ -620,6 +627,9 @@ teardown: index: test_security body: { "field": "foo", "string": "b" } - length: { terms: 0 } + - match: { complete: false } + - match: { _shards.failed: 1 } + - match: { _shards.failures.0.reason.type: "broadcast_shard_operation_failed_exception" } - do: headers: { Authorization: "Basic ZGxzX3NvbWVfdXNlcjp4LXBhY2stdGVzdC1wYXNzd29yZA==" } # dls_some_user @@ -641,7 +651,7 @@ teardown: } } - match: { name: "dls_some_user_key" } - - set: { encoded: login_creds} + - set: { encoded: login_creds } - do: headers: Authorization: ApiKey ${login_creds} # dls_some_user's API key sees selected user regardless of the key's role descriptor @@ -649,3 +659,6 @@ teardown: index: test_security body: { "field": "foo", "string": "b" } - length: { terms: 0 } + - match: { complete: false } + - match: { _shards.failed: 1 } + - match: { _shards.failures.0.reason.type: "broadcast_shard_operation_failed_exception" } From 4ee518c22fd0f60f571b28245c3274f5362702a4 Mon Sep 17 00:00:00 2001 From: Artem Prigoda Date: Thu, 1 Dec 2022 12:02:30 +0100 Subject: [PATCH 126/919] [CI] Mute MaxDocsLimitIT.testMaxDocsLimit (#92038) See https://github.com/elastic/elasticsearch/issues/92037 --- .../java/org/elasticsearch/index/engine/MaxDocsLimitIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/index/engine/MaxDocsLimitIT.java b/server/src/internalClusterTest/java/org/elasticsearch/index/engine/MaxDocsLimitIT.java index 9e392a8c5fa2..b459cb4c4db3 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/index/engine/MaxDocsLimitIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/index/engine/MaxDocsLimitIT.java @@ -72,6 +72,7 @@ public void restoreMaxDocs() { restoreIndexWriterMaxDocs(); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/92037") public void testMaxDocsLimit() throws Exception { internalCluster().ensureAtLeastNumDataNodes(1); assertAcked( From c098a6c72c4ae001b879b4bc646f838143f60741 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Thu, 1 Dec 2022 13:17:04 +0100 Subject: [PATCH 127/919] Upgrade to Netty 4.1.85 (#91846) Just staying up to date. Also, removed the long deprecated and unused tiny cache size configuration from the allocator while I was at it. --- build-tools-internal/version.properties | 2 +- docs/changelog/91846.yaml | 5 ++ gradle/verification-metadata.xml | 78 +++++++++---------- .../transport/netty4/NettyAllocator.java | 2 - 4 files changed, 45 insertions(+), 42 deletions(-) create mode 100644 docs/changelog/91846.yaml diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index f7f5b5c670cf..5ee8391c28e2 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -17,7 +17,7 @@ ecsLogging = 1.2.0 jna = 5.10.0 -netty = 4.1.84.Final +netty = 4.1.85.Final commons_lang3 = 3.9 diff --git a/docs/changelog/91846.yaml b/docs/changelog/91846.yaml new file mode 100644 index 000000000000..3c9db04c8337 --- /dev/null +++ b/docs/changelog/91846.yaml @@ -0,0 +1,5 @@ +pr: 91846 +summary: Upgrade to Netty 4.1.85 +area: Network +type: upgrade +issues: [] diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index ab7c66f5678d..6789634a99ec 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1189,9 +1189,9 @@ - - - + + + @@ -1199,29 +1199,29 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + @@ -1229,9 +1229,9 @@ - - - + + + @@ -1239,14 +1239,14 @@ - - - + + + - - - + + + @@ -1254,14 +1254,14 @@ - - - + + + - - - + + + @@ -1269,9 +1269,9 @@ - - - + + + @@ -1284,9 +1284,9 @@ - - - + + + diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/NettyAllocator.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/NettyAllocator.java index 1a73c0d1a648..ea999ce0f471 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/NettyAllocator.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/NettyAllocator.java @@ -98,7 +98,6 @@ public class NettyAllocator { maxOrder = 5; } } - int tinyCacheSize = PooledByteBufAllocator.defaultTinyCacheSize(); int smallCacheSize = PooledByteBufAllocator.defaultSmallCacheSize(); int normalCacheSize = PooledByteBufAllocator.defaultNormalCacheSize(); boolean useCacheForAllThreads = PooledByteBufAllocator.defaultUseCacheForAllThreads(); @@ -108,7 +107,6 @@ public class NettyAllocator { 0, pageSize, maxOrder, - tinyCacheSize, smallCacheSize, normalCacheSize, useCacheForAllThreads From d80f4a68afbc49ef6e2defd5f91a298f7825480e Mon Sep 17 00:00:00 2001 From: Anthony McGlone <102866938+anthonymcglone2022@users.noreply.github.com> Date: Thu, 1 Dec 2022 13:50:06 +0000 Subject: [PATCH 128/919] [DOCS] Add missing xpack security setting (#91995) * [DOCS] Add missing xpack security setting * [DOCS] CR feedback for missing xpack security setting * Update docs/reference/settings/security-settings.asciidoc Co-authored-by: Abdon Pijpelink Co-authored-by: Abdon Pijpelink --- docs/reference/settings/security-settings.asciidoc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/reference/settings/security-settings.asciidoc b/docs/reference/settings/security-settings.asciidoc index 21d53aeebf89..ed1ce8d72f3f 100644 --- a/docs/reference/settings/security-settings.asciidoc +++ b/docs/reference/settings/security-settings.asciidoc @@ -45,6 +45,18 @@ starting {es} for the first time, which means that you must <>. -- +`xpack.security.enrollment.enabled`:: +(<>) +Defaults to `false`. Controls enrollment (of nodes and {kib}) to a local node +that's been <>. +When set to `true`, the local node can generate new enrollment tokens. Existing +tokens can be used for enrollment if they are still valid. ++ +-- +The security autoconfiguration process will set this to `true` unless +an administrator sets it to `false` before starting {es}. +-- + `xpack.security.hide_settings`:: (<>) A comma-separated list of settings that are omitted from the results of the From d5ee6ab648f372c8c5ba5f2a3a5f4d6d91235d4a Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Thu, 1 Dec 2022 17:11:29 +0100 Subject: [PATCH 129/919] Fix Chunked APIs sending incorrect responses to HEAD requests (#92042) Response bodies must always be empty for HEAD requests. Since the request encoder does not know that its dealing with a response to a HEAD request we have to indicate this fact to it. Also, needed to adjust the test http client to use the http-codec so it is able to correlate what responses are meant for HEAD requests and will correctly read responses for HEAD requests. Without this change the added test reproduces the extra bytes and fails with an assert about more than one response received. closes #92032 --- docs/changelog/92042.yaml | 6 ++ .../netty4/Netty4HttpServerTransport.java | 15 +++- .../http/netty4/Netty4HttpClient.java | 6 +- .../Netty4HttpServerTransportTests.java | 69 +++++++++++++++++-- 4 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 docs/changelog/92042.yaml diff --git a/docs/changelog/92042.yaml b/docs/changelog/92042.yaml new file mode 100644 index 000000000000..afec680949ed --- /dev/null +++ b/docs/changelog/92042.yaml @@ -0,0 +1,6 @@ +pr: 92042 +summary: Fix Chunked APIs sending incorrect responses to HEAD requests +area: Network +type: bug +issues: + - 92032 diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpServerTransport.java b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpServerTransport.java index b2550d6ec42a..efd970411701 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpServerTransport.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpServerTransport.java @@ -24,7 +24,9 @@ import io.netty.handler.codec.http.HttpContentDecompressor; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseEncoder; +import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.timeout.ReadTimeoutException; import io.netty.handler.timeout.ReadTimeoutHandler; import io.netty.util.AttributeKey; @@ -324,7 +326,18 @@ protected void initChannel(Channel ch) throws Exception { ch.pipeline() .addLast("decoder", decoder) .addLast("decoder_compress", new HttpContentDecompressor()) - .addLast("encoder", new HttpResponseEncoder()) + .addLast("encoder", new HttpResponseEncoder() { + @Override + protected boolean isContentAlwaysEmpty(HttpResponse msg) { + // non-chunked responses (Netty4HttpResponse extends Netty's DefaultFullHttpResponse) with chunked transfer + // encoding are only sent by us in response to HEAD requests and must always have an empty body + if (msg instanceof Netty4HttpResponse netty4HttpResponse && HttpUtil.isTransferEncodingChunked(msg)) { + assert netty4HttpResponse.content().isReadable() == false; + return true; + } + return super.isContentAlwaysEmpty(msg); + } + }) .addLast("aggregator", aggregator); if (handlingSettings.compression()) { ch.pipeline().addLast("encoder_compress", new HttpContentCompressor(handlingSettings.compressionLevel())); diff --git a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpClient.java b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpClient.java index eb510c50159a..2524be154414 100644 --- a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpClient.java +++ b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpClient.java @@ -21,15 +21,14 @@ import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpContentDecompressor; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpRequestEncoder; import io.netty.handler.codec.http.HttpResponse; -import io.netty.handler.codec.http.HttpResponseDecoder; import io.netty.handler.codec.http.HttpVersion; import org.elasticsearch.common.unit.ByteSizeUnit; @@ -175,8 +174,7 @@ private static class CountDownLatchHandler extends ChannelInitializer() { diff --git a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerTransportTests.java b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerTransportTests.java index 087f277c819e..e0ad1b697ad9 100644 --- a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerTransportTests.java +++ b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerTransportTests.java @@ -40,6 +40,7 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.network.NetworkAddress; import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.settings.ClusterSettings; @@ -47,8 +48,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.unit.ByteSizeValue; -import org.elasticsearch.common.util.MockPageCacheRecycler; -import org.elasticsearch.common.util.PageCacheRecycler; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.core.TimeValue; import org.elasticsearch.http.AbstractHttpServerTransportTestCase; @@ -57,6 +56,7 @@ import org.elasticsearch.http.HttpServerTransport; import org.elasticsearch.http.HttpTransportSettings; import org.elasticsearch.http.NullDispatcher; +import org.elasticsearch.rest.ChunkedRestResponseBody; import org.elasticsearch.rest.RestChannel; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestResponse; @@ -66,6 +66,7 @@ import org.elasticsearch.tracing.Tracer; import org.elasticsearch.transport.netty4.NettyAllocator; import org.elasticsearch.transport.netty4.SharedGroupFactory; +import org.elasticsearch.xcontent.ToXContent; import org.junit.After; import org.junit.Before; @@ -94,14 +95,12 @@ public class Netty4HttpServerTransportTests extends AbstractHttpServerTransportT private NetworkService networkService; private ThreadPool threadPool; - private PageCacheRecycler recycler; private ClusterSettings clusterSettings; @Before public void setup() throws Exception { networkService = new NetworkService(Collections.emptyList()); threadPool = new TestThreadPool("test"); - recycler = new MockPageCacheRecycler(Settings.EMPTY); clusterSettings = randomClusterSettings(); } @@ -112,7 +111,6 @@ public void shutdown() throws Exception { } threadPool = null; networkService = null; - recycler = null; clusterSettings = null; } @@ -560,6 +558,67 @@ protected void initChannel(SocketChannel ch) { } } + public void testHeadRequestToChunkedApi() throws InterruptedException { + final HttpServerTransport.Dispatcher dispatcher = new HttpServerTransport.Dispatcher() { + + @Override + public void dispatchRequest(final RestRequest request, final RestChannel channel, final ThreadContext threadContext) { + try { + channel.sendResponse( + new RestResponse( + OK, + ChunkedRestResponseBody.fromXContent( + ignored -> Iterators.single( + (builder, params) -> { throw new AssertionError("should not be called for HEAD REQUEST"); } + ), + ToXContent.EMPTY_PARAMS, + channel + ) + ) + ); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + @Override + public void dispatchBadRequest(final RestChannel channel, final ThreadContext threadContext, final Throwable cause) { + throw new AssertionError(); + } + + }; + + final Settings settings = createSettings(); + try ( + Netty4HttpServerTransport transport = new Netty4HttpServerTransport( + settings, + networkService, + threadPool, + xContentRegistry(), + dispatcher, + clusterSettings, + new SharedGroupFactory(settings), + Tracer.NOOP + ) + ) { + transport.start(); + final TransportAddress remoteAddress = randomFrom(transport.boundAddress().boundAddresses()); + + try (Netty4HttpClient client = new Netty4HttpClient()) { + final String url = "/some-head-endpoint"; + final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.HEAD, url); + + final FullHttpResponse response = client.send(remoteAddress.address(), request); + try { + assertThat(response.status(), equalTo(HttpResponseStatus.OK)); + assertFalse(response.content().isReadable()); + } finally { + response.release(); + } + } + } + } + private Settings createSettings() { return createBuilderWithPort().build(); } From 5d1204b462160d32d8754984290248954408ae1c Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 1 Dec 2022 16:58:11 +0000 Subject: [PATCH 130/919] Chunked encoding for RestGetIndicesAction (#92034) This response scales with the number of indices requested and can reach many MiB in size in a large cluster, let's use chunking here. Relates #89838 --- .../rest/Netty4HeadBodyIsEmptyIT.java | 8 +- .../elasticsearch/action/ActionModule.java | 2 +- .../admin/indices/get/GetIndexResponse.java | 95 ++++++++++--------- .../admin/indices/RestGetIndicesAction.java | 14 +-- .../indices/get/GetIndexResponseTests.java | 19 ++++ .../indices/RestGetIndicesActionTests.java | 5 +- 6 files changed, 77 insertions(+), 66 deletions(-) diff --git a/modules/transport-netty4/src/javaRestTest/java/org/elasticsearch/rest/Netty4HeadBodyIsEmptyIT.java b/modules/transport-netty4/src/javaRestTest/java/org/elasticsearch/rest/Netty4HeadBodyIsEmptyIT.java index b1e2a6d7fdd1..e705c4303687 100644 --- a/modules/transport-netty4/src/javaRestTest/java/org/elasticsearch/rest/Netty4HeadBodyIsEmptyIT.java +++ b/modules/transport-netty4/src/javaRestTest/java/org/elasticsearch/rest/Netty4HeadBodyIsEmptyIT.java @@ -25,6 +25,7 @@ import static org.elasticsearch.rest.RestStatus.OK; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.nullValue; public class Netty4HeadBodyIsEmptyIT extends ESRestTestCase { public void testHeadRoot() throws IOException { @@ -59,8 +60,8 @@ public void testDocumentExists() throws IOException { public void testIndexExists() throws IOException { createTestDoc(); - headTestCase("/test", emptyMap(), greaterThan(0)); - headTestCase("/test", singletonMap("pretty", "true"), greaterThan(0)); + headTestCase("/test", emptyMap(), nullValue(Integer.class)); + headTestCase("/test", singletonMap("pretty", "true"), nullValue(Integer.class)); } public void testAliasExists() throws IOException { @@ -177,7 +178,8 @@ private void headTestCase( request.setOptions(expectWarnings(expectedWarnings)); Response response = client().performRequest(request); assertEquals(expectedStatusCode, response.getStatusLine().getStatusCode()); - assertThat(Integer.valueOf(response.getHeader("Content-Length")), matcher); + final var contentLength = response.getHeader("Content-Length"); + assertThat(contentLength == null ? null : Integer.valueOf(contentLength), matcher); assertNull("HEAD requests shouldn't have a response body but " + url + " did", response.getEntity()); } diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index 40ee8e4db171..59d055e27415 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -768,7 +768,7 @@ public void initRestHandlers(Supplier nodesInCluster) { registerHandler.accept(new RestResetFeatureStateAction()); registerHandler.accept(new RestGetFeatureUpgradeStatusAction()); registerHandler.accept(new RestPostFeatureUpgradeAction()); - registerHandler.accept(new RestGetIndicesAction(threadPool)); + registerHandler.accept(new RestGetIndicesAction()); registerHandler.accept(new RestIndicesStatsAction()); registerHandler.accept(new RestIndicesSegmentsAction()); registerHandler.accept(new RestIndicesShardStoresAction()); diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/get/GetIndexResponse.java b/server/src/main/java/org/elasticsearch/action/admin/indices/get/GetIndexResponse.java index 4f8f24ea5b72..1e96b950c7a1 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/get/GetIndexResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/get/GetIndexResponse.java @@ -13,16 +13,18 @@ import org.elasticsearch.cluster.metadata.AliasMetadata; import org.elasticsearch.cluster.metadata.MappingMetadata; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.core.RestApiVersion; import org.elasticsearch.index.mapper.MapperService; -import org.elasticsearch.xcontent.ToXContentObject; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import java.io.IOException; import java.util.Arrays; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -33,7 +35,7 @@ /** * A response for a get index action. */ -public class GetIndexResponse extends ActionResponse implements ToXContentObject { +public class GetIndexResponse extends ActionResponse implements ChunkedToXContent { private Map mappings = Map.of(); private Map> aliases = Map.of(); @@ -178,59 +180,58 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - { - for (final String index : indices) { + public Iterator toXContentChunked(ToXContent.Params ignored) { + return Iterators.concat( + Iterators.single((builder, params) -> builder.startObject()), + Arrays.stream(indices).map(index -> (builder, params) -> { builder.startObject(index); - { - builder.startObject("aliases"); - List indexAliases = aliases.get(index); - if (indexAliases != null) { - for (final AliasMetadata alias : indexAliases) { - AliasMetadata.Builder.toXContent(alias, builder, params); - } - } - builder.endObject(); - - MappingMetadata indexMappings = mappings.get(index); - if (indexMappings == null) { - builder.startObject("mappings").endObject(); - } else { - if (builder.getRestApiVersion() == RestApiVersion.V_7 - && params.paramAsBoolean(INCLUDE_TYPE_NAME_PARAMETER, DEFAULT_INCLUDE_TYPE_NAME_POLICY)) { - builder.startObject("mappings"); - builder.field(MapperService.SINGLE_MAPPING_NAME, indexMappings.sourceAsMap()); - builder.endObject(); - } else { - builder.field("mappings", indexMappings.sourceAsMap()); - } - } - builder.startObject("settings"); - Settings indexSettings = settings.get(index); - if (indexSettings != null) { - indexSettings.toXContent(builder, params); + builder.startObject("aliases"); + List indexAliases = aliases.get(index); + if (indexAliases != null) { + for (final AliasMetadata alias : indexAliases) { + AliasMetadata.Builder.toXContent(alias, builder, params); } - builder.endObject(); + } + builder.endObject(); - Settings defaultIndexSettings = defaultSettings.get(index); - if (defaultIndexSettings != null && defaultIndexSettings.isEmpty() == false) { - builder.startObject("defaults"); - defaultIndexSettings.toXContent(builder, params); + MappingMetadata indexMappings = mappings.get(index); + if (indexMappings == null) { + builder.startObject("mappings").endObject(); + } else { + if (builder.getRestApiVersion() == RestApiVersion.V_7 + && params.paramAsBoolean(INCLUDE_TYPE_NAME_PARAMETER, DEFAULT_INCLUDE_TYPE_NAME_POLICY)) { + builder.startObject("mappings"); + builder.field(MapperService.SINGLE_MAPPING_NAME, indexMappings.sourceAsMap()); builder.endObject(); + } else { + builder.field("mappings", indexMappings.sourceAsMap()); } + } - String dataStream = dataStreams.get(index); - if (dataStream != null) { - builder.field("data_stream", dataStream); - } + builder.startObject("settings"); + Settings indexSettings = settings.get(index); + if (indexSettings != null) { + indexSettings.toXContent(builder, params); } builder.endObject(); - } - } - builder.endObject(); - return builder; + + Settings defaultIndexSettings = defaultSettings.get(index); + if (defaultIndexSettings != null && defaultIndexSettings.isEmpty() == false) { + builder.startObject("defaults"); + defaultIndexSettings.toXContent(builder, params); + builder.endObject(); + } + + String dataStream = dataStreams.get(index); + if (dataStream != null) { + builder.field("data_stream", dataStream); + } + + return builder.endObject(); + }).iterator(), + Iterators.single((builder, params) -> builder.endObject()) + ); } @Override diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesAction.java index 11c725695aa1..46a686d6e74b 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesAction.java @@ -17,9 +17,8 @@ import org.elasticsearch.core.RestApiVersion; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; -import org.elasticsearch.rest.action.DispatchingRestToXContentListener; import org.elasticsearch.rest.action.RestCancellableNodeClient; -import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.rest.action.RestChunkedToXContentListener; import java.io.IOException; import java.util.List; @@ -39,12 +38,6 @@ public class RestGetIndicesAction extends BaseRestHandler { private static final Set COMPATIBLE_RESPONSE_PARAMS = addToCopy(Settings.FORMAT_PARAMS, INCLUDE_TYPE_NAME_PARAMETER); - private final ThreadPool threadPool; - - public RestGetIndicesAction(ThreadPool threadPool) { - this.threadPool = threadPool; - } - @Override public List routes() { return List.of(new Route(GET, "/{index}"), new Route(HEAD, "/{index}")); @@ -76,10 +69,7 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC final var httpChannel = request.getHttpChannel(); return channel -> new RestCancellableNodeClient(client, httpChannel).admin() .indices() - .getIndex( - getIndexRequest, - new DispatchingRestToXContentListener<>(threadPool.executor(ThreadPool.Names.MANAGEMENT), channel, request) - ); + .getIndex(getIndexRequest, new RestChunkedToXContentListener<>(channel)); } /** diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/get/GetIndexResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/get/GetIndexResponseTests.java index 3f6f0baaabf2..85ab886542a8 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/get/GetIndexResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/get/GetIndexResponseTests.java @@ -18,7 +18,9 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.RandomCreateIndexGenerator; import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.xcontent.ToXContent; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -27,6 +29,9 @@ import java.util.Locale; import java.util.Map; +import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; +import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; + public class GetIndexResponseTests extends AbstractWireSerializingTestCase { @Override @@ -73,4 +78,18 @@ protected GetIndexResponse createTestInstance() { } return new GetIndexResponse(indices, mappings, aliases, settings, defaultSettings, dataStreams); } + + public void testChunking() throws IOException { + final var response = createTestInstance(); + + try (var builder = jsonBuilder()) { + int chunkCount = 0; + final var iterator = response.toXContentChunked(EMPTY_PARAMS); + while (iterator.hasNext()) { + iterator.next().toXContent(builder, ToXContent.EMPTY_PARAMS); + chunkCount += 1; + } + assertEquals(response.getIndices().length + 2, chunkCount); + } // closing the builder verifies that the XContent is well-formed + } } diff --git a/server/src/test/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesActionTests.java b/server/src/test/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesActionTests.java index 164fc38d15c4..5885c6f8c988 100644 --- a/server/src/test/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesActionTests.java +++ b/server/src/test/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesActionTests.java @@ -9,7 +9,6 @@ package org.elasticsearch.rest.action.admin.indices; import org.elasticsearch.client.internal.node.NodeClient; -import org.elasticsearch.common.util.concurrent.DeterministicTaskQueue; import org.elasticsearch.core.RestApiVersion; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.test.ESTestCase; @@ -37,7 +36,7 @@ public void testIncludeTypeNamesWarning() throws IOException { Map.of("Content-Type", contentTypeHeader, "Accept", contentTypeHeader) ).withMethod(RestRequest.Method.GET).withPath("/some_index").withParams(params).build(); - RestGetIndicesAction handler = new RestGetIndicesAction(new DeterministicTaskQueue().getThreadPool()); + RestGetIndicesAction handler = new RestGetIndicesAction(); handler.prepareRequest(request, mock(NodeClient.class)); assertCriticalWarnings(RestGetIndicesAction.TYPES_DEPRECATION_MESSAGE); @@ -58,7 +57,7 @@ public void testIncludeTypeNamesWarningExists() throws IOException { Map.of("Content-Type", contentTypeHeader, "Accept", contentTypeHeader) ).withMethod(RestRequest.Method.HEAD).withPath("/some_index").withParams(params).build(); - RestGetIndicesAction handler = new RestGetIndicesAction(new DeterministicTaskQueue().getThreadPool()); + RestGetIndicesAction handler = new RestGetIndicesAction(); handler.prepareRequest(request, mock(NodeClient.class)); } } From c43571bbb63ba464bcb92f94b5471eeba7545e87 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 1 Dec 2022 17:24:58 +0000 Subject: [PATCH 131/919] AwaitsFix for #92039 --- .../org/elasticsearch/xpack/profiler/GetProfilingActionIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/GetProfilingActionIT.java b/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/GetProfilingActionIT.java index 0c6038186c91..7c3b7a056971 100644 --- a/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/GetProfilingActionIT.java +++ b/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/GetProfilingActionIT.java @@ -154,6 +154,7 @@ public void testGetProfilingDataUnfiltered() throws Exception { assertNotNull("libc.so.6", response.getExecutables().get("QCCDqjSg3bMK1C4YRK6Tiw")); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/92039") public void testAutomaticCancellation() throws Exception { Request restRequest = new Request("POST", "/_profiling/stacktraces"); restRequest.setEntity(new StringEntity(""" From a720b946179787b9c9159371737c6f2ba91d2a3e Mon Sep 17 00:00:00 2001 From: Rene Groeschke Date: Fri, 2 Dec 2022 00:45:53 +0100 Subject: [PATCH 132/919] Use elasticsearch DRAs in example plugin build (#92053) This should fix our example build by resolving elasticsearch artifacts from the synced DRA location --- plugins/examples/build.gradle | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/examples/build.gradle b/plugins/examples/build.gradle index 263d73b128f0..e983292f7ab3 100644 --- a/plugins/examples/build.gradle +++ b/plugins/examples/build.gradle @@ -29,6 +29,14 @@ subprojects { } } } + if (gradle.includedBuilds.isEmpty()) { + maven { + url = "https://artifacts-snapshot.elastic.co/elasticsearch/${elasticsearchVersion}/maven" + mavenContent { + includeModule 'org.elasticsearch', 'elasticsearch' + } + } + } // Same for Lucene, add the snapshot repo based on the currently used Lucene version def luceneVersion = VersionProperties.getLucene() From a721c66ead984a2be4dc1bd1769908c7caa1a08b Mon Sep 17 00:00:00 2001 From: Daniel Mitterdorfer Date: Fri, 2 Dec 2022 06:59:44 +0100 Subject: [PATCH 133/919] Correct assertions on profiling action cancel (#92046) With this commit we address two issues when testing cancellation of the profiling task: 1. We add the actual profiling task to the list of tasks to check, not only its child tasks. 2. The original test contained a race where child tasks might have been unregistered (expectedly) in between collecting tasks and checking for cancelled tasks. Now we only check all remaining tasks have been cancelled. Co-authored-by: David Turner Closes #92039 --- .../xpack/profiler/GetProfilingActionIT.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/GetProfilingActionIT.java b/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/GetProfilingActionIT.java index 7c3b7a056971..dd9d43e21374 100644 --- a/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/GetProfilingActionIT.java +++ b/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/GetProfilingActionIT.java @@ -154,7 +154,6 @@ public void testGetProfilingDataUnfiltered() throws Exception { assertNotNull("libc.so.6", response.getExecutables().get("QCCDqjSg3bMK1C4YRK6Tiw")); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/92039") public void testAutomaticCancellation() throws Exception { Request restRequest = new Request("POST", "/_profiling/stacktraces"); restRequest.setEntity(new StringEntity(""" @@ -223,7 +222,13 @@ private static Collection collectProfilingRelatedTasks(String transportA } } assertNotNull(profilingTask.get()); - return taskToParent.get(profilingTask.get().taskId()); + Set childTaskIds = taskToParent.get(profilingTask.get().taskId()); + Set profilingTaskIds = new HashSet<>(); + profilingTaskIds.add(profilingTask.get().taskId()); + if (childTaskIds != null) { + profilingTaskIds.addAll(childTaskIds); + } + return profilingTaskIds; } private static void ensureTasksAreCancelled(Collection taskIds, Function nodeIdToName) throws Exception { @@ -232,8 +237,12 @@ private static void ensureTasksAreCancelled(Collection taskIds, Function String nodeName = nodeIdToName.apply(taskId.getNodeId()); TaskManager taskManager = internalCluster().getInstance(TransportService.class, nodeName).getTaskManager(); Task task = taskManager.getTask(taskId.getId()); - assertThat(task, instanceOf(CancellableTask.class)); - assertTrue(((CancellableTask) task).isCancelled()); + // as we capture the task hierarchy at the beginning but cancel in the middle of execution, some tasks have been + // unregistered already by the time we verify cancellation. + if (task != null) { + assertThat(task, instanceOf(CancellableTask.class)); + assertTrue(((CancellableTask) task).isCancelled()); + } } }); } From 0ed03469988147f9babf778dc0020c671c292bd0 Mon Sep 17 00:00:00 2001 From: Howard Date: Fri, 2 Dec 2022 16:32:27 +0800 Subject: [PATCH 134/919] Fix typo in create index cluster state update request javadocs (#92035) --- .../indices/create/CreateIndexClusterStateUpdateRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexClusterStateUpdateRequest.java b/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexClusterStateUpdateRequest.java index 9e3cb34e9a5d..8a46daa45e73 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexClusterStateUpdateRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexClusterStateUpdateRequest.java @@ -190,7 +190,7 @@ public CreateIndexClusterStateUpdateRequest performReroute(boolean performRerout } /** - * @return The composable index template that matches with the index that will be cretaed by this request. + * @return The composable index template that matches with the index that will be created by this request. */ public ComposableIndexTemplate matchingTemplate() { return matchingTemplate; From eeab493607319aaeb33098facd63a0f295c1f969 Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Fri, 2 Dec 2022 09:59:06 +0100 Subject: [PATCH 135/919] Avoid intermediate collection creation when checking relocating shards count. (#91990) --- .../java/org/elasticsearch/health/node/LocalHealthMonitor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/health/node/LocalHealthMonitor.java b/server/src/main/java/org/elasticsearch/health/node/LocalHealthMonitor.java index 857dda9f0d46..24d01a0b0936 100644 --- a/server/src/main/java/org/elasticsearch/health/node/LocalHealthMonitor.java +++ b/server/src/main/java/org/elasticsearch/health/node/LocalHealthMonitor.java @@ -462,7 +462,7 @@ private DiskUsage getDiskUsage() { } private boolean hasRelocatingShards(ClusterState clusterState, String nodeId) { - return clusterState.getRoutingNodes().node(nodeId).shardsWithState(ShardRoutingState.RELOCATING).isEmpty() == false; + return clusterState.getRoutingNodes().node(nodeId).numberOfShardsWithState(ShardRoutingState.RELOCATING) > 0; } } } From 9139dd9e1d6c4ab8ee20ec527607b3a9dce17e54 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Fri, 2 Dec 2022 11:30:49 +0100 Subject: [PATCH 136/919] Deserialize responses on the handling thread-pool (#91367) This is the start of moving message deserialization off of the transport threads where possible. This PR introduces the basic facilities to ref count and fork serialization of transport message instances which already provides some tangible benefits to transport thread latencies. We can't not fork for large messages (which are mostly responses) in scenarios where responses can grow beyond O(1M) as this introduces unmanageable latency on the transport pool when e.g. deserializing a O(100M) cluster state or a similarly sized search response. --- docs/changelog/91367.yaml | 5 + .../transport/InboundAggregator.java | 4 +- .../transport/InboundHandler.java | 110 +++++++++++------- .../transport/InboundMessage.java | 16 +-- .../transport/InboundPipeline.java | 5 +- .../transport/InboundAggregatorTests.java | 2 +- 6 files changed, 92 insertions(+), 50 deletions(-) create mode 100644 docs/changelog/91367.yaml diff --git a/docs/changelog/91367.yaml b/docs/changelog/91367.yaml new file mode 100644 index 000000000000..2ec5842ee132 --- /dev/null +++ b/docs/changelog/91367.yaml @@ -0,0 +1,5 @@ +pr: 91367 +summary: Deserialize responses on the handling thread-pool +area: Network +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/transport/InboundAggregator.java b/server/src/main/java/org/elasticsearch/transport/InboundAggregator.java index 3c82c5459274..0dbd3f8fe00a 100644 --- a/server/src/main/java/org/elasticsearch/transport/InboundAggregator.java +++ b/server/src/main/java/org/elasticsearch/transport/InboundAggregator.java @@ -119,7 +119,7 @@ public InboundMessage finishAggregation() throws IOException { checkBreaker(aggregated.getHeader(), aggregated.getContentLength(), breakerControl); } if (isShortCircuited()) { - aggregated.close(); + aggregated.decRef(); success = true; return new InboundMessage(aggregated.getHeader(), aggregationException); } else { @@ -130,7 +130,7 @@ public InboundMessage finishAggregation() throws IOException { } finally { resetCurrentAggregation(); if (success == false) { - aggregated.close(); + aggregated.decRef(); } } } diff --git a/server/src/main/java/org/elasticsearch/transport/InboundHandler.java b/server/src/main/java/org/elasticsearch/transport/InboundHandler.java index 3ef4b5f6769b..c79f9b0cef3d 100644 --- a/server/src/main/java/org/elasticsearch/transport/InboundHandler.java +++ b/server/src/main/java/org/elasticsearch/transport/InboundHandler.java @@ -19,6 +19,8 @@ import org.elasticsearch.common.network.HandlingTimeTracker; import org.elasticsearch.common.util.concurrent.AbstractRunnable; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.core.Releasable; +import org.elasticsearch.core.Releasables; import org.elasticsearch.core.TimeValue; import org.elasticsearch.threadpool.ThreadPool; @@ -133,34 +135,17 @@ private void messageReceived(TcpChannel channel, InboundMessage message, long st } // ignore if its null, the service logs it if (responseHandler != null) { - final StreamInput streamInput; if (message.getContentLength() > 0 || header.getVersion().equals(Version.CURRENT) == false) { - streamInput = namedWriteableStream(message.openOrGetStreamInput()); + final StreamInput streamInput = namedWriteableStream(message.openOrGetStreamInput()); assertRemoteVersion(streamInput, header.getVersion()); if (header.isError()) { - handlerResponseError(streamInput, responseHandler); + handlerResponseError(streamInput, message, responseHandler); } else { - handleResponse(remoteAddress, streamInput, responseHandler); - } - // Check the entire message has been read - final int nextByte = streamInput.read(); - // calling read() is useful to make sure the message is fully read, even if there is an EOS marker - if (nextByte != -1) { - final IllegalStateException exception = new IllegalStateException( - "Message not fully read (response) for requestId [" - + requestId - + "], handler [" - + responseHandler - + "], error [" - + header.isError() - + "]; resetting" - ); - assert ignoreDeserializationErrors : exception; - throw exception; + handleResponse(remoteAddress, streamInput, responseHandler, message); } } else { assert header.isError() == false; - handleResponse(remoteAddress, EMPTY_STREAM_INPUT, responseHandler); + handleResponse(remoteAddress, EMPTY_STREAM_INPUT, responseHandler, message); } } } @@ -189,6 +174,26 @@ private void messageReceived(TcpChannel channel, InboundMessage message, long st } } + private void verifyResponseReadFully(Header header, TransportResponseHandler responseHandler, StreamInput streamInput) + throws IOException { + // Check the entire message has been read + final int nextByte = streamInput.read(); + // calling read() is useful to make sure the message is fully read, even if there is an EOS marker + if (nextByte != -1) { + final IllegalStateException exception = new IllegalStateException( + "Message not fully read (response) for requestId [" + + header.getRequestId() + + "], handler [" + + responseHandler + + "], error [" + + header.isError() + + "]; resetting" + ); + assert ignoreDeserializationErrors : exception; + throw exception; + } + } + private void handleRequest(TcpChannel channel, Header header, InboundMessage message) throws IOException { final String action = header.getActionName(); final long requestId = header.getRequestId(); @@ -335,10 +340,49 @@ private static void sendErrorResponse(String actionName, TransportChannel transp private void handleResponse( InetSocketAddress remoteAddress, final StreamInput stream, - final TransportResponseHandler handler + final TransportResponseHandler handler, + final InboundMessage inboundMessage + ) { + final String executor = handler.executor(); + if (ThreadPool.Names.SAME.equals(executor)) { + // no need to provide a buffer release here, we never escape the buffer when handling directly + doHandleResponse(handler, remoteAddress, stream, inboundMessage.getHeader(), () -> {}); + } else { + inboundMessage.incRef(); + // release buffer once we deserialize the message, but have a fail-safe in #onAfter below in case that didn't work out + final Releasable releaseBuffer = Releasables.releaseOnce(inboundMessage::decRef); + threadPool.executor(executor).execute(new ForkingResponseHandlerRunnable(handler, null) { + @Override + protected void doRun() { + doHandleResponse(handler, remoteAddress, stream, inboundMessage.getHeader(), releaseBuffer); + } + + @Override + public void onAfter() { + Releasables.closeExpectNoException(releaseBuffer); + } + }); + } + } + + /** + * + * @param handler response handler + * @param remoteAddress remote address that the message was sent from + * @param stream bytes stream for reading the message + * @param header message header + * @param releaseResponseBuffer releasable that will be released once the message has been read from the {@code stream} + * @param response message type + */ + private void doHandleResponse( + TransportResponseHandler handler, + InetSocketAddress remoteAddress, + final StreamInput stream, + final Header header, + Releasable releaseResponseBuffer ) { final T response; - try { + try (releaseResponseBuffer) { response = handler.read(stream); response.remoteAddress(remoteAddress); } catch (Exception e) { @@ -348,24 +392,11 @@ private void handleResponse( ); logger.warn(() -> "Failed to deserialize response from [" + remoteAddress + "]", serializationException); assert ignoreDeserializationErrors : e; - handleException(handler, serializationException); + doHandleException(handler, serializationException); return; } - final String executor = handler.executor(); - if (ThreadPool.Names.SAME.equals(executor)) { - doHandleResponse(handler, response); - } else { - threadPool.executor(executor).execute(new ForkingResponseHandlerRunnable(handler, null) { - @Override - protected void doRun() { - doHandleResponse(handler, response); - } - }); - } - } - - private static void doHandleResponse(TransportResponseHandler handler, T response) { try { + verifyResponseReadFully(header, handler, stream); handler.handleResponse(response); } catch (Exception e) { doHandleException(handler, new ResponseHandlerFailureTransportException(e)); @@ -374,10 +405,11 @@ private static void doHandleResponse(TransportResp } } - private void handlerResponseError(StreamInput stream, final TransportResponseHandler handler) { + private void handlerResponseError(StreamInput stream, InboundMessage message, final TransportResponseHandler handler) { Exception error; try { error = stream.readException(); + verifyResponseReadFully(message.getHeader(), handler, stream); } catch (Exception e) { error = new TransportSerializationException( "Failed to deserialize exception response from stream for handler [" + handler + "]", diff --git a/server/src/main/java/org/elasticsearch/transport/InboundMessage.java b/server/src/main/java/org/elasticsearch/transport/InboundMessage.java index 8d050ba19cef..d9ddbba60b8d 100644 --- a/server/src/main/java/org/elasticsearch/transport/InboundMessage.java +++ b/server/src/main/java/org/elasticsearch/transport/InboundMessage.java @@ -11,13 +11,14 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.bytes.ReleasableBytesReference; import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.core.AbstractRefCounted; import org.elasticsearch.core.IOUtils; import org.elasticsearch.core.Releasable; import java.io.IOException; import java.util.Objects; -public class InboundMessage implements Releasable { +public class InboundMessage extends AbstractRefCounted { private final Header header; private final ReleasableBytesReference content; @@ -82,6 +83,7 @@ public Releasable takeBreakerReleaseControl() { public StreamInput openOrGetStreamInput() throws IOException { assert isPing == false && content != null; + assert hasReferences(); if (streamInput == null) { streamInput = content.streamInput(); streamInput.setVersion(header.getVersion()); @@ -90,7 +92,12 @@ public StreamInput openOrGetStreamInput() throws IOException { } @Override - public void close() { + public String toString() { + return "InboundMessage{" + header + "}"; + } + + @Override + protected void closeInternal() { try { IOUtils.close(streamInput, content, breakerRelease); } catch (Exception e) { @@ -98,9 +105,4 @@ public void close() { throw new ElasticsearchException(e); } } - - @Override - public String toString() { - return "InboundMessage{" + header + "}"; - } } diff --git a/server/src/main/java/org/elasticsearch/transport/InboundPipeline.java b/server/src/main/java/org/elasticsearch/transport/InboundPipeline.java index 7a8cc9173abb..17744c86c50c 100644 --- a/server/src/main/java/org/elasticsearch/transport/InboundPipeline.java +++ b/server/src/main/java/org/elasticsearch/transport/InboundPipeline.java @@ -144,9 +144,12 @@ private void forwardFragments(TcpChannel channel, ArrayList fragments) t messageHandler.accept(channel, PING_MESSAGE); } else if (fragment == InboundDecoder.END_CONTENT) { assert aggregator.isAggregating(); - try (InboundMessage aggregated = aggregator.finishAggregation()) { + InboundMessage aggregated = aggregator.finishAggregation(); + try { statsTracker.markMessageReceived(); messageHandler.accept(channel, aggregated); + } finally { + aggregated.decRef(); } } else { assert aggregator.isAggregating(); diff --git a/server/src/test/java/org/elasticsearch/transport/InboundAggregatorTests.java b/server/src/test/java/org/elasticsearch/transport/InboundAggregatorTests.java index 97e7dddc720c..7e2aad9b5dbb 100644 --- a/server/src/test/java/org/elasticsearch/transport/InboundAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/transport/InboundAggregatorTests.java @@ -93,7 +93,7 @@ public void testInboundAggregation() throws IOException { for (ReleasableBytesReference reference : references) { assertTrue(reference.hasReferences()); } - aggregated.close(); + aggregated.decRef(); for (ReleasableBytesReference reference : references) { assertFalse(reference.hasReferences()); } From 4ebb23d20e42ea67d78cf6aa4f7b71841a19b551 Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Fri, 2 Dec 2022 13:35:02 +0100 Subject: [PATCH 137/919] Simplify usages of shardsWithState (#92055) --- .../cluster/routing/DelayedAllocationIT.java | 4 +- .../common/util/CollectionUtils.java | 2 +- .../allocation/AddIncrementallyTests.java | 78 +++++++++---------- .../allocation/AllocationCommandsTests.java | 68 ++++++++-------- .../allocation/BalanceConfigurationTests.java | 10 ++- .../DesiredBalanceReconcilerTests.java | 11 ++- .../decider/DiskThresholdDeciderTests.java | 6 +- .../cluster/routing/RoutingNodesHelper.java | 36 +++------ .../ReactiveStorageDeciderDecisionTests.java | 2 - .../NodeShutdownDelayedAllocationIT.java | 4 +- 10 files changed, 102 insertions(+), 119 deletions(-) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/cluster/routing/DelayedAllocationIT.java b/server/src/internalClusterTest/java/org/elasticsearch/cluster/routing/DelayedAllocationIT.java index 7bf202d64fbe..8172316e8a7b 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/cluster/routing/DelayedAllocationIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/cluster/routing/DelayedAllocationIT.java @@ -15,7 +15,6 @@ import org.elasticsearch.core.TimeValue; import org.elasticsearch.test.ESIntegTestCase; -import java.util.Collections; import java.util.List; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; @@ -191,7 +190,6 @@ private void indexRandomData() throws Exception { private String findNodeWithShard() { ClusterState state = client().admin().cluster().prepareState().get().getState(); List startedShards = RoutingNodesHelper.shardsWithState(state.getRoutingNodes(), ShardRoutingState.STARTED); - Collections.shuffle(startedShards, random()); - return state.nodes().get(startedShards.get(0).currentNodeId()).getName(); + return state.nodes().get(randomFrom(startedShards).currentNodeId()).getName(); } } diff --git a/server/src/main/java/org/elasticsearch/common/util/CollectionUtils.java b/server/src/main/java/org/elasticsearch/common/util/CollectionUtils.java index 79a177b48869..9eb24ea30c86 100644 --- a/server/src/main/java/org/elasticsearch/common/util/CollectionUtils.java +++ b/server/src/main/java/org/elasticsearch/common/util/CollectionUtils.java @@ -178,7 +178,7 @@ public static ArrayList iterableAsArrayList(Iterable element throw new NullPointerException("elements"); } if (elements instanceof Collection) { - return new ArrayList<>((Collection) elements); + return new ArrayList<>((Collection) elements); } else { ArrayList list = new ArrayList<>(); for (E element : elements) { diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/AddIncrementallyTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/AddIncrementallyTests.java index f2186b83564e..c78c78e7b5fd 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/AddIncrementallyTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/AddIncrementallyTests.java @@ -47,12 +47,12 @@ public void testAddNodesAndIndices() { AllocationService service = createAllocationService(settings.build()); ClusterState clusterState = initCluster(service, 1, 3, 3, 1); - assertThat(clusterState.getRoutingNodes().node("node0").shardsWithState(STARTED).size(), equalTo(9)); + assertThat(clusterState.getRoutingNodes().node("node0").numberOfShardsWithState(STARTED), equalTo(9)); assertThat(clusterState.getRoutingNodes().unassigned().size(), equalTo(9)); int nodeOffset = 1; clusterState = addNodes(clusterState, service, 1, nodeOffset++); - assertThat(clusterState.getRoutingNodes().node("node0").shardsWithState(STARTED).size(), equalTo(9)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(STARTED).size(), equalTo(9)); + assertThat(clusterState.getRoutingNodes().node("node0").numberOfShardsWithState(STARTED), equalTo(9)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(STARTED), equalTo(9)); assertThat(clusterState.getRoutingNodes().unassigned().size(), equalTo(0)); assertNumIndexShardsPerNode(clusterState, equalTo(3)); clusterState = addNodes(clusterState, service, 1, nodeOffset++); @@ -93,12 +93,12 @@ public void testMinimalRelocations() { AllocationService service = createAllocationService(settings.build()); ClusterState clusterState = initCluster(service, 1, 3, 3, 1); - assertThat(clusterState.getRoutingNodes().node("node0").shardsWithState(STARTED).size(), equalTo(9)); + assertThat(clusterState.getRoutingNodes().node("node0").numberOfShardsWithState(STARTED), equalTo(9)); assertThat(clusterState.getRoutingNodes().unassigned().size(), equalTo(9)); int nodeOffset = 1; clusterState = addNodes(clusterState, service, 1, nodeOffset++); - assertThat(clusterState.getRoutingNodes().node("node0").shardsWithState(STARTED).size(), equalTo(9)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(STARTED).size(), equalTo(9)); + assertThat(clusterState.getRoutingNodes().node("node0").numberOfShardsWithState(STARTED), equalTo(9)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(STARTED), equalTo(9)); assertThat(clusterState.getRoutingNodes().unassigned().size(), equalTo(0)); assertNumIndexShardsPerNode(clusterState, equalTo(3)); @@ -110,36 +110,36 @@ public void testMinimalRelocations() { clusterState = service.reroute(clusterState, "reroute", ActionListener.noop()); RoutingNodes routingNodes = clusterState.getRoutingNodes(); - assertThat(routingNodes.node("node2").shardsWithState(INITIALIZING).size(), equalTo(2)); - assertThat(routingNodes.node("node0").shardsWithState(INITIALIZING).size(), equalTo(0)); - assertThat(routingNodes.node("node1").shardsWithState(INITIALIZING).size(), equalTo(0)); + assertThat(routingNodes.node("node2").numberOfShardsWithState(INITIALIZING), equalTo(2)); + assertThat(routingNodes.node("node0").numberOfShardsWithState(INITIALIZING), equalTo(0)); + assertThat(routingNodes.node("node1").numberOfShardsWithState(INITIALIZING), equalTo(0)); ClusterState newState = startInitializingShardsAndReroute(service, clusterState); assertThat(newState, not(equalTo(clusterState))); clusterState = newState; routingNodes = clusterState.getRoutingNodes(); - assertThat(routingNodes.node("node2").shardsWithState(STARTED).size(), equalTo(2)); - assertThat(routingNodes.node("node2").shardsWithState(INITIALIZING).size(), equalTo(2)); - assertThat(routingNodes.node("node0").shardsWithState(INITIALIZING).size(), equalTo(0)); - assertThat(routingNodes.node("node1").shardsWithState(INITIALIZING).size(), equalTo(0)); + assertThat(routingNodes.node("node2").numberOfShardsWithState(STARTED), equalTo(2)); + assertThat(routingNodes.node("node2").numberOfShardsWithState(INITIALIZING), equalTo(2)); + assertThat(routingNodes.node("node0").numberOfShardsWithState(INITIALIZING), equalTo(0)); + assertThat(routingNodes.node("node1").numberOfShardsWithState(INITIALIZING), equalTo(0)); newState = startInitializingShardsAndReroute(service, clusterState); assertThat(newState, not(equalTo(clusterState))); clusterState = newState; routingNodes = clusterState.getRoutingNodes(); - assertThat(routingNodes.node("node2").shardsWithState(STARTED).size(), equalTo(4)); - assertThat(routingNodes.node("node2").shardsWithState(INITIALIZING).size(), equalTo(2)); - assertThat(routingNodes.node("node0").shardsWithState(INITIALIZING).size(), equalTo(0)); - assertThat(routingNodes.node("node1").shardsWithState(INITIALIZING).size(), equalTo(0)); + assertThat(routingNodes.node("node2").numberOfShardsWithState(STARTED), equalTo(4)); + assertThat(routingNodes.node("node2").numberOfShardsWithState(INITIALIZING), equalTo(2)); + assertThat(routingNodes.node("node0").numberOfShardsWithState(INITIALIZING), equalTo(0)); + assertThat(routingNodes.node("node1").numberOfShardsWithState(INITIALIZING), equalTo(0)); newState = startInitializingShardsAndReroute(service, clusterState); assertThat(newState, not(equalTo(clusterState))); clusterState = newState; routingNodes = clusterState.getRoutingNodes(); - assertThat(routingNodes.node("node2").shardsWithState(STARTED).size(), equalTo(6)); - assertThat(routingNodes.node("node2").shardsWithState(INITIALIZING).size(), equalTo(0)); - assertThat(routingNodes.node("node0").shardsWithState(INITIALIZING).size(), equalTo(0)); - assertThat(routingNodes.node("node1").shardsWithState(INITIALIZING).size(), equalTo(0)); + assertThat(routingNodes.node("node2").numberOfShardsWithState(STARTED), equalTo(6)); + assertThat(routingNodes.node("node2").numberOfShardsWithState(INITIALIZING), equalTo(0)); + assertThat(routingNodes.node("node0").numberOfShardsWithState(INITIALIZING), equalTo(0)); + assertThat(routingNodes.node("node1").numberOfShardsWithState(INITIALIZING), equalTo(0)); newState = startInitializingShardsAndReroute(service, clusterState); assertThat(newState, equalTo(clusterState)); @@ -158,12 +158,12 @@ public void testMinimalRelocationsNoLimit() { AllocationService service = createAllocationService(settings.build()); ClusterState clusterState = initCluster(service, 1, 3, 3, 1); - assertThat(clusterState.getRoutingNodes().node("node0").shardsWithState(STARTED).size(), equalTo(9)); + assertThat(clusterState.getRoutingNodes().node("node0").numberOfShardsWithState(STARTED), equalTo(9)); assertThat(clusterState.getRoutingNodes().unassigned().size(), equalTo(9)); int nodeOffset = 1; clusterState = addNodes(clusterState, service, 1, nodeOffset++); - assertThat(clusterState.getRoutingNodes().node("node0").shardsWithState(STARTED).size(), equalTo(9)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(STARTED).size(), equalTo(9)); + assertThat(clusterState.getRoutingNodes().node("node0").numberOfShardsWithState(STARTED), equalTo(9)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(STARTED), equalTo(9)); assertThat(clusterState.getRoutingNodes().unassigned().size(), equalTo(0)); assertNumIndexShardsPerNode(clusterState, equalTo(3)); @@ -175,36 +175,36 @@ public void testMinimalRelocationsNoLimit() { clusterState = service.reroute(clusterState, "reroute", ActionListener.noop()); RoutingNodes routingNodes = clusterState.getRoutingNodes(); - assertThat(routingNodes.node("node2").shardsWithState(INITIALIZING).size(), equalTo(2)); - assertThat(routingNodes.node("node0").shardsWithState(INITIALIZING).size(), equalTo(0)); - assertThat(routingNodes.node("node1").shardsWithState(INITIALIZING).size(), equalTo(0)); + assertThat(routingNodes.node("node2").numberOfShardsWithState(INITIALIZING), equalTo(2)); + assertThat(routingNodes.node("node0").numberOfShardsWithState(INITIALIZING), equalTo(0)); + assertThat(routingNodes.node("node1").numberOfShardsWithState(INITIALIZING), equalTo(0)); ClusterState newState = startInitializingShardsAndReroute(service, clusterState); assertThat(newState, not(equalTo(clusterState))); clusterState = newState; routingNodes = clusterState.getRoutingNodes(); - assertThat(routingNodes.node("node2").shardsWithState(STARTED).size(), equalTo(2)); - assertThat(routingNodes.node("node2").shardsWithState(INITIALIZING).size(), equalTo(2)); - assertThat(routingNodes.node("node0").shardsWithState(INITIALIZING).size(), equalTo(0)); - assertThat(routingNodes.node("node1").shardsWithState(INITIALIZING).size(), equalTo(0)); + assertThat(routingNodes.node("node2").numberOfShardsWithState(STARTED), equalTo(2)); + assertThat(routingNodes.node("node2").numberOfShardsWithState(INITIALIZING), equalTo(2)); + assertThat(routingNodes.node("node0").numberOfShardsWithState(INITIALIZING), equalTo(0)); + assertThat(routingNodes.node("node1").numberOfShardsWithState(INITIALIZING), equalTo(0)); newState = startInitializingShardsAndReroute(service, clusterState); assertThat(newState, not(equalTo(clusterState))); clusterState = newState; routingNodes = clusterState.getRoutingNodes(); - assertThat(routingNodes.node("node2").shardsWithState(STARTED).size(), equalTo(4)); - assertThat(routingNodes.node("node2").shardsWithState(INITIALIZING).size(), equalTo(2)); - assertThat(routingNodes.node("node0").shardsWithState(INITIALIZING).size(), equalTo(0)); - assertThat(routingNodes.node("node1").shardsWithState(INITIALIZING).size(), equalTo(0)); + assertThat(routingNodes.node("node2").numberOfShardsWithState(STARTED), equalTo(4)); + assertThat(routingNodes.node("node2").numberOfShardsWithState(INITIALIZING), equalTo(2)); + assertThat(routingNodes.node("node0").numberOfShardsWithState(INITIALIZING), equalTo(0)); + assertThat(routingNodes.node("node1").numberOfShardsWithState(INITIALIZING), equalTo(0)); newState = startInitializingShardsAndReroute(service, clusterState); assertThat(newState, not(equalTo(clusterState))); clusterState = newState; routingNodes = clusterState.getRoutingNodes(); - assertThat(routingNodes.node("node2").shardsWithState(STARTED).size(), equalTo(6)); - assertThat(routingNodes.node("node2").shardsWithState(INITIALIZING).size(), equalTo(0)); - assertThat(routingNodes.node("node0").shardsWithState(INITIALIZING).size(), equalTo(0)); - assertThat(routingNodes.node("node1").shardsWithState(INITIALIZING).size(), equalTo(0)); + assertThat(routingNodes.node("node2").numberOfShardsWithState(STARTED), equalTo(6)); + assertThat(routingNodes.node("node2").numberOfShardsWithState(INITIALIZING), equalTo(0)); + assertThat(routingNodes.node("node0").numberOfShardsWithState(INITIALIZING), equalTo(0)); + assertThat(routingNodes.node("node1").numberOfShardsWithState(INITIALIZING), equalTo(0)); newState = startInitializingShardsAndReroute(service, clusterState); assertThat(newState, equalTo(clusterState)); diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/AllocationCommandsTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/AllocationCommandsTests.java index 80fe61f1abf3..ebac38764fc8 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/AllocationCommandsTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/AllocationCommandsTests.java @@ -294,13 +294,13 @@ public void testAllocateCommand() { assertThat(newState, not(equalTo(clusterState))); clusterState = newState; assertThat(clusterState.getRoutingNodes().node("node1").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(INITIALIZING).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(INITIALIZING), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node2").size(), equalTo(0)); logger.info("--> start the primary shard"); clusterState = startInitializingShardsAndReroute(allocation, clusterState); assertThat(clusterState.getRoutingNodes().node("node1").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(STARTED), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node2").size(), equalTo(0)); logger.info("--> allocate the replica shard on the primary shard node, should fail"); @@ -328,16 +328,16 @@ public void testAllocateCommand() { assertThat(newState, not(equalTo(clusterState))); clusterState = newState; assertThat(clusterState.getRoutingNodes().node("node1").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(STARTED), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node2").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node2").shardsWithState(INITIALIZING).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node2").numberOfShardsWithState(INITIALIZING), equalTo(1)); logger.info("--> start the replica shard"); clusterState = startInitializingShardsAndReroute(allocation, clusterState); assertThat(clusterState.getRoutingNodes().node("node1").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(STARTED), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node2").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node2").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node2").numberOfShardsWithState(STARTED), equalTo(1)); logger.info("--> verify that we fail when there are no unassigned shards"); try { @@ -402,14 +402,14 @@ public void testAllocateStalePrimaryCommand() { ).clusterState(); RoutingNode routingNode1 = clusterState.getRoutingNodes().node(node1); assertThat(routingNode1.size(), equalTo(1)); - assertThat(routingNode1.shardsWithState(INITIALIZING).size(), equalTo(1)); + assertThat(routingNode1.numberOfShardsWithState(INITIALIZING), equalTo(1)); Set inSyncAllocationIds = clusterState.metadata().index(index).inSyncAllocationIds(0); assertThat(inSyncAllocationIds, equalTo(Collections.singleton(RecoverySource.ExistingStoreRecoverySource.FORCED_ALLOCATION_ID))); clusterState = startInitializingShardsAndReroute(allocation, clusterState); routingNode1 = clusterState.getRoutingNodes().node(node1); assertThat(routingNode1.size(), equalTo(1)); - assertThat(routingNode1.shardsWithState(STARTED).size(), equalTo(1)); + assertThat(routingNode1.numberOfShardsWithState(STARTED), equalTo(1)); inSyncAllocationIds = clusterState.metadata().index(index).inSyncAllocationIds(0); assertThat(inSyncAllocationIds, hasSize(1)); assertThat(inSyncAllocationIds, not(Collections.singleton(RecoverySource.ExistingStoreRecoverySource.FORCED_ALLOCATION_ID))); @@ -452,7 +452,7 @@ public void testCancelCommand() { assertThat(newState, not(equalTo(clusterState))); clusterState = newState; assertThat(clusterState.getRoutingNodes().node("node1").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(INITIALIZING).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(INITIALIZING), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node2").size(), equalTo(0)); logger.info("--> cancel primary allocation, make sure it fails..."); @@ -471,7 +471,7 @@ public void testCancelCommand() { logger.info("--> start the primary shard"); clusterState = startInitializingShardsAndReroute(allocation, clusterState); assertThat(clusterState.getRoutingNodes().node("node1").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(STARTED), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node2").size(), equalTo(0)); logger.info("--> cancel primary allocation, make sure it fails..."); @@ -499,9 +499,9 @@ public void testCancelCommand() { assertThat(newState, not(equalTo(clusterState))); clusterState = newState; assertThat(clusterState.getRoutingNodes().node("node1").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(STARTED), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node2").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node2").shardsWithState(INITIALIZING).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node2").numberOfShardsWithState(INITIALIZING), equalTo(1)); logger.info("--> cancel the relocation allocation"); newState = allocation.reroute( @@ -515,7 +515,7 @@ public void testCancelCommand() { assertThat(newState, not(equalTo(clusterState))); clusterState = newState; assertThat(clusterState.getRoutingNodes().node("node1").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(STARTED), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node2").size(), equalTo(0)); assertThat(clusterState.getRoutingNodes().node("node3").size(), equalTo(0)); @@ -531,9 +531,9 @@ public void testCancelCommand() { assertThat(newState, not(equalTo(clusterState))); clusterState = newState; assertThat(clusterState.getRoutingNodes().node("node1").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(STARTED), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node2").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node2").shardsWithState(INITIALIZING).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node2").numberOfShardsWithState(INITIALIZING), equalTo(1)); logger.info("--> cancel the primary being replicated, make sure it fails"); try { @@ -551,9 +551,9 @@ public void testCancelCommand() { logger.info("--> start the replica shard"); clusterState = startInitializingShardsAndReroute(allocation, clusterState); assertThat(clusterState.getRoutingNodes().node("node1").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(STARTED), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node2").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node2").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node2").numberOfShardsWithState(STARTED), equalTo(1)); logger.info("--> cancel allocation of the replica shard"); newState = allocation.reroute( @@ -567,7 +567,7 @@ public void testCancelCommand() { assertThat(newState, not(equalTo(clusterState))); clusterState = newState; assertThat(clusterState.getRoutingNodes().node("node1").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(STARTED), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node2").size(), equalTo(0)); assertThat(clusterState.getRoutingNodes().node("node3").size(), equalTo(0)); @@ -583,15 +583,15 @@ public void testCancelCommand() { assertThat(newState, not(equalTo(clusterState))); clusterState = newState; assertThat(clusterState.getRoutingNodes().node("node1").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(STARTED), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node2").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node2").shardsWithState(INITIALIZING).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node2").numberOfShardsWithState(INITIALIZING), equalTo(1)); logger.info("--> start the replica shard"); clusterState = startInitializingShardsAndReroute(allocation, clusterState); assertThat(clusterState.getRoutingNodes().node("node1").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(STARTED), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node2").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node2").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node2").numberOfShardsWithState(STARTED), equalTo(1)); logger.info("--> move the replica shard"); clusterState = allocation.reroute( @@ -603,11 +603,11 @@ public void testCancelCommand() { ActionListener.noop() ).clusterState(); assertThat(clusterState.getRoutingNodes().node("node1").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(STARTED), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node2").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node2").shardsWithState(RELOCATING).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node2").numberOfShardsWithState(RELOCATING), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node3").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node3").shardsWithState(INITIALIZING).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node3").numberOfShardsWithState(INITIALIZING), equalTo(1)); if (randomBoolean()) { logger.info("--> cancel the primary allocation (with allow_primary set to true)"); @@ -635,9 +635,9 @@ public void testCancelCommand() { ActionListener.noop() ).clusterState(); assertThat(clusterState.getRoutingNodes().node("node1").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(STARTED), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node2").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node2").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node2").numberOfShardsWithState(STARTED), equalTo(1)); logger.info("--> move the replica shard again"); clusterState = allocation.reroute( @@ -649,11 +649,11 @@ public void testCancelCommand() { ActionListener.noop() ).clusterState(); assertThat(clusterState.getRoutingNodes().node("node1").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(STARTED), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node2").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node2").shardsWithState(RELOCATING).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node2").numberOfShardsWithState(RELOCATING), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node3").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node3").shardsWithState(INITIALIZING).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node3").numberOfShardsWithState(INITIALIZING), equalTo(1)); logger.info("--> cancel the source replica shard"); clusterState = allocation.reroute( @@ -665,18 +665,18 @@ public void testCancelCommand() { ActionListener.noop() ).clusterState(); assertThat(clusterState.getRoutingNodes().node("node1").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(STARTED), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node2").size(), equalTo(0)); assertThat(clusterState.getRoutingNodes().node("node3").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node3").shardsWithState(INITIALIZING).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node3").numberOfShardsWithState(INITIALIZING), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node3").shardsWithState(INITIALIZING).get(0).relocatingNodeId(), nullValue()); logger.info("--> start the former target replica shard"); clusterState = startInitializingShardsAndReroute(allocation, clusterState); assertThat(clusterState.getRoutingNodes().node("node1").size(), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node1").numberOfShardsWithState(STARTED), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node2").size(), equalTo(0)); - assertThat(clusterState.getRoutingNodes().node("node3").shardsWithState(STARTED).size(), equalTo(1)); + assertThat(clusterState.getRoutingNodes().node("node3").numberOfShardsWithState(STARTED), equalTo(1)); logger.info("--> cancel the primary allocation (with allow_primary set to true)"); newState = allocation.reroute( diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/BalanceConfigurationTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/BalanceConfigurationTests.java index 3eb5c353585a..fb0c2f0b2fd3 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/BalanceConfigurationTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/BalanceConfigurationTests.java @@ -39,6 +39,8 @@ import static org.elasticsearch.cluster.routing.RoutingNodesHelper.shardsWithState; import static org.elasticsearch.cluster.routing.ShardRoutingState.STARTED; import static org.elasticsearch.cluster.routing.ShardRoutingState.UNASSIGNED; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.lessThanOrEqualTo; public class BalanceConfigurationTests extends ESAllocationTestCase { @@ -255,8 +257,8 @@ private void assertReplicaBalance( final int maxAvgNumberOfShards = Math.round(Math.round(Math.ceil(avgNumShards + threshold))); for (RoutingNode node : nodes) { - assertThat(node.shardsWithState(STARTED).size(), Matchers.greaterThanOrEqualTo(minAvgNumberOfShards)); - assertThat(node.shardsWithState(STARTED).size(), Matchers.lessThanOrEqualTo(maxAvgNumberOfShards)); + assertThat(node.numberOfShardsWithState(STARTED), greaterThanOrEqualTo(minAvgNumberOfShards)); + assertThat(node.numberOfShardsWithState(STARTED), lessThanOrEqualTo(maxAvgNumberOfShards)); } } @@ -277,8 +279,8 @@ private void assertIndexBalance( for (String index : routingTable.indicesRouting().keySet()) { for (RoutingNode node : nodes) { - assertThat(node.shardsWithState(index, STARTED).size(), Matchers.greaterThanOrEqualTo(minAvgNumberOfShards)); - assertThat(node.shardsWithState(index, STARTED).size(), Matchers.lessThanOrEqualTo(maxAvgNumberOfShards)); + assertThat(node.shardsWithState(index, STARTED).size(), greaterThanOrEqualTo(minAvgNumberOfShards)); + assertThat(node.shardsWithState(index, STARTED).size(), lessThanOrEqualTo(maxAvgNumberOfShards)); } } } diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceReconcilerTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceReconcilerTests.java index e80be30262e7..1f325a26dc57 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceReconcilerTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceReconcilerTests.java @@ -85,7 +85,6 @@ import static org.elasticsearch.cluster.routing.allocation.decider.ThrottlingAllocationDecider.CLUSTER_ROUTING_ALLOCATION_NODE_CONCURRENT_OUTGOING_RECOVERIES_SETTING; import static org.elasticsearch.cluster.routing.allocation.decider.ThrottlingAllocationDecider.CLUSTER_ROUTING_ALLOCATION_NODE_INITIAL_PRIMARIES_RECOVERIES_SETTING; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.oneOf; @@ -883,8 +882,8 @@ public Decision canAllocate(ShardRouting shardRouting, RoutingAllocation allocat // The next reroute starts moving shards to node-2 and node-3, but interleaves the decisions between node-0 and node-1 for fairness. // There's an inbound throttle of 1 but no outbound throttle, so without the interleaving one node would relocate 2 shards. final var reroutedState = allocationService.reroute(clusterState, "test", ActionListener.noop()); - assertThat(reroutedState.getRoutingNodes().node("node-0").shardsWithState(ShardRoutingState.RELOCATING), hasSize(1)); - assertThat(reroutedState.getRoutingNodes().node("node-1").shardsWithState(ShardRoutingState.RELOCATING), hasSize(1)); + assertThat(reroutedState.getRoutingNodes().node("node-0").numberOfShardsWithState(ShardRoutingState.RELOCATING), equalTo(1)); + assertThat(reroutedState.getRoutingNodes().node("node-1").numberOfShardsWithState(ShardRoutingState.RELOCATING), equalTo(1)); // Ensuring that we check the shortcut two-param canAllocate() method up front canAllocateRef.set(Decision.NO); @@ -925,7 +924,7 @@ public Decision canAllocate(ShardRouting shardRouting, RoutingAllocation allocat "test", ActionListener.noop() ); - assertThat(shuttingDownState.getRoutingNodes().node("node-2").shardsWithState(ShardRoutingState.INITIALIZING), hasSize(1)); + assertThat(shuttingDownState.getRoutingNodes().node("node-2").numberOfShardsWithState(ShardRoutingState.INITIALIZING), equalTo(1)); } public void testRebalance() { @@ -1011,8 +1010,8 @@ public Decision canAllocate(ShardRouting shardRouting, RoutingAllocation allocat // The next reroute starts moving shards to node-2 and node-3, but interleaves the decisions between node-0 and node-1 for fairness. // There's an inbound throttle of 1 but no outbound throttle, so without the interleaving one node would relocate 2 shards. final var reroutedState = allocationService.reroute(clusterState, "test", ActionListener.noop()); - assertThat(reroutedState.getRoutingNodes().node("node-0").shardsWithState(ShardRoutingState.RELOCATING), hasSize(1)); - assertThat(reroutedState.getRoutingNodes().node("node-1").shardsWithState(ShardRoutingState.RELOCATING), hasSize(1)); + assertThat(reroutedState.getRoutingNodes().node("node-0").numberOfShardsWithState(ShardRoutingState.RELOCATING), equalTo(1)); + assertThat(reroutedState.getRoutingNodes().node("node-1").numberOfShardsWithState(ShardRoutingState.RELOCATING), equalTo(1)); } public void testDoNotRebalanceToTheNodeThatNoLongerExists() { diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderTests.java index 98578e44cc08..b5727b9d23a0 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderTests.java @@ -1200,7 +1200,11 @@ private void doTestDiskThresholdWithSnapshotShardSizes(boolean testMaxHeadroom) logShardStates(clusterState); assertThat(shardsWithState(clusterState.getRoutingNodes(), UNASSIGNED).size(), equalTo(shouldAllocate ? 0 : 1)); - assertThat(shardsWithState(clusterState.getRoutingNodes(), "test", INITIALIZING, STARTED).size(), equalTo(shouldAllocate ? 1 : 0)); + assertThat( + shardsWithState(clusterState.getRoutingNodes(), "test", INITIALIZING).size() // + + shardsWithState(clusterState.getRoutingNodes(), "test", STARTED).size(), + equalTo(shouldAllocate ? 1 : 0) + ); } public void testDiskThresholdWithSnapshotShardSizesWithPercentages() { diff --git a/test/framework/src/main/java/org/elasticsearch/cluster/routing/RoutingNodesHelper.java b/test/framework/src/main/java/org/elasticsearch/cluster/routing/RoutingNodesHelper.java index 95420293e80c..fc123429cea7 100644 --- a/test/framework/src/main/java/org/elasticsearch/cluster/routing/RoutingNodesHelper.java +++ b/test/framework/src/main/java/org/elasticsearch/cluster/routing/RoutingNodesHelper.java @@ -10,43 +10,27 @@ import org.elasticsearch.cluster.node.DiscoveryNode; -import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.stream.IntStream; import java.util.stream.Stream; +import static org.elasticsearch.common.util.CollectionUtils.iterableAsArrayList; + public final class RoutingNodesHelper { private RoutingNodesHelper() {} public static List shardsWithState(RoutingNodes routingNodes, ShardRoutingState state) { - List shards = new ArrayList<>(); - if (state == ShardRoutingState.UNASSIGNED) { - routingNodes.unassigned().forEach(shards::add); - } else { - for (RoutingNode routingNode : routingNodes) { - shards.addAll(routingNode.shardsWithState(state)); - } - } - return shards; + return state == ShardRoutingState.UNASSIGNED + ? iterableAsArrayList(routingNodes.unassigned()) + : routingNodes.stream().flatMap(routingNode -> routingNode.shardsWithState(state).stream()).toList(); } - public static List shardsWithState(RoutingNodes routingNodes, String index, ShardRoutingState... states) { - List shards = new ArrayList<>(); - for (ShardRoutingState state : states) { - if (state == ShardRoutingState.UNASSIGNED) { - for (ShardRouting unassignedShard : routingNodes.unassigned()) { - if (unassignedShard.index().getName().equals(index)) { - shards.add(unassignedShard); - } - } - } else { - for (RoutingNode routingNode : routingNodes) { - shards.addAll(routingNode.shardsWithState(index, state)); - } - } - } - return shards; + public static List shardsWithState(RoutingNodes routingNodes, String index, ShardRoutingState states) { + return shardsWithState(routingNodes, states).stream() + .filter(shardRouting -> Objects.equals(shardRouting.getIndexName(), index)) + .toList(); } /** diff --git a/x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/storage/ReactiveStorageDeciderDecisionTests.java b/x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/storage/ReactiveStorageDeciderDecisionTests.java index ed9278aca34f..7f81428ecd47 100644 --- a/x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/storage/ReactiveStorageDeciderDecisionTests.java +++ b/x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/storage/ReactiveStorageDeciderDecisionTests.java @@ -52,7 +52,6 @@ import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -515,7 +514,6 @@ private void startRandomShards() { allocation.routingNodes(), ShardRoutingState.INITIALIZING ); - initializingShards.sort(Comparator.comparing(ShardRouting::shardId).thenComparing(ShardRouting::primary, Boolean::compare)); List shards = randomSubsetOf(Math.min(randomIntBetween(1, 100), initializingShards.size()), initializingShards); // replicas before primaries, since replicas can be reinit'ed, resulting in a new ShardRouting instance. diff --git a/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownDelayedAllocationIT.java b/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownDelayedAllocationIT.java index 4231d4094f19..937b67c8d787 100644 --- a/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownDelayedAllocationIT.java +++ b/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownDelayedAllocationIT.java @@ -24,7 +24,6 @@ import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.List; import static org.hamcrest.Matchers.equalTo; @@ -230,8 +229,7 @@ private void indexRandomData() throws Exception { private String findIdOfNodeWithShard() { ClusterState state = client().admin().cluster().prepareState().get().getState(); List startedShards = RoutingNodesHelper.shardsWithState(state.getRoutingNodes(), ShardRoutingState.STARTED); - Collections.shuffle(startedShards, random()); - return startedShards.get(0).currentNodeId(); + return randomFrom(startedShards).currentNodeId(); } private String findNodeNameFromId(String id) { From 3d7f1e60cd07ecb8d3e1f800c7f17728daea7950 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Fri, 2 Dec 2022 14:46:54 +0100 Subject: [PATCH 138/919] Fork Reponse Handler on non-master node in TransportMasterNodeAction (#92061) This makes it so we fork to the executor specified for the transport action in transport master node action. This has two advantages: 1. Responses that might be large will be deserialized off the transport threads (mostly relevant for cluster state requests as far as I can see) 2. Transport master node action handling happens on the same thread, regardless of whether or not the executing node is the current master or if the action was actually forwarded via a transport message. --- .../action/support/master/TransportMasterNodeAction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/action/support/master/TransportMasterNodeAction.java b/server/src/main/java/org/elasticsearch/action/support/master/TransportMasterNodeAction.java index 67268712c49c..d404212fc834 100644 --- a/server/src/main/java/org/elasticsearch/action/support/master/TransportMasterNodeAction.java +++ b/server/src/main/java/org/elasticsearch/action/support/master/TransportMasterNodeAction.java @@ -245,7 +245,7 @@ protected void doStart(ClusterState clusterState) { masterNode, actionName, request, - new ActionListenerResponseHandler(listener, responseReader) { + new ActionListenerResponseHandler<>(listener, responseReader, executor) { @Override public void handleException(final TransportException exp) { Throwable cause = exp.unwrapCause(); From dd82d3bfebd4f9f2d01642267e0e5ef83a643e44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Fern=C3=A1ndez=20Casta=C3=B1o?= Date: Fri, 2 Dec 2022 15:25:19 +0100 Subject: [PATCH 139/919] Update the default `cluster.routing.allocation.balance.disk_usage` (#92065) Set the new default to `2e-11f` to make 50GiB count roughly as 1 shard. --- docs/changelog/92065.yaml | 5 +++++ .../allocation/allocator/BalancedShardsAllocator.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/92065.yaml diff --git a/docs/changelog/92065.yaml b/docs/changelog/92065.yaml new file mode 100644 index 000000000000..f9ef18a08b00 --- /dev/null +++ b/docs/changelog/92065.yaml @@ -0,0 +1,5 @@ +pr: 92065 +summary: Update the default `cluster.routing.allocation.balance.disk_usage` +area: Allocation +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocator.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocator.java index 600b4fa372ef..1586b0ed6068 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocator.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocator.java @@ -101,7 +101,7 @@ public class BalancedShardsAllocator implements ShardsAllocator { ); public static final Setting DISK_USAGE_BALANCE_FACTOR_SETTING = Setting.floatSetting( "cluster.routing.allocation.balance.disk_usage", - 5e-11f, + 2e-11f, 0.0f, Property.Dynamic, Property.NodeScope From 251e830cc35c3476147f5336248cc2eedb6b0ad1 Mon Sep 17 00:00:00 2001 From: Tim Brooks Date: Fri, 2 Dec 2022 10:48:46 -0700 Subject: [PATCH 140/919] Move security http server logic to http server (#91870) This commit moves logic related to implementing security features into the core http server implementation. --- modules/transport-netty4/build.gradle | 2 + .../src/main/java/module-info.java | 1 + .../netty4/Netty4HttpServerTransport.java | 63 ++++++++++- .../netty4/AcceptChannelHandler.java | 40 +++++++ .../transport/netty4/Netty4Plugin.java | 4 +- .../transport/netty4}/SSLExceptionHelper.java | 8 +- .../transport/netty4/TLSConfig.java | 37 ++++++ .../http/netty4/Netty4BadRequestTests.java | 9 +- .../Netty4HttpServerPipeliningTests.java | 5 +- .../Netty4HttpServerTransportTests.java | 35 ++++-- .../http/HttpServerTransport.java | 1 + .../SecurityTransportExceptionHandler.java | 1 + .../xpack/security/Security.java | 50 +++++++-- .../SecurityHttpExceptionHandler.java | 54 --------- .../security/transport/filter/IPFilter.java | 3 +- .../SecurityNetty4HttpServerTransport.java | 105 ------------------ ...ecurityNetty4HttpServerTransportTests.java | 67 +++++------ 17 files changed, 253 insertions(+), 232 deletions(-) create mode 100644 modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/AcceptChannelHandler.java rename {x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/core/security/transport => modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4}/SSLExceptionHelper.java (87%) create mode 100644 modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/TLSConfig.java delete mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityHttpExceptionHandler.java delete mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransport.java diff --git a/modules/transport-netty4/build.gradle b/modules/transport-netty4/build.gradle index bf43c4a88d91..d3ce40ad32f0 100644 --- a/modules/transport-netty4/build.gradle +++ b/modules/transport-netty4/build.gradle @@ -34,6 +34,8 @@ configurations { } dependencies { + api project(":libs:elasticsearch-ssl-config") + // network stack api "io.netty:netty-buffer:${versions.netty}" api "io.netty:netty-codec:${versions.netty}" diff --git a/modules/transport-netty4/src/main/java/module-info.java b/modules/transport-netty4/src/main/java/module-info.java index 92217b419c66..5f94b97be782 100644 --- a/modules/transport-netty4/src/main/java/module-info.java +++ b/modules/transport-netty4/src/main/java/module-info.java @@ -10,6 +10,7 @@ requires jdk.net; requires org.elasticsearch.base; requires org.elasticsearch.server; + requires org.elasticsearch.sslconfig; requires org.elasticsearch.xcontent; requires org.apache.logging.log4j; requires org.apache.lucene.core; diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpServerTransport.java b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpServerTransport.java index efd970411701..e741772eedb5 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpServerTransport.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpServerTransport.java @@ -27,6 +27,7 @@ import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseEncoder; import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.ssl.SslHandler; import io.netty.handler.timeout.ReadTimeoutException; import io.netty.handler.timeout.ReadTimeoutHandler; import io.netty.util.AttributeKey; @@ -34,6 +35,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.common.network.CloseableChannel; import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Setting; @@ -43,23 +45,29 @@ import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.core.IOUtils; +import org.elasticsearch.core.Nullable; import org.elasticsearch.http.AbstractHttpServerTransport; import org.elasticsearch.http.HttpChannel; import org.elasticsearch.http.HttpHandlingSettings; import org.elasticsearch.http.HttpReadTimeoutException; import org.elasticsearch.http.HttpServerChannel; +import org.elasticsearch.http.HttpServerTransport; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.tracing.Tracer; +import org.elasticsearch.transport.netty4.AcceptChannelHandler; import org.elasticsearch.transport.netty4.NetUtils; import org.elasticsearch.transport.netty4.Netty4Utils; import org.elasticsearch.transport.netty4.Netty4WriteThrottlingHandler; import org.elasticsearch.transport.netty4.NettyAllocator; import org.elasticsearch.transport.netty4.NettyByteBufSizer; +import org.elasticsearch.transport.netty4.SSLExceptionHelper; import org.elasticsearch.transport.netty4.SharedGroupFactory; +import org.elasticsearch.transport.netty4.TLSConfig; import org.elasticsearch.xcontent.NamedXContentRegistry; import java.net.InetSocketAddress; import java.util.concurrent.TimeUnit; +import java.util.function.BiPredicate; import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_MAX_CHUNK_SIZE; import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_MAX_CONTENT_LENGTH; @@ -133,6 +141,8 @@ public class Netty4HttpServerTransport extends AbstractHttpServerTransport { private final SharedGroupFactory sharedGroupFactory; private final RecvByteBufAllocator recvByteBufAllocator; + private final TLSConfig tlsConfig; + private final AcceptChannelHandler.AcceptPredicate acceptChannelPredicate; private final int readTimeoutMillis; private final int maxCompositeBufferComponents; @@ -148,7 +158,10 @@ public Netty4HttpServerTransport( Dispatcher dispatcher, ClusterSettings clusterSettings, SharedGroupFactory sharedGroupFactory, - Tracer tracer + Tracer tracer, + TLSConfig tlsConfig, + @Nullable AcceptChannelHandler.AcceptPredicate acceptChannelPredicate + ) { super( settings, @@ -163,6 +176,8 @@ public Netty4HttpServerTransport( Netty4Utils.setAvailableProcessors(EsExecutors.allocatedProcessors(settings)); NettyAllocator.logAllocatorDescriptionIfNeeded(); this.sharedGroupFactory = sharedGroupFactory; + this.tlsConfig = tlsConfig; + this.acceptChannelPredicate = acceptChannelPredicate; this.pipeliningMaxEvents = SETTING_PIPELINING_MAX_EVENTS.get(settings); @@ -254,6 +269,9 @@ protected void doStart() { serverBootstrap.childOption(ChannelOption.SO_REUSEADDR, reuseAddress); bindServer(); + if (acceptChannelPredicate != null) { + acceptChannelPredicate.setBoundAddress(boundAddress()); + } success = true; } finally { if (success == false) { @@ -281,7 +299,23 @@ protected void stopInternal() { @Override public void onException(HttpChannel channel, Exception cause) { - if (cause instanceof ReadTimeoutException) { + if (lifecycle.started() == false) { + return; + } + + if (SSLExceptionHelper.isNotSslRecordException(cause)) { + logger.warn("received plaintext http traffic on an https channel, closing connection {}", channel); + CloseableChannel.closeChannel(channel); + } else if (SSLExceptionHelper.isCloseDuringHandshakeException(cause)) { + logger.debug("connection {} closed during ssl handshake", channel); + CloseableChannel.closeChannel(channel); + } else if (SSLExceptionHelper.isInsufficientBufferRemainingException(cause)) { + logger.debug("connection {} closed abruptly", channel); + CloseableChannel.closeChannel(channel); + } else if (SSLExceptionHelper.isReceivedCertificateUnknownException(cause)) { + logger.warn("http client did not trust this server's certificate, closing connection {}", channel); + CloseableChannel.closeChannel(channel); + } else if (cause instanceof ReadTimeoutException) { super.onException(channel, new HttpReadTimeoutException(readTimeoutMillis, cause)); } else { super.onException(channel, cause); @@ -289,7 +323,7 @@ public void onException(HttpChannel channel, Exception cause) { } public ChannelHandler configureServerChannelHandler() { - return new HttpChannelHandler(this, handlingSettings); + return new HttpChannelHandler(this, handlingSettings, tlsConfig, acceptChannelPredicate); } static final AttributeKey HTTP_CHANNEL_KEY = AttributeKey.newInstance("es-http-channel"); @@ -299,16 +333,35 @@ protected static class HttpChannelHandler extends ChannelInitializer { private final Netty4HttpServerTransport transport; private final HttpHandlingSettings handlingSettings; - - protected HttpChannelHandler(final Netty4HttpServerTransport transport, final HttpHandlingSettings handlingSettings) { + private final TLSConfig tlsConfig; + private final BiPredicate acceptChannelPredicate; + + protected HttpChannelHandler( + final Netty4HttpServerTransport transport, + final HttpHandlingSettings handlingSettings, + final TLSConfig tlsConfig, + @Nullable final BiPredicate acceptChannelPredicate + ) { this.transport = transport; this.handlingSettings = handlingSettings; + this.tlsConfig = tlsConfig; + this.acceptChannelPredicate = acceptChannelPredicate; } @Override protected void initChannel(Channel ch) throws Exception { Netty4HttpChannel nettyHttpChannel = new Netty4HttpChannel(ch); ch.attr(HTTP_CHANNEL_KEY).set(nettyHttpChannel); + if (acceptChannelPredicate != null) { + ch.pipeline() + .addLast( + "accept_channel_handler", + new AcceptChannelHandler(acceptChannelPredicate, HttpServerTransport.HTTP_PROFILE_NAME) + ); + } + if (tlsConfig.isTLSEnabled()) { + ch.pipeline().addLast("ssl", new SslHandler(tlsConfig.createServerSSLEngine())); + } ch.pipeline() .addLast("chunked_writer", new Netty4WriteThrottlingHandler(transport.getThreadPool().getThreadContext())) .addLast("byte_buf_sizer", NettyByteBufSizer.INSTANCE); diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/AcceptChannelHandler.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/AcceptChannelHandler.java new file mode 100644 index 000000000000..993d70c6e89f --- /dev/null +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/AcceptChannelHandler.java @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.transport.netty4; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.ipfilter.AbstractRemoteAddressFilter; + +import org.elasticsearch.common.transport.BoundTransportAddress; + +import java.net.InetSocketAddress; +import java.util.function.BiPredicate; + +@ChannelHandler.Sharable +public class AcceptChannelHandler extends AbstractRemoteAddressFilter { + + private final BiPredicate predicate; + private final String profile; + + public AcceptChannelHandler(final BiPredicate predicate, final String profile) { + this.predicate = predicate; + this.profile = profile; + } + + @Override + protected boolean accept(final ChannelHandlerContext ctx, final InetSocketAddress remoteAddress) throws Exception { + return predicate.test(profile, remoteAddress); + } + + public interface AcceptPredicate extends BiPredicate { + + void setBoundAddress(BoundTransportAddress boundHttpTransportAddress); + } +} diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Plugin.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Plugin.java index e39994c05a44..c8a9fd384988 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Plugin.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Plugin.java @@ -112,7 +112,9 @@ public Map> getHttpTransports( dispatcher, clusterSettings, getSharedGroupFactory(settings), - tracer + tracer, + TLSConfig.noTLS(), + null ) ); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/core/security/transport/SSLExceptionHelper.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/SSLExceptionHelper.java similarity index 87% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/core/security/transport/SSLExceptionHelper.java rename to modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/SSLExceptionHelper.java index 9bc63be18e04..19294c193605 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/core/security/transport/SSLExceptionHelper.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/SSLExceptionHelper.java @@ -1,10 +1,12 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ -package org.elasticsearch.xpack.core.security.transport; + +package org.elasticsearch.transport.netty4; import io.netty.handler.codec.DecoderException; import io.netty.handler.ssl.NotSslRecordException; diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/TLSConfig.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/TLSConfig.java new file mode 100644 index 000000000000..b78fcd1a5a9b --- /dev/null +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/TLSConfig.java @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.transport.netty4; + +import org.elasticsearch.common.ssl.SslConfiguration; + +import javax.net.ssl.SSLEngine; + +public record TLSConfig(SslConfiguration sslConfiguration, EngineProvider engineProvider) { + + public boolean isTLSEnabled() { + return sslConfiguration != null; + } + + public SSLEngine createServerSSLEngine() { + assert isTLSEnabled(); + SSLEngine sslEngine = engineProvider.create(sslConfiguration, null, -1); + sslEngine.setUseClientMode(false); + return sslEngine; + } + + public static TLSConfig noTLS() { + return new TLSConfig(null, null); + } + + @FunctionalInterface + public interface EngineProvider { + + SSLEngine create(SslConfiguration configuration, String host, int port); + } +} diff --git a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4BadRequestTests.java b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4BadRequestTests.java index 08721c68a34c..4b0757dd5144 100644 --- a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4BadRequestTests.java +++ b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4BadRequestTests.java @@ -16,8 +16,6 @@ import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.TransportAddress; -import org.elasticsearch.common.util.MockPageCacheRecycler; -import org.elasticsearch.common.util.PageCacheRecycler; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.http.HttpServerTransport; import org.elasticsearch.http.HttpTransportSettings; @@ -30,6 +28,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.tracing.Tracer; import org.elasticsearch.transport.netty4.SharedGroupFactory; +import org.elasticsearch.transport.netty4.TLSConfig; import org.junit.After; import org.junit.Before; @@ -45,13 +44,11 @@ public class Netty4BadRequestTests extends ESTestCase { private NetworkService networkService; - private PageCacheRecycler recycler; private ThreadPool threadPool; @Before public void setup() throws Exception { networkService = new NetworkService(Collections.emptyList()); - recycler = new MockPageCacheRecycler(Settings.EMPTY); threadPool = new TestThreadPool("test"); } @@ -88,7 +85,9 @@ public void dispatchBadRequest(RestChannel channel, ThreadContext threadContext, dispatcher, new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), new SharedGroupFactory(Settings.EMPTY), - Tracer.NOOP + Tracer.NOOP, + TLSConfig.noTLS(), + null ) ) { httpServerTransport.start(); diff --git a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerPipeliningTests.java b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerPipeliningTests.java index 0813fd5afc99..42573ea4fe3f 100644 --- a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerPipeliningTests.java +++ b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerPipeliningTests.java @@ -30,6 +30,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.tracing.Tracer; import org.elasticsearch.transport.netty4.SharedGroupFactory; +import org.elasticsearch.transport.netty4.TLSConfig; import org.junit.After; import org.junit.Before; @@ -104,7 +105,9 @@ class CustomNettyHttpServerTransport extends Netty4HttpServerTransport { new NullDispatcher(), new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), new SharedGroupFactory(settings), - Tracer.NOOP + Tracer.NOOP, + TLSConfig.noTLS(), + null ); } diff --git a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerTransportTests.java b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerTransportTests.java index e0ad1b697ad9..8fd764299cfe 100644 --- a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerTransportTests.java +++ b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerTransportTests.java @@ -66,6 +66,7 @@ import org.elasticsearch.tracing.Tracer; import org.elasticsearch.transport.netty4.NettyAllocator; import org.elasticsearch.transport.netty4.SharedGroupFactory; +import org.elasticsearch.transport.netty4.TLSConfig; import org.elasticsearch.xcontent.ToXContent; import org.junit.After; import org.junit.Before; @@ -174,7 +175,9 @@ public void dispatchBadRequest(RestChannel channel, ThreadContext threadContext, dispatcher, clusterSettings, new SharedGroupFactory(settings), - Tracer.NOOP + Tracer.NOOP, + TLSConfig.noTLS(), + null ) ) { transport.start(); @@ -223,7 +226,9 @@ public void testBindUnavailableAddress() { new NullDispatcher(), clusterSettings, new SharedGroupFactory(Settings.EMPTY), - Tracer.NOOP + Tracer.NOOP, + TLSConfig.noTLS(), + null ) ) { transport.start(); @@ -241,7 +246,9 @@ public void testBindUnavailableAddress() { new NullDispatcher(), clusterSettings, new SharedGroupFactory(settings), - Tracer.NOOP + Tracer.NOOP, + TLSConfig.noTLS(), + null ) ) { BindHttpException bindHttpException = expectThrows(BindHttpException.class, otherTransport::start); @@ -293,7 +300,9 @@ public void dispatchBadRequest(final RestChannel channel, final ThreadContext th dispatcher, clusterSettings, new SharedGroupFactory(settings), - Tracer.NOOP + Tracer.NOOP, + TLSConfig.noTLS(), + null ) ) { transport.start(); @@ -361,11 +370,13 @@ public void dispatchBadRequest(final RestChannel channel, final ThreadContext th dispatcher, clusterSettings, new SharedGroupFactory(Settings.EMPTY), - Tracer.NOOP + Tracer.NOOP, + TLSConfig.noTLS(), + null ) { @Override public ChannelHandler configureServerChannelHandler() { - return new HttpChannelHandler(this, handlingSettings) { + return new HttpChannelHandler(this, handlingSettings, TLSConfig.noTLS(), null) { @Override protected void initChannel(Channel ch) throws Exception { super.initChannel(ch); @@ -458,7 +469,9 @@ public void dispatchBadRequest(final RestChannel channel, final ThreadContext th dispatcher, randomClusterSettings(), new SharedGroupFactory(settings), - Tracer.NOOP + Tracer.NOOP, + TLSConfig.noTLS(), + null ) ) { transport.start(); @@ -528,7 +541,9 @@ public void dispatchBadRequest(final RestChannel channel, final ThreadContext th dispatcher, randomClusterSettings(), new SharedGroupFactory(settings), - Tracer.NOOP + Tracer.NOOP, + TLSConfig.noTLS(), + null ) ) { transport.start(); @@ -598,7 +613,9 @@ public void dispatchBadRequest(final RestChannel channel, final ThreadContext th dispatcher, clusterSettings, new SharedGroupFactory(settings), - Tracer.NOOP + Tracer.NOOP, + TLSConfig.noTLS(), + null ) ) { transport.start(); diff --git a/server/src/main/java/org/elasticsearch/http/HttpServerTransport.java b/server/src/main/java/org/elasticsearch/http/HttpServerTransport.java index 42eaa0a38445..b2528cf9f87a 100644 --- a/server/src/main/java/org/elasticsearch/http/HttpServerTransport.java +++ b/server/src/main/java/org/elasticsearch/http/HttpServerTransport.java @@ -17,6 +17,7 @@ public interface HttpServerTransport extends LifecycleComponent, ReportingService { + String HTTP_PROFILE_NAME = ".http"; String HTTP_SERVER_WORKER_THREAD_NAME_PREFIX = "http_server_worker"; BoundTransportAddress boundAddress(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/core/security/transport/SecurityTransportExceptionHandler.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/core/security/transport/SecurityTransportExceptionHandler.java index 051801479e3f..d8f5f773bd0c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/core/security/transport/SecurityTransportExceptionHandler.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/core/security/transport/SecurityTransportExceptionHandler.java @@ -10,6 +10,7 @@ import org.elasticsearch.common.component.Lifecycle; import org.elasticsearch.common.network.CloseableChannel; import org.elasticsearch.transport.TcpChannel; +import org.elasticsearch.transport.netty4.SSLExceptionHelper; import java.util.function.BiConsumer; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 3c9e48d78de2..d8d3cf61c041 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -38,6 +38,7 @@ import org.elasticsearch.common.settings.SettingsFilter; import org.elasticsearch.common.ssl.KeyStoreUtil; import org.elasticsearch.common.ssl.SslConfiguration; +import org.elasticsearch.common.transport.BoundTransportAddress; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.PageCacheRecycler; import org.elasticsearch.common.util.concurrent.EsExecutors; @@ -49,6 +50,7 @@ import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.env.NodeMetadata; import org.elasticsearch.http.HttpServerTransport; +import org.elasticsearch.http.netty4.Netty4HttpServerTransport; import org.elasticsearch.index.IndexModule; import org.elasticsearch.indices.SystemIndexDescriptor; import org.elasticsearch.indices.breaker.CircuitBreakerService; @@ -82,7 +84,9 @@ import org.elasticsearch.transport.TransportInterceptor; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.transport.TransportRequestHandler; +import org.elasticsearch.transport.netty4.AcceptChannelHandler; import org.elasticsearch.transport.netty4.SharedGroupFactory; +import org.elasticsearch.transport.netty4.TLSConfig; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xpack.core.XPackField; @@ -338,10 +342,10 @@ import org.elasticsearch.xpack.security.transport.SecurityHttpSettings; import org.elasticsearch.xpack.security.transport.SecurityServerTransportInterceptor; import org.elasticsearch.xpack.security.transport.filter.IPFilter; -import org.elasticsearch.xpack.security.transport.netty4.SecurityNetty4HttpServerTransport; import org.elasticsearch.xpack.security.transport.netty4.SecurityNetty4ServerTransport; import java.io.IOException; +import java.net.InetSocketAddress; import java.time.Clock; import java.util.ArrayList; import java.util.Arrays; @@ -1552,24 +1556,48 @@ public Map> getHttpTransports( return Collections.emptyMap(); } + final IPFilter ipFilter = this.ipFilter.get(); + final AcceptChannelHandler.AcceptPredicate acceptPredicate = new AcceptChannelHandler.AcceptPredicate() { + @Override + public void setBoundAddress(BoundTransportAddress boundHttpTransportAddress) { + ipFilter.setBoundHttpTransportAddress(boundHttpTransportAddress); + } + + @Override + public boolean test(String profile, InetSocketAddress peerAddress) { + return ipFilter.accept(profile, peerAddress); + } + }; + Map> httpTransports = new HashMap<>(); - httpTransports.put( - SecurityField.NAME4, - () -> new SecurityNetty4HttpServerTransport( + httpTransports.put(SecurityField.NAME4, () -> { + final boolean ssl = HTTP_SSL_ENABLED.get(settings); + SSLService sslService = getSslService(); + final SslConfiguration sslConfiguration; + if (ssl) { + sslConfiguration = sslService.getHttpTransportSSLConfiguration(); + if (SSLService.isConfigurationValidForServerUsage(sslConfiguration) == false) { + throw new IllegalArgumentException( + "a key must be provided to run as a server. the key should be configured using the " + + "[xpack.security.http.ssl.key] or [xpack.security.http.ssl.keystore.path] setting" + ); + } + } else { + sslConfiguration = null; + } + return new Netty4HttpServerTransport( settings, networkService, - pageCacheRecycler, - ipFilter.get(), - getSslService(), threadPool, xContentRegistry, dispatcher, clusterSettings, getNettySharedGroupFactory(settings), - tracer - ) - ); - + tracer, + new TLSConfig(sslConfiguration, sslService::createSSLEngine), + acceptPredicate + ); + }); return httpTransports; } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityHttpExceptionHandler.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityHttpExceptionHandler.java deleted file mode 100644 index ef8b0e56f313..000000000000 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityHttpExceptionHandler.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -package org.elasticsearch.xpack.security.transport; - -import org.apache.logging.log4j.Logger; -import org.elasticsearch.common.component.Lifecycle; -import org.elasticsearch.common.network.CloseableChannel; -import org.elasticsearch.http.HttpChannel; - -import java.util.function.BiConsumer; - -import static org.elasticsearch.xpack.core.security.transport.SSLExceptionHelper.isCloseDuringHandshakeException; -import static org.elasticsearch.xpack.core.security.transport.SSLExceptionHelper.isInsufficientBufferRemainingException; -import static org.elasticsearch.xpack.core.security.transport.SSLExceptionHelper.isNotSslRecordException; -import static org.elasticsearch.xpack.core.security.transport.SSLExceptionHelper.isReceivedCertificateUnknownException; - -public final class SecurityHttpExceptionHandler implements BiConsumer { - - private final Lifecycle lifecycle; - private final Logger logger; - private final BiConsumer fallback; - - public SecurityHttpExceptionHandler(Logger logger, Lifecycle lifecycle, BiConsumer fallback) { - this.lifecycle = lifecycle; - this.logger = logger; - this.fallback = fallback; - } - - public void accept(HttpChannel channel, Exception e) { - if (lifecycle.started() == false) { - return; - } - - if (isNotSslRecordException(e)) { - logger.warn("received plaintext http traffic on an https channel, closing connection {}", channel); - CloseableChannel.closeChannel(channel); - } else if (isCloseDuringHandshakeException(e)) { - logger.debug("connection {} closed during ssl handshake", channel); - CloseableChannel.closeChannel(channel); - } else if (isInsufficientBufferRemainingException(e)) { - logger.debug("connection {} closed abruptly", channel); - CloseableChannel.closeChannel(channel); - } else if (isReceivedCertificateUnknownException(e)) { - logger.warn("http client did not trust this server's certificate, closing connection {}", channel); - CloseableChannel.closeChannel(channel); - } else { - fallback.accept(channel, e); - } - } -} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/filter/IPFilter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/filter/IPFilter.java index 28087e64bcc8..e7c1cc5b9718 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/filter/IPFilter.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/filter/IPFilter.java @@ -18,6 +18,7 @@ import org.elasticsearch.common.transport.BoundTransportAddress; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.util.Maps; +import org.elasticsearch.http.HttpServerTransport; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.transport.TransportSettings; import org.elasticsearch.xpack.security.Security; @@ -45,7 +46,7 @@ public class IPFilter { * for HTTP. This name starts withs a dot, because no profile name can ever start like that due to * how we handle settings */ - public static final String HTTP_PROFILE_NAME = ".http"; + public static final String HTTP_PROFILE_NAME = HttpServerTransport.HTTP_PROFILE_NAME; public static final Setting ALLOW_BOUND_ADDRESSES_SETTING = Setting.boolSetting( setting("filter.always_allow_bound_address"), diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransport.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransport.java deleted file mode 100644 index 3bad4773a4a6..000000000000 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransport.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -package org.elasticsearch.xpack.security.transport.netty4; - -import io.netty.channel.Channel; -import io.netty.channel.ChannelHandler; -import io.netty.handler.ssl.SslHandler; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.elasticsearch.common.network.NetworkService; -import org.elasticsearch.common.settings.ClusterSettings; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.ssl.SslConfiguration; -import org.elasticsearch.common.util.PageCacheRecycler; -import org.elasticsearch.http.HttpChannel; -import org.elasticsearch.http.netty4.Netty4HttpServerTransport; -import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.tracing.Tracer; -import org.elasticsearch.transport.netty4.SharedGroupFactory; -import org.elasticsearch.xcontent.NamedXContentRegistry; -import org.elasticsearch.xpack.core.ssl.SSLService; -import org.elasticsearch.xpack.security.transport.SecurityHttpExceptionHandler; -import org.elasticsearch.xpack.security.transport.filter.IPFilter; - -import javax.net.ssl.SSLEngine; - -import static org.elasticsearch.xpack.core.XPackSettings.HTTP_SSL_ENABLED; - -public class SecurityNetty4HttpServerTransport extends Netty4HttpServerTransport { - private static final Logger logger = LogManager.getLogger(SecurityNetty4HttpServerTransport.class); - - private final SecurityHttpExceptionHandler securityExceptionHandler; - private final IPFilter ipFilter; - private final SSLService sslService; - private final SslConfiguration sslConfiguration; - - public SecurityNetty4HttpServerTransport( - Settings settings, - NetworkService networkService, - PageCacheRecycler recycler, - IPFilter ipFilter, - SSLService sslService, - ThreadPool threadPool, - NamedXContentRegistry xContentRegistry, - Dispatcher dispatcher, - ClusterSettings clusterSettings, - SharedGroupFactory sharedGroupFactory, - Tracer tracer - ) { - super(settings, networkService, threadPool, xContentRegistry, dispatcher, clusterSettings, sharedGroupFactory, tracer); - this.securityExceptionHandler = new SecurityHttpExceptionHandler(logger, lifecycle, (c, e) -> super.onException(c, e)); - this.ipFilter = ipFilter; - final boolean ssl = HTTP_SSL_ENABLED.get(settings); - this.sslService = sslService; - if (ssl) { - this.sslConfiguration = sslService.getHttpTransportSSLConfiguration(); - if (SSLService.isConfigurationValidForServerUsage(sslConfiguration) == false) { - throw new IllegalArgumentException( - "a key must be provided to run as a server. the key should be configured using the " - + "[xpack.security.http.ssl.key] or [xpack.security.http.ssl.keystore.path] setting" - ); - } - } else { - this.sslConfiguration = null; - } - } - - @Override - public void onException(HttpChannel channel, Exception e) { - securityExceptionHandler.accept(channel, e); - } - - @Override - protected void doStart() { - super.doStart(); - ipFilter.setBoundHttpTransportAddress(this.boundAddress()); - } - - @Override - public ChannelHandler configureServerChannelHandler() { - return new HttpSslChannelHandler(); - } - - private final class HttpSslChannelHandler extends HttpChannelHandler { - HttpSslChannelHandler() { - super(SecurityNetty4HttpServerTransport.this, handlingSettings); - } - - @Override - protected void initChannel(Channel ch) throws Exception { - super.initChannel(ch); - if (sslConfiguration != null) { - SSLEngine sslEngine = sslService.createSSLEngine(sslConfiguration, null, -1); - sslEngine.setUseClientMode(false); - ch.pipeline().addFirst("ssl", new SslHandler(sslEngine)); - } - ch.pipeline().addFirst("ip_filter", new IpFilterRemoteAddressFilter(ipFilter, IPFilter.HTTP_PROFILE_NAME)); - } - } -} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransportTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransportTests.java index d89eea591b13..6be9b738aaaf 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransportTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransportTests.java @@ -14,17 +14,17 @@ import org.elasticsearch.common.settings.MockSecureSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.ssl.SslClientAuthenticationMode; -import org.elasticsearch.common.util.PageCacheRecycler; import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.http.AbstractHttpServerTransportTestCase; import org.elasticsearch.http.NullDispatcher; +import org.elasticsearch.http.netty4.Netty4HttpServerTransport; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.tracing.Tracer; import org.elasticsearch.transport.netty4.SharedGroupFactory; +import org.elasticsearch.transport.netty4.TLSConfig; import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.ssl.SSLService; -import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.junit.Before; import java.nio.file.Path; @@ -67,18 +67,17 @@ public void createSSLService() { public void testDefaultClientAuth() throws Exception { Settings settings = Settings.builder().put(env.settings()).put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true).build(); sslService = new SSLService(TestEnvironment.newEnvironment(settings)); - SecurityNetty4HttpServerTransport transport = new SecurityNetty4HttpServerTransport( + Netty4HttpServerTransport transport = new Netty4HttpServerTransport( settings, new NetworkService(Collections.emptyList()), - mock(PageCacheRecycler.class), - mock(IPFilter.class), - sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), randomClusterSettings(), new SharedGroupFactory(settings), - Tracer.NOOP + Tracer.NOOP, + new TLSConfig(sslService.getHttpTransportSSLConfiguration(), sslService::createSSLEngine), + null ); ChannelHandler handler = transport.configureServerChannelHandler(); final EmbeddedChannel ch = new EmbeddedChannel(handler); @@ -94,18 +93,17 @@ public void testOptionalClientAuth() throws Exception { .put("xpack.security.http.ssl.client_authentication", value) .build(); sslService = new SSLService(TestEnvironment.newEnvironment(settings)); - SecurityNetty4HttpServerTransport transport = new SecurityNetty4HttpServerTransport( + Netty4HttpServerTransport transport = new Netty4HttpServerTransport( settings, new NetworkService(Collections.emptyList()), - mock(PageCacheRecycler.class), - mock(IPFilter.class), - sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), randomClusterSettings(), new SharedGroupFactory(settings), - Tracer.NOOP + Tracer.NOOP, + new TLSConfig(sslService.getHttpTransportSSLConfiguration(), sslService::createSSLEngine), + null ); ChannelHandler handler = transport.configureServerChannelHandler(); final EmbeddedChannel ch = new EmbeddedChannel(handler); @@ -121,18 +119,17 @@ public void testRequiredClientAuth() throws Exception { .put("xpack.security.http.ssl.client_authentication", value) .build(); sslService = new SSLService(TestEnvironment.newEnvironment(settings)); - SecurityNetty4HttpServerTransport transport = new SecurityNetty4HttpServerTransport( + Netty4HttpServerTransport transport = new Netty4HttpServerTransport( settings, new NetworkService(Collections.emptyList()), - mock(PageCacheRecycler.class), - mock(IPFilter.class), - sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), randomClusterSettings(), new SharedGroupFactory(settings), - Tracer.NOOP + Tracer.NOOP, + new TLSConfig(sslService.getHttpTransportSSLConfiguration(), sslService::createSSLEngine), + null ); ChannelHandler handler = transport.configureServerChannelHandler(); final EmbeddedChannel ch = new EmbeddedChannel(handler); @@ -148,18 +145,17 @@ public void testNoClientAuth() throws Exception { .put("xpack.security.http.ssl.client_authentication", value) .build(); sslService = new SSLService(TestEnvironment.newEnvironment(settings)); - SecurityNetty4HttpServerTransport transport = new SecurityNetty4HttpServerTransport( + Netty4HttpServerTransport transport = new Netty4HttpServerTransport( settings, new NetworkService(Collections.emptyList()), - mock(PageCacheRecycler.class), - mock(IPFilter.class), - sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), randomClusterSettings(), new SharedGroupFactory(settings), - Tracer.NOOP + Tracer.NOOP, + new TLSConfig(sslService.getHttpTransportSSLConfiguration(), sslService::createSSLEngine), + null ); ChannelHandler handler = transport.configureServerChannelHandler(); final EmbeddedChannel ch = new EmbeddedChannel(handler); @@ -170,18 +166,17 @@ public void testNoClientAuth() throws Exception { public void testCustomSSLConfiguration() throws Exception { Settings settings = Settings.builder().put(env.settings()).put(XPackSettings.HTTP_SSL_ENABLED.getKey(), true).build(); sslService = new SSLService(TestEnvironment.newEnvironment(settings)); - SecurityNetty4HttpServerTransport transport = new SecurityNetty4HttpServerTransport( + Netty4HttpServerTransport transport = new Netty4HttpServerTransport( settings, new NetworkService(Collections.emptyList()), - mock(PageCacheRecycler.class), - mock(IPFilter.class), - sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), randomClusterSettings(), new SharedGroupFactory(settings), - Tracer.NOOP + Tracer.NOOP, + new TLSConfig(sslService.getHttpTransportSSLConfiguration(), sslService::createSSLEngine), + null ); ChannelHandler handler = transport.configureServerChannelHandler(); EmbeddedChannel ch = new EmbeddedChannel(handler); @@ -193,18 +188,17 @@ public void testCustomSSLConfiguration() throws Exception { .put("xpack.security.http.ssl.supported_protocols", "TLSv1.2") .build(); sslService = new SSLService(TestEnvironment.newEnvironment(settings)); - transport = new SecurityNetty4HttpServerTransport( + transport = new Netty4HttpServerTransport( settings, new NetworkService(Collections.emptyList()), - mock(PageCacheRecycler.class), - mock(IPFilter.class), - sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), randomClusterSettings(), new SharedGroupFactory(settings), - Tracer.NOOP + Tracer.NOOP, + new TLSConfig(sslService.getHttpTransportSSLConfiguration(), sslService::createSSLEngine), + null ); handler = transport.configureServerChannelHandler(); ch = new EmbeddedChannel(handler); @@ -225,18 +219,17 @@ public void testNoExceptionWhenConfiguredWithoutSslKeySSLDisabled() throws Excep .build(); env = TestEnvironment.newEnvironment(settings); sslService = new SSLService(env); - SecurityNetty4HttpServerTransport transport = new SecurityNetty4HttpServerTransport( + Netty4HttpServerTransport transport = new Netty4HttpServerTransport( settings, new NetworkService(Collections.emptyList()), - mock(PageCacheRecycler.class), - mock(IPFilter.class), - sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), randomClusterSettings(), new SharedGroupFactory(settings), - Tracer.NOOP + Tracer.NOOP, + new TLSConfig(sslService.getHttpTransportSSLConfiguration(), sslService::createSSLEngine), + null ); assertNotNull(transport.configureServerChannelHandler()); } From 22f54f643781125307160877ea8828fb04ed7e36 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 5 Dec 2022 17:32:54 +1100 Subject: [PATCH 141/919] Fix time unit for connection request timeout of JWKs reload (#92080) This PR fixes a bug where connection request timeout is configured using the wrong time unit (it should be milli-second instead of second). Some test failures have been caused by this issue. Relates: #90467 Relates: #89509 --- docs/changelog/92080.yaml | 5 +++++ .../org/elasticsearch/xpack/security/authc/jwt/JwtUtil.java | 2 +- .../xpack/security/authc/jwt/JwtRealmAuthenticateTests.java | 2 -- 3 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 docs/changelog/92080.yaml diff --git a/docs/changelog/92080.yaml b/docs/changelog/92080.yaml new file mode 100644 index 000000000000..8d98c66a6562 --- /dev/null +++ b/docs/changelog/92080.yaml @@ -0,0 +1,5 @@ +pr: 92080 +summary: Fix time unit for connection request timeout of JWKs reload +area: Authentication +type: bug +issues: [] diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtUtil.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtUtil.java index b4a3f910467c..5b0be7de76fa 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtUtil.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtUtil.java @@ -251,7 +251,7 @@ public static CloseableHttpAsyncClient createHttpClient(final RealmConfig realmC final RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(Math.toIntExact(realmConfig.getSetting(JwtRealmSettings.HTTP_CONNECT_TIMEOUT).getMillis())) .setConnectionRequestTimeout( - Math.toIntExact(realmConfig.getSetting(JwtRealmSettings.HTTP_CONNECTION_READ_TIMEOUT).getSeconds()) + Math.toIntExact(realmConfig.getSetting(JwtRealmSettings.HTTP_CONNECTION_READ_TIMEOUT).getMillis()) ) .setSocketTimeout(Math.toIntExact(realmConfig.getSetting(JwtRealmSettings.HTTP_SOCKET_TIMEOUT).getMillis())) .build(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateTests.java index e8dfe16e448e..32c2ce32083a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateTests.java @@ -22,7 +22,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsException; import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.test.junit.annotations.TestLogging; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.Realm; import org.elasticsearch.xpack.core.security.authc.RealmSettings; @@ -98,7 +97,6 @@ public void testJwtAuthcRealmAuthcAuthzWithoutAuthzRealms() throws Exception { * Test with updated/removed/restored JWKs. * @throws Exception Unexpected test failure */ - @TestLogging(value = "org.elasticsearch.xpack.security.authc.jwt:trace", reason = "debug") public void testJwkSetUpdates() throws Exception { this.jwtIssuerAndRealms = this.generateJwtIssuerRealmPairs( this.createJwtRealmsSettingsBuilder(), From 10ee9f7d220dc03355501b16879f124e2a661f1a Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Mon, 5 Dec 2022 09:48:41 +0100 Subject: [PATCH 142/919] Upgrade vector tile protobuffer dependency to 3.16.3 (#92083) --- gradle/verification-metadata.xml | 6 +++--- x-pack/plugin/vector-tile/build.gradle | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 6789634a99ec..78110446539c 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -634,9 +634,9 @@ - - - + + + diff --git a/x-pack/plugin/vector-tile/build.gradle b/x-pack/plugin/vector-tile/build.gradle index e9ed8848cc38..46c3e2830ffb 100644 --- a/x-pack/plugin/vector-tile/build.gradle +++ b/x-pack/plugin/vector-tile/build.gradle @@ -29,11 +29,11 @@ dependencies { testImplementation(testArtifact(project(xpackModule('core')))) compileOnly "org.locationtech.jts:jts-core:${versions.jts}" api "com.wdtinc:mapbox-vector-tile:3.1.0" - api "com.google.protobuf:protobuf-java:3.16.1" + api "com.google.protobuf:protobuf-java:3.16.3" runtimeOnly("org.slf4j:slf4j-api:${versions.slf4j}") runtimeOnly("org.apache.logging.log4j:log4j-slf4j-impl:${versions.log4j}") javaRestTestImplementation("com.wdtinc:mapbox-vector-tile:3.1.0") - javaRestTestImplementation("com.google.protobuf:protobuf-java:3.16.1") + javaRestTestImplementation("com.google.protobuf:protobuf-java:3.16.3") } testClusters.configureEach { From 2e34bb61d7e43d56bd0d7f55673a52175f1240e9 Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Mon, 5 Dec 2022 10:09:59 +0100 Subject: [PATCH 143/919] Add commits listener for InternalEngine and CombinedDeletionPolicy (#92017) This committ introduces a listener-based mechanism to the InternalEngine and CombinedDeletionPolicy that allows to listen to newly created or deleted index commits. The listener can be configured in the IndexModule. It allows to listen to all commits creation (or deletion) as soon as an index shard is created (thus it also captures the Lucene commits executed when the shard is bootstrapped). When a listener is defined, the CombinedDeletionPolicy automatically acquires the new IndexCommit. This is important to ensure that the files of the commit won't be deleted by Lucene while a listener is working on the commit. For that reason the acquired commit must be released as soon as possible since it retains files on disk. By default, no listener is defined. --- docs/changelog/92017.yaml | 5 + .../index/shard/IndexShardIT.java | 3 +- .../indices/IndexingMemoryControllerIT.java | 3 +- .../org/elasticsearch/index/IndexModule.java | 9 +- .../org/elasticsearch/index/IndexService.java | 8 +- .../index/engine/CombinedDeletionPolicy.java | 48 ++++++- .../elasticsearch/index/engine/Engine.java | 22 ++++ .../index/engine/EngineConfig.java | 12 +- .../index/engine/InternalEngine.java | 69 ++++++---- .../elasticsearch/index/shard/IndexShard.java | 8 +- .../elasticsearch/index/IndexModuleTests.java | 104 +++++++++++++++ .../engine/CombinedDeletionPolicyTests.java | 117 ++++++++++++++++- .../index/engine/InternalEngineTests.java | 124 +++++++++++++++++- .../index/shard/IndexShardTests.java | 3 +- .../index/shard/RefreshListenersTests.java | 3 +- .../IndexingMemoryControllerTests.java | 3 +- .../index/engine/EngineTestCase.java | 24 ++-- .../index/shard/IndexShardTestCase.java | 3 +- .../index/engine/FollowingEngineTests.java | 3 +- 19 files changed, 515 insertions(+), 56 deletions(-) create mode 100644 docs/changelog/92017.yaml diff --git a/docs/changelog/92017.yaml b/docs/changelog/92017.yaml new file mode 100644 index 000000000000..84016910b258 --- /dev/null +++ b/docs/changelog/92017.yaml @@ -0,0 +1,5 @@ +pr: 92017 +summary: Add commits listener for `InternalEngine` and `CombinedDeletionPolicy` +area: Engine +type: enhancement +issues: [] diff --git a/server/src/internalClusterTest/java/org/elasticsearch/index/shard/IndexShardIT.java b/server/src/internalClusterTest/java/org/elasticsearch/index/shard/IndexShardIT.java index 839ef9fe52a0..cf18f1c3eaa1 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/index/shard/IndexShardIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/index/shard/IndexShardIT.java @@ -660,7 +660,8 @@ public static final IndexShard newIndexShard( RetentionLeaseSyncer.EMPTY, cbs, IndexModule.DEFAULT_SNAPSHOT_COMMIT_SUPPLIER, - System::nanoTime + System::nanoTime, + null ); } diff --git a/server/src/internalClusterTest/java/org/elasticsearch/indices/IndexingMemoryControllerIT.java b/server/src/internalClusterTest/java/org/elasticsearch/indices/IndexingMemoryControllerIT.java index e7b24d49008f..623cdd6c1070 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/indices/IndexingMemoryControllerIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/indices/IndexingMemoryControllerIT.java @@ -78,7 +78,8 @@ EngineConfig engineConfigWithLargerIndexingMemory(EngineConfig config) { config.getPrimaryTermSupplier(), config.getSnapshotCommitSupplier(), config.getLeafSorter(), - config.getRelativeTimeInNanosSupplier() + config.getRelativeTimeInNanosSupplier(), + config.getIndexCommitListener() ); } diff --git a/server/src/main/java/org/elasticsearch/index/IndexModule.java b/server/src/main/java/org/elasticsearch/index/IndexModule.java index 8fcd4a29c6b0..3610bb33a098 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexModule.java +++ b/server/src/main/java/org/elasticsearch/index/IndexModule.java @@ -165,6 +165,7 @@ public interface DirectoryWrapper extends CheckedFunction recoveryStateFactories; + private final SetOnce indexCommitListener = new SetOnce<>(); /** * Construct the index module for the index with the specified index settings. The index module contains extension points for plugins @@ -370,6 +371,11 @@ public void setDirectoryWrapper(DirectoryWrapper wrapper) { this.indexDirectoryWrapper.set(Objects.requireNonNull(wrapper)); } + public void setIndexCommitListener(Engine.IndexCommitListener listener) { + ensureNotFrozen(); + this.indexCommitListener.set(Objects.requireNonNull(listener)); + } + IndexEventListener freeze() { // pkg private for testing if (this.frozen.compareAndSet(false, true)) { return new CompositeIndexEventListener(indexSettings, indexEventListeners); @@ -517,7 +523,8 @@ public IndexService newIndexService( valuesSourceRegistry, recoveryStateFactory, indexFoldersDeletionListener, - snapshotCommitSupplier + snapshotCommitSupplier, + indexCommitListener.get() ); success = true; return indexService; diff --git a/server/src/main/java/org/elasticsearch/index/IndexService.java b/server/src/main/java/org/elasticsearch/index/IndexService.java index 53624459be47..ea5ae3a3c27b 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexService.java +++ b/server/src/main/java/org/elasticsearch/index/IndexService.java @@ -112,6 +112,7 @@ public class IndexService extends AbstractIndexComponent implements IndicesClust private final IndexStorePlugin.RecoveryStateFactory recoveryStateFactory; private final IndexStorePlugin.SnapshotCommitSupplier snapshotCommitSupplier; private final CheckedFunction readerWrapper; + private final Engine.IndexCommitListener indexCommitListener; private final IndexCache indexCache; private final MapperService mapperService; private final XContentParserConfiguration parserConfiguration; @@ -175,7 +176,8 @@ public IndexService( ValuesSourceRegistry valuesSourceRegistry, IndexStorePlugin.RecoveryStateFactory recoveryStateFactory, IndexStorePlugin.IndexFoldersDeletionListener indexFoldersDeletionListener, - IndexStorePlugin.SnapshotCommitSupplier snapshotCommitSupplier + IndexStorePlugin.SnapshotCommitSupplier snapshotCommitSupplier, + Engine.IndexCommitListener indexCommitListener ) { super(indexSettings); this.allowExpensiveQueries = allowExpensiveQueries; @@ -242,6 +244,7 @@ public IndexService( this.readerWrapper = wrapperFactory.apply(this); this.searchOperationListeners = Collections.unmodifiableList(searchOperationListeners); this.indexingOperationListeners = Collections.unmodifiableList(indexingOperationListeners); + this.indexCommitListener = indexCommitListener; try (var ignored = threadPool.getThreadContext().clearTraceContext()) { // kick off async ops for the first shard in this index this.refreshTask = new AsyncRefreshTask(this); @@ -516,7 +519,8 @@ public synchronized IndexShard createShard( retentionLeaseSyncer, circuitBreakerService, snapshotCommitSupplier, - System::nanoTime + System::nanoTime, + indexCommitListener ); eventListener.indexShardStateChanged(indexShard, null, indexShard.state(), "shard created"); eventListener.afterIndexShardCreated(indexShard); diff --git a/server/src/main/java/org/elasticsearch/index/engine/CombinedDeletionPolicy.java b/server/src/main/java/org/elasticsearch/index/engine/CombinedDeletionPolicy.java index dfdbafe6eb2a..77a72b27057c 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/CombinedDeletionPolicy.java +++ b/server/src/main/java/org/elasticsearch/index/engine/CombinedDeletionPolicy.java @@ -13,12 +13,14 @@ import org.apache.lucene.index.IndexDeletionPolicy; import org.apache.lucene.index.SegmentInfos; import org.elasticsearch.common.lucene.FilterIndexCommit; +import org.elasticsearch.core.Nullable; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.translog.Translog; import org.elasticsearch.index.translog.TranslogDeletionPolicy; import java.io.IOException; import java.nio.file.Path; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -38,6 +40,17 @@ public class CombinedDeletionPolicy extends IndexDeletionPolicy { private final SoftDeletesPolicy softDeletesPolicy; private final LongSupplier globalCheckpointSupplier; private final Map snapshottedCommits; // Number of snapshots held against each commit point. + + interface CommitsListener { + + void onNewAcquiredCommit(IndexCommit commit); + + void onDeletedCommit(IndexCommit commit); + } + + @Nullable + private final CommitsListener commitsListener; + private volatile IndexCommit safeCommit; // the most recent safe commit point - its max_seqno at most the persisted global checkpoint. private volatile long maxSeqNoOfNextSafeCommit; private volatile IndexCommit lastCommit; // the most recent commit point @@ -47,12 +60,14 @@ public class CombinedDeletionPolicy extends IndexDeletionPolicy { Logger logger, TranslogDeletionPolicy translogDeletionPolicy, SoftDeletesPolicy softDeletesPolicy, - LongSupplier globalCheckpointSupplier + LongSupplier globalCheckpointSupplier, + @Nullable CommitsListener commitsListener ) { this.logger = logger; this.translogDeletionPolicy = translogDeletionPolicy; this.softDeletesPolicy = softDeletesPolicy; this.globalCheckpointSupplier = globalCheckpointSupplier; + this.commitsListener = commitsListener; this.snapshottedCommits = new HashMap<>(); } @@ -86,11 +101,14 @@ public void onCommit(List commits) throws IOException { logger.info("failed to get the total docs from the safe commit; use the total docs from the previous safe commit", ex); totalDocsOfSafeCommit = safeCommitInfo.docCount; } + IndexCommit newCommit = null; + List deletedCommits = null; synchronized (this) { this.safeCommitInfo = new SafeCommitInfo( Long.parseLong(safeCommit.getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)), totalDocsOfSafeCommit ); + final IndexCommit previousLastCommit = this.lastCommit; this.lastCommit = commits.get(commits.size() - 1); this.safeCommit = safeCommit; updateRetentionPolicy(); @@ -99,13 +117,31 @@ public void onCommit(List commits) throws IOException { } else { this.maxSeqNoOfNextSafeCommit = Long.parseLong(commits.get(keptPosition + 1).getUserData().get(SequenceNumbers.MAX_SEQ_NO)); } + if (commitsListener != null && previousLastCommit != this.lastCommit) { + newCommit = acquireIndexCommit(false); + } for (int i = 0; i < keptPosition; i++) { - if (snapshottedCommits.containsKey(commits.get(i)) == false) { - deleteCommit(commits.get(i)); + final IndexCommit commit = commits.get(i); + if (snapshottedCommits.containsKey(commit) == false) { + deleteCommit(commit); + if (deletedCommits == null) { + deletedCommits = new ArrayList<>(); + } + deletedCommits.add(commit); } } } assert assertSafeCommitUnchanged(safeCommit); + if (commitsListener != null) { + if (newCommit != null) { + commitsListener.onNewAcquiredCommit(newCommit); + } + if (deletedCommits != null) { + for (IndexCommit deletedCommit : deletedCommits) { + commitsListener.onDeletedCommit(deletedCommit); + } + } + } } private boolean assertSafeCommitUnchanged(IndexCommit safeCommit) { @@ -154,7 +190,11 @@ synchronized IndexCommit acquireIndexCommit(boolean acquiringSafeCommit) { assert lastCommit != null : "Last commit is not initialized yet"; final IndexCommit snapshotting = acquiringSafeCommit ? safeCommit : lastCommit; snapshottedCommits.merge(snapshotting, 1, Integer::sum); // increase refCount - return new SnapshotIndexCommit(snapshotting); + return wrapCommit(snapshotting); + } + + protected IndexCommit wrapCommit(IndexCommit indexCommit) { + return new SnapshotIndexCommit(indexCommit); } /** diff --git a/server/src/main/java/org/elasticsearch/index/engine/Engine.java b/server/src/main/java/org/elasticsearch/index/engine/Engine.java index c05428e4abb2..0e28541ac3f3 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/Engine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/Engine.java @@ -214,6 +214,28 @@ public void verifyEngineBeforeIndexClosing() throws IllegalStateException { } } + public interface IndexCommitListener { + + /** + * This method is invoked each time a new Lucene commit is created through this engine. There is no guarantee that a listener will + * be notified of the commits in order, ie newer commits may appear before older ones. The {@link IndexCommitRef} prevents the + * {@link IndexCommitRef} files to be deleted from disk until the reference is closed. As such, the listener must close the + * reference as soon as it is done with it. + * + * @param indexCommitRef a reference on the newly created index commit + */ + void onNewCommit(ShardId shardId, Engine.IndexCommitRef indexCommitRef); + + /** + * This method is invoked after the policy deleted the given {@link IndexCommit}. A listener is never notified of a deleted commit + * until the corresponding {@link Engine.IndexCommitRef} received through {@link #onNewCommit(ShardId, IndexCommitRef)} has been + * closed; closing which in turn can call this method directly. + * + * @param deletedCommit the deleted {@link IndexCommit} + */ + void onIndexCommitDelete(ShardId shardId, IndexCommit deletedCommit); + } + /** * A throttling class that can be activated, causing the * {@code acquireThrottle} method to block on a lock when throttling diff --git a/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java b/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java index e8a2bf0aa58d..139446164a78 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java +++ b/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java @@ -125,6 +125,9 @@ public Supplier retentionLeasesSupplier() { private final LongSupplier relativeTimeInNanosSupplier; + @Nullable + private final Engine.IndexCommitListener indexCommitListener; + /** * Creates a new {@link org.elasticsearch.index.engine.EngineConfig} */ @@ -152,7 +155,8 @@ public EngineConfig( LongSupplier primaryTermSupplier, IndexStorePlugin.SnapshotCommitSupplier snapshotCommitSupplier, Comparator leafSorter, - LongSupplier relativeTimeInNanosSupplier + LongSupplier relativeTimeInNanosSupplier, + Engine.IndexCommitListener indexCommitListener ) { this.shardId = shardId; this.indexSettings = indexSettings; @@ -193,6 +197,7 @@ public EngineConfig( this.snapshotCommitSupplier = snapshotCommitSupplier; this.leafSorter = leafSorter; this.relativeTimeInNanosSupplier = relativeTimeInNanosSupplier; + this.indexCommitListener = indexCommitListener; } /** @@ -395,4 +400,9 @@ public Comparator getLeafSorter() { public LongSupplier getRelativeTimeInNanosSupplier() { return relativeTimeInNanosSupplier; } + + @Nullable + public Engine.IndexCommitListener getIndexCommitListener() { + return indexCommitListener; + } } diff --git a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java index a3cd492918ff..005090bc969c 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -105,6 +105,7 @@ import java.util.function.Function; import java.util.function.LongConsumer; import java.util.function.LongSupplier; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -226,7 +227,8 @@ public InternalEngine(EngineConfig engineConfig) { logger, translogDeletionPolicy, softDeletesPolicy, - translog::getLastSyncedGlobalCheckpoint + translog::getLastSyncedGlobalCheckpoint, + newCommitsListener() ); this.localCheckpointTracker = createLocalCheckpointTracker(localCheckpointTrackerSupplier); writer = createWriter(); @@ -322,6 +324,27 @@ private SoftDeletesPolicy newSoftDeletesPolicy() throws IOException { ); } + @Nullable + private CombinedDeletionPolicy.CommitsListener newCommitsListener() { + final Engine.IndexCommitListener listener = engineConfig.getIndexCommitListener(); + if (listener != null) { + return new CombinedDeletionPolicy.CommitsListener() { + @Override + public void onNewAcquiredCommit(final IndexCommit commit) { + final IndexCommitRef indexCommitRef = acquireIndexCommitRef(() -> commit); + assert indexCommitRef.getIndexCommit() == commit; + listener.onNewCommit(shardId, indexCommitRef); + } + + @Override + public void onDeletedCommit(IndexCommit commit) { + listener.onIndexCommitDelete(shardId, commit); + } + }; + } + return null; + } + @Override public CompletionStats completionStats(String... fieldNamePatterns) { return completionStatsCache.get(fieldNamePatterns); @@ -2158,22 +2181,14 @@ public void forceMerge(final boolean flush, int maxNumSegments, boolean onlyExpu } } - @Override - public IndexCommitRef acquireLastIndexCommit(final boolean flushFirst) throws EngineException { - // we have to flush outside of the readlock otherwise we might have a problem upgrading - // the to a write lock when we fail the engine in this operation - if (flushFirst) { - logger.trace("start flush for snapshot"); - flush(false, true); - logger.trace("finish flush for snapshot"); - } + private IndexCommitRef acquireIndexCommitRef(final Supplier indexCommitSupplier) { store.incRef(); boolean success = false; try { - final IndexCommit lastCommit = combinedDeletionPolicy.acquireIndexCommit(false); + final IndexCommit indexCommit = indexCommitSupplier.get(); final IndexCommitRef commitRef = new IndexCommitRef( - lastCommit, - () -> IOUtils.close(() -> releaseIndexCommit(lastCommit), store::decRef) + indexCommit, + () -> IOUtils.close(() -> releaseIndexCommit(indexCommit), store::decRef) ); success = true; return commitRef; @@ -2185,22 +2200,20 @@ public IndexCommitRef acquireLastIndexCommit(final boolean flushFirst) throws En } @Override - public IndexCommitRef acquireSafeIndexCommit() throws EngineException { - store.incRef(); - boolean success = false; - try { - final IndexCommit safeCommit = combinedDeletionPolicy.acquireIndexCommit(true); - final IndexCommitRef commitRef = new IndexCommitRef( - safeCommit, - () -> IOUtils.close(() -> releaseIndexCommit(safeCommit), store::decRef) - ); - success = true; - return commitRef; - } finally { - if (success == false) { - store.decRef(); - } + public IndexCommitRef acquireLastIndexCommit(final boolean flushFirst) throws EngineException { + // we have to flush outside of the readlock otherwise we might have a problem upgrading + // the to a write lock when we fail the engine in this operation + if (flushFirst) { + logger.trace("start flush for snapshot"); + flush(false, true); + logger.trace("finish flush for snapshot"); } + return acquireIndexCommitRef(() -> combinedDeletionPolicy.acquireIndexCommit(false)); + } + + @Override + public IndexCommitRef acquireSafeIndexCommit() throws EngineException { + return acquireIndexCommitRef(() -> combinedDeletionPolicy.acquireIndexCommit(true)); } private void releaseIndexCommit(IndexCommit snapshot) throws IOException { diff --git a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java index 947349ecb97e..336aa89c3eea 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java +++ b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java @@ -222,6 +222,7 @@ public class IndexShard extends AbstractIndexShardComponent implements IndicesCl private final PendingReplicationActions pendingReplicationActions; private final ReplicationTracker replicationTracker; private final IndexStorePlugin.SnapshotCommitSupplier snapshotCommitSupplier; + private final Engine.IndexCommitListener indexCommitListener; protected volatile ShardRouting shardRouting; protected volatile IndexShardState state; @@ -311,7 +312,8 @@ public IndexShard( final RetentionLeaseSyncer retentionLeaseSyncer, final CircuitBreakerService circuitBreakerService, final IndexStorePlugin.SnapshotCommitSupplier snapshotCommitSupplier, - final LongSupplier relativeTimeInNanosSupplier + final LongSupplier relativeTimeInNanosSupplier, + final Engine.IndexCommitListener indexCommitListener ) throws IOException { super(shardRouting.shardId(), indexSettings); assert shardRouting.initializing(); @@ -390,6 +392,7 @@ public IndexShard( this.refreshPendingLocationListener = new RefreshPendingLocationListener(); this.isDataStreamIndex = mapperService == null ? false : mapperService.mappingLookup().isDataStreamTimestampFieldEnabled(); this.relativeTimeInNanosSupplier = relativeTimeInNanosSupplier; + this.indexCommitListener = indexCommitListener; } public ThreadPool getThreadPool() { @@ -3270,7 +3273,8 @@ private EngineConfig newEngineConfig(LongSupplier globalCheckpointSupplier) { this::getOperationPrimaryTerm, snapshotCommitSupplier, isTimeseriesIndex ? TIMESERIES_LEAF_READERS_SORTER : null, - relativeTimeInNanosSupplier + relativeTimeInNanosSupplier, + indexCommitListener ); } diff --git a/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java b/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java index 61fa1809a665..9535eb671750 100644 --- a/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java +++ b/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java @@ -11,6 +11,7 @@ import org.apache.lucene.analysis.standard.StandardTokenizer; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.FieldInvertState; +import org.apache.lucene.index.IndexCommit; import org.apache.lucene.index.Term; import org.apache.lucene.search.CollectionStatistics; import org.apache.lucene.search.QueryCachingPolicy; @@ -23,13 +24,17 @@ import org.apache.lucene.tests.index.AssertingDirectoryReader; import org.apache.lucene.util.SetOnce.AlreadySetException; import org.elasticsearch.Version; +import org.elasticsearch.action.admin.indices.flush.FlushRequest; +import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.node.DiscoveryNodeRole; import org.elasticsearch.cluster.routing.RecoverySource; import org.elasticsearch.cluster.routing.ShardRouting; import org.elasticsearch.cluster.routing.UnassignedInfo; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.breaker.CircuitBreaker; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; @@ -51,12 +56,15 @@ import org.elasticsearch.index.cache.query.QueryCache; import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.engine.EngineTestCase; +import org.elasticsearch.index.engine.InternalEngine; import org.elasticsearch.index.engine.InternalEngineFactory; import org.elasticsearch.index.fielddata.IndexFieldDataCache; import org.elasticsearch.index.mapper.MapperRegistry; import org.elasticsearch.index.mapper.ParsedDocument; import org.elasticsearch.index.mapper.Uid; +import org.elasticsearch.index.seqno.RetentionLeaseSyncer; import org.elasticsearch.index.shard.IndexEventListener; +import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.IndexingOperationListener; import org.elasticsearch.index.shard.SearchOperationListener; import org.elasticsearch.index.shard.ShardId; @@ -84,24 +92,31 @@ import org.elasticsearch.threadpool.ThreadPool; import org.hamcrest.Matchers; +import java.io.Closeable; import java.io.IOException; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonMap; import static org.elasticsearch.index.IndexService.IndexCreationContext.CREATE_INDEX; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasToString; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.mock; public class IndexModuleTests extends ESTestCase { @@ -415,6 +430,7 @@ public void testFrozen() { assertEquals(msg, expectThrows(IllegalStateException.class, () -> module.setReaderWrapper(null)).getMessage()); assertEquals(msg, expectThrows(IllegalStateException.class, () -> module.forceQueryCacheProvider(null)).getMessage()); assertEquals(msg, expectThrows(IllegalStateException.class, () -> module.setDirectoryWrapper(null)).getMessage()); + assertEquals(msg, expectThrows(IllegalStateException.class, () -> module.setIndexCommitListener(null)).getMessage()); } public void testSetupUnknownSimilarity() { @@ -610,6 +626,94 @@ public void testRegisterCustomRecoveryStateFactory() throws IOException { indexService.close("closing", false); } + public void testIndexCommitListenerIsBound() throws IOException, ExecutionException, InterruptedException { + IndexModule module = new IndexModule( + indexSettings, + emptyAnalysisRegistry, + InternalEngine::new, + Collections.emptyMap(), + () -> true, + indexNameExpressionResolver, + Collections.emptyMap() + ); + + final AtomicReference lastAcquiredCommit = new AtomicReference<>(); + final AtomicReference lastDeletedCommit = new AtomicReference<>(); + + module.setIndexCommitListener(new Engine.IndexCommitListener() { + @Override + public void onNewCommit(ShardId shardId, Engine.IndexCommitRef indexCommitRef) { + lastAcquiredCommit.set(indexCommitRef); + } + + @Override + public void onIndexCommitDelete(ShardId shardId, IndexCommit deletedCommit) { + lastDeletedCommit.set(deletedCommit); + } + }); + + final List closeables = new ArrayList<>(); + try { + ShardId shardId = new ShardId("index", UUIDs.randomBase64UUID(random()), 0); + ShardRouting shardRouting = ShardRouting.newUnassigned( + shardId, + true, + RecoverySource.EmptyStoreRecoverySource.INSTANCE, + new UnassignedInfo(UnassignedInfo.Reason.INDEX_CREATED, null) + ).initialize("_node_id", null, -1); + + IndexService indexService = newIndexService(module); + closeables.add(() -> indexService.close("close index service at end of test", false)); + + IndexShard indexShard = indexService.createShard(shardRouting, s -> {}, RetentionLeaseSyncer.EMPTY); + closeables.add(() -> indexShard.close("close shard at end of test", true)); + indexShard.markAsRecovering( + "test", + new RecoveryState( + shardRouting, + new DiscoveryNode( + "_node_id", + "_node_id", + buildNewFakeTransportAddress(), + Collections.emptyMap(), + DiscoveryNodeRole.roles(), + Version.CURRENT + ), + null + ) + ); + + final PlainActionFuture recoveryFuture = PlainActionFuture.newFuture(); + indexShard.recoverFromStore(recoveryFuture); + recoveryFuture.get(); + + Engine.IndexCommitRef lastCommitRef = lastAcquiredCommit.get(); + assertThat(lastCommitRef, notNullValue()); + IndexCommit lastCommit = lastCommitRef.getIndexCommit(); + assertThat(lastCommit.getGeneration(), equalTo(2L)); + IndexCommit lastDeleted = lastDeletedCommit.get(); + assertThat(lastDeleted, nullValue()); + + lastCommitRef.close(); + + indexShard.flush(new FlushRequest("index").force(true)); + + lastDeleted = lastDeletedCommit.get(); + assertThat(lastDeleted.getGeneration(), equalTo(lastCommit.getGeneration())); + assertThat(lastDeleted.getSegmentsFileName(), equalTo(lastCommit.getSegmentsFileName())); + assertThat(lastDeleted.isDeleted(), equalTo(true)); + + lastCommitRef = lastAcquiredCommit.get(); + assertThat(lastCommitRef, notNullValue()); + lastCommit = lastCommitRef.getIndexCommit(); + assertThat(lastCommit.getGeneration(), equalTo(3L)); + + lastCommitRef.close(); + } finally { + IOUtils.close(closeables); + } + } + private ShardRouting createInitializedShardRouting() { ShardRouting shard = ShardRouting.newUnassigned( new ShardId("test", "_na_", 0), diff --git a/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java b/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java index 5d96464c8c97..9dbe040168e3 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java @@ -10,6 +10,7 @@ import org.apache.lucene.index.IndexCommit; import org.apache.lucene.store.Directory; +import org.elasticsearch.common.lucene.FilterIndexCommit; import org.elasticsearch.index.seqno.RetentionLeases; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.translog.Translog; @@ -26,7 +27,10 @@ import java.util.concurrent.atomic.AtomicLong; import static org.elasticsearch.index.seqno.SequenceNumbers.NO_OPS_PERFORMED; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -266,12 +270,123 @@ public void testCheckUnreferencedCommits() throws Exception { } } + public void testCommitsListener() throws Exception { + final List acquiredCommits = new ArrayList<>(); + final List deletedCommits = new ArrayList<>(); + final CombinedDeletionPolicy.CommitsListener commitsListener = new CombinedDeletionPolicy.CommitsListener() { + @Override + public void onNewAcquiredCommit(IndexCommit commit) { + assertThat(commit, instanceOf(FilterIndexCommit.class)); + assertThat(acquiredCommits.add(((FilterIndexCommit) commit).getIndexCommit()), equalTo(true)); + } + + @Override + public void onDeletedCommit(IndexCommit commit) { + assertThat(acquiredCommits.remove(commit), equalTo(true)); + assertThat(deletedCommits.add(commit), equalTo(true)); + assertThat(commit.isDeleted(), equalTo(true)); + } + }; + + final AtomicLong globalCheckpoint = new AtomicLong(0L); + final TranslogDeletionPolicy translogDeletionPolicy = new TranslogDeletionPolicy(); + final SoftDeletesPolicy softDeletesPolicy = new SoftDeletesPolicy( + globalCheckpoint::get, + NO_OPS_PERFORMED, + 0L, + () -> RetentionLeases.EMPTY + ); + final CombinedDeletionPolicy combinedDeletionPolicy = new CombinedDeletionPolicy( + logger, + translogDeletionPolicy, + softDeletesPolicy, + globalCheckpoint::get, + commitsListener + ) { + @Override + protected int getDocCountOfCommit(IndexCommit indexCommit) { + return 10; + } + + @Override + synchronized boolean releaseCommit(IndexCommit indexCommit) { + return super.releaseCommit(wrapCommit(indexCommit)); + } + }; + + final UUID translogUUID = UUID.randomUUID(); + final IndexCommit commit0 = mockIndexCommit(NO_OPS_PERFORMED, NO_OPS_PERFORMED, translogUUID); + combinedDeletionPolicy.onInit(List.of(commit0)); + + assertThat(acquiredCommits, contains(commit0)); + assertThat(deletedCommits, hasSize(0)); + + final IndexCommit commit1 = mockIndexCommit(10L, 10L, translogUUID); + combinedDeletionPolicy.onCommit(List.of(commit0, commit1)); + + assertThat(acquiredCommits, contains(commit0, commit1)); + assertThat(deletedCommits, hasSize(0)); + + globalCheckpoint.set(10L); + final IndexCommit commit2 = mockIndexCommit(20L, 20L, translogUUID); + combinedDeletionPolicy.onCommit(List.of(commit0, commit1, commit2)); + + assertThat(acquiredCommits, contains(commit0, commit1, commit2)); + assertThat(deletedCommits, hasSize(0)); + + boolean maybeCleanUpCommits = combinedDeletionPolicy.releaseCommit(commit0); + assertThat(maybeCleanUpCommits, equalTo(true)); + + globalCheckpoint.set(20L); + final IndexCommit commit3 = mockIndexCommit(30L, 30L, translogUUID); + combinedDeletionPolicy.onCommit(List.of(commit0, commit1, commit2, commit3)); + + assertThat(acquiredCommits, contains(commit1, commit2, commit3)); + assertThat(deletedCommits, contains(commit0)); + + maybeCleanUpCommits = combinedDeletionPolicy.releaseCommit(commit2); + assertThat("No commits to clean up (commit #2 is the safe commit)", maybeCleanUpCommits, equalTo(false)); + + globalCheckpoint.set(30L); + final IndexCommit commit4 = mockIndexCommit(40L, 40L, translogUUID); + combinedDeletionPolicy.onCommit(List.of(commit1, commit2, commit3, commit4)); + + assertThat(acquiredCommits, contains(commit1, commit3, commit4)); + assertThat(deletedCommits, contains(commit0, commit2)); + + maybeCleanUpCommits = combinedDeletionPolicy.releaseCommit(commit3); + assertThat("No commits to clean up (commit #3 is the safe commit)", maybeCleanUpCommits, equalTo(false)); + + maybeCleanUpCommits = combinedDeletionPolicy.releaseCommit(commit4); + assertThat("No commits to clean up (commit #4 is the last commit)", maybeCleanUpCommits, equalTo(false)); + + maybeCleanUpCommits = combinedDeletionPolicy.releaseCommit(commit1); + assertThat(maybeCleanUpCommits, equalTo(true)); + + final boolean globalCheckpointCatchUp = randomBoolean(); + globalCheckpoint.set(globalCheckpointCatchUp ? 50L : 40L); + + final IndexCommit commit5 = mockIndexCommit(50L, 50L, translogUUID); + combinedDeletionPolicy.onCommit(List.of(commit1, commit3, commit4, commit5)); + + if (globalCheckpointCatchUp) { + assertThat(acquiredCommits, contains(commit5)); + assertThat(deletedCommits, contains(commit0, commit2, commit1, commit3, commit4)); + } else { + assertThat(acquiredCommits, contains(commit4, commit5)); + assertThat(deletedCommits, contains(commit0, commit2, commit1, commit3)); + } + + maybeCleanUpCommits = combinedDeletionPolicy.releaseCommit(commit5); + assertThat("No commits to clean up (commit #5 is the last commit)", maybeCleanUpCommits, equalTo(false)); + } + private CombinedDeletionPolicy newCombinedDeletionPolicy( TranslogDeletionPolicy translogPolicy, SoftDeletesPolicy softDeletesPolicy, AtomicLong globalCheckpoint ) { - return new CombinedDeletionPolicy(logger, translogPolicy, softDeletesPolicy, globalCheckpoint::get) { + return new CombinedDeletionPolicy(logger, translogPolicy, softDeletesPolicy, globalCheckpoint::get, null) { @Override protected int getDocCountOfCommit(IndexCommit indexCommit) throws IOException { if (randomBoolean()) { diff --git a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java index 9fabf2333daf..418830c7b2eb 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -193,6 +193,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.emptyArray; +import static org.hamcrest.Matchers.emptyIterable; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; @@ -3588,7 +3589,8 @@ public void testRecoverFromForeignTranslog() throws IOException { primaryTerm::get, IndexModule.DEFAULT_SNAPSHOT_COMMIT_SUPPLIER, null, - config.getRelativeTimeInNanosSupplier() + config.getRelativeTimeInNanosSupplier(), + null ); expectThrows(EngineCreationFailureException.class, () -> new InternalEngine(brokenConfig)); @@ -7257,7 +7259,8 @@ public void testNotWarmUpSearcherInEngineCtor() throws Exception { config.getPrimaryTermSupplier(), config.getSnapshotCommitSupplier(), config.getLeafSorter(), - config.getRelativeTimeInNanosSupplier() + config.getRelativeTimeInNanosSupplier(), + config.getIndexCommitListener() ); try (InternalEngine engine = createEngine(configWithWarmer)) { assertThat(warmedUpReaders, empty()); @@ -7471,4 +7474,121 @@ public void testTrimUnsafeCommitHasESVersionInUserData() throws IOException { assertThat(userDataAfterTrimUnsafeCommits.get(ES_VERSION), is(equalTo(Version.CURRENT.toString()))); } } + + public void testIndexCommitsListener() throws Exception { + final Map acquiredCommits = new HashMap<>(); + final List deletedCommits = new ArrayList<>(); + + final Engine.IndexCommitListener indexCommitListener = new Engine.IndexCommitListener() { + @Override + public void onNewCommit(ShardId shardId, Engine.IndexCommitRef indexCommitRef) { + assertThat(acquiredCommits.put(indexCommitRef.getIndexCommit(), indexCommitRef), nullValue()); + assertThat(shardId, equalTo(InternalEngineTests.this.shardId)); + } + + @Override + public void onIndexCommitDelete(ShardId shardId, IndexCommit deletedCommit) { + assertThat(acquiredCommits.remove(deletedCommit), notNullValue()); + assertThat(deletedCommits.add(deletedCommit), equalTo(true)); + assertThat(deletedCommit.isDeleted(), equalTo(true)); + assertThat(shardId, equalTo(InternalEngineTests.this.shardId)); + } + }; + + final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); + try ( + Store store = createStore(); + InternalEngine engine = createEngine( + config( + defaultSettings, + store, + createTempDir(), + NoMergePolicy.INSTANCE, + null, + null, + null, + globalCheckpoint::get, + () -> RetentionLeases.EMPTY, + new NoneCircuitBreakerService(), + indexCommitListener + ) + ) + ) { + assertCommitGenerations(acquiredCommits, List.of(2L)); + assertCommitGenerations(deletedCommits, List.of()); + + engine.index(indexForDoc(createParsedDoc("a", EngineTestCase.randomIdFieldType(), null))); + engine.flush(); + + assertCommitGenerations(acquiredCommits, List.of(2L, 3L)); + assertCommitGenerations(deletedCommits, List.of()); + + globalCheckpoint.set(0L); + engine.index(indexForDoc(createParsedDoc("b", EngineTestCase.randomIdFieldType(), null))); + engine.flush(); + + assertCommitGenerations(acquiredCommits, List.of(2L, 3L, 4L)); + assertCommitGenerations(deletedCommits, List.of()); + + releaseCommitRef(acquiredCommits, 2L); + + globalCheckpoint.set(1L); + engine.index(indexForDoc(createParsedDoc("c", EngineTestCase.randomIdFieldType(), null))); + engine.flush(); + + assertCommitGenerations(acquiredCommits, List.of(3L, 4L, 5L)); + assertCommitGenerations(deletedCommits, List.of(2L)); + + releaseCommitRef(acquiredCommits, 4L); + + globalCheckpoint.set(2L); + engine.index(indexForDoc(createParsedDoc("d", EngineTestCase.randomIdFieldType(), null))); + engine.flush(); + + assertCommitGenerations(acquiredCommits, List.of(3L, 5L, 6L)); + assertCommitGenerations(deletedCommits, List.of(2L, 4L)); + + releaseCommitRef(acquiredCommits, 5L); + releaseCommitRef(acquiredCommits, 6L); + releaseCommitRef(acquiredCommits, 3L); + + final boolean globalCheckpointCatchUp = randomBoolean(); + globalCheckpoint.set(globalCheckpointCatchUp ? 4L : 3L); + + engine.index(indexForDoc(createParsedDoc("e", EngineTestCase.randomIdFieldType(), null))); + engine.flush(); + + if (globalCheckpointCatchUp) { + assertCommitGenerations(acquiredCommits, List.of(7L)); + assertCommitGenerations(deletedCommits, List.of(2L, 3L, 4L, 5L, 6L)); + } else { + assertCommitGenerations(acquiredCommits, List.of(6L, 7L)); + assertCommitGenerations(deletedCommits, List.of(2L, 3L, 4L, 5L)); + } + + releaseCommitRef(acquiredCommits, 7L); + } + } + + private static void assertCommitGenerations(Map commits, List expectedGenerations) { + assertCommitGenerations(commits.values().stream().map(Engine.IndexCommitRef::getIndexCommit).toList(), expectedGenerations); + } + + private static void assertCommitGenerations(List commits, List expectedGenerations) { + assertThat( + commits.stream().map(IndexCommit::getGeneration).sorted().toList(), + expectedGenerations.isEmpty() ? emptyIterable() : equalTo(expectedGenerations) + ); + } + + private static void releaseCommitRef(Map commits, long generation) { + var releasable = commits.keySet().stream().filter(c -> c.getGeneration() == generation).findFirst(); + assertThat(releasable.isPresent(), is(true)); + Engine.IndexCommitRef indexCommitRef = commits.get(releasable.get()); + try { + indexCommitRef.close(); + } catch (IOException e) { + throw new AssertionError("Failed to release IndexCommitRef for commit generation " + generation, e); + } + } } diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java index 9d49cca516da..9dda27b4521b 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -4519,7 +4519,8 @@ public void testCloseShardWhileEngineIsWarming() throws Exception { config.getPrimaryTermSupplier(), IndexModule.DEFAULT_SNAPSHOT_COMMIT_SUPPLIER, config.getLeafSorter(), - config.getRelativeTimeInNanosSupplier() + config.getRelativeTimeInNanosSupplier(), + config.getIndexCommitListener() ); return new InternalEngine(configWithWarmer); }); diff --git a/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java b/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java index 7f7a1e0cf2b4..59e2fe1baa19 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java @@ -152,7 +152,8 @@ public void onFailedEngine(String reason, @Nullable Exception e) { () -> primaryTerm, IndexModule.DEFAULT_SNAPSHOT_COMMIT_SUPPLIER, null, - System::nanoTime + System::nanoTime, + null ); engine = new InternalEngine(config); engine.recoverFromTranslog((e, s) -> 0, Long.MAX_VALUE); diff --git a/server/src/test/java/org/elasticsearch/indices/IndexingMemoryControllerTests.java b/server/src/test/java/org/elasticsearch/indices/IndexingMemoryControllerTests.java index b8cc5b2fd1c1..78606856bf15 100644 --- a/server/src/test/java/org/elasticsearch/indices/IndexingMemoryControllerTests.java +++ b/server/src/test/java/org/elasticsearch/indices/IndexingMemoryControllerTests.java @@ -401,7 +401,8 @@ EngineConfig configWithRefreshListener(EngineConfig config, ReferenceManager.Ref config.getPrimaryTermSupplier(), config.getSnapshotCommitSupplier(), config.getLeafSorter(), - config.getRelativeTimeInNanosSupplier() + config.getRelativeTimeInNanosSupplier(), + config.getIndexCommitListener() ); } diff --git a/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java index ac48caafb8e8..9156a8c3b242 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java @@ -274,7 +274,8 @@ public EngineConfig copy(EngineConfig config, LongSupplier globalCheckpointSuppl config.getPrimaryTermSupplier(), config.getSnapshotCommitSupplier(), config.getLeafSorter(), - config.getRelativeTimeInNanosSupplier() + config.getRelativeTimeInNanosSupplier(), + config.getIndexCommitListener() ); } @@ -303,7 +304,8 @@ public EngineConfig copy(EngineConfig config, Analyzer analyzer) { config.getPrimaryTermSupplier(), config.getSnapshotCommitSupplier(), config.getLeafSorter(), - config.getRelativeTimeInNanosSupplier() + config.getRelativeTimeInNanosSupplier(), + config.getIndexCommitListener() ); } @@ -332,7 +334,8 @@ public EngineConfig copy(EngineConfig config, MergePolicy mergePolicy) { config.getPrimaryTermSupplier(), config.getSnapshotCommitSupplier(), config.getLeafSorter(), - config.getRelativeTimeInNanosSupplier() + config.getRelativeTimeInNanosSupplier(), + config.getIndexCommitListener() ); } @@ -754,7 +757,8 @@ public EngineConfig config( indexSort, globalCheckpointSupplier, retentionLeasesSupplier, - new NoneCircuitBreakerService() + new NoneCircuitBreakerService(), + null ); } @@ -779,7 +783,8 @@ public EngineConfig config( indexSort, maybeGlobalCheckpointSupplier, maybeGlobalCheckpointSupplier == null ? null : () -> RetentionLeases.EMPTY, - breakerService + breakerService, + null ); } @@ -793,7 +798,8 @@ public EngineConfig config( final Sort indexSort, final @Nullable LongSupplier maybeGlobalCheckpointSupplier, final @Nullable Supplier maybeRetentionLeasesSupplier, - final CircuitBreakerService breakerService + final CircuitBreakerService breakerService, + final @Nullable Engine.IndexCommitListener indexCommitListener ) { final IndexWriterConfig iwc = newIndexWriterConfig(); final TranslogConfig translogConfig = new TranslogConfig(shardId, translogPath, indexSettings, BigArrays.NON_RECYCLING_INSTANCE); @@ -851,7 +857,8 @@ public EngineConfig config( primaryTerm, IndexModule.DEFAULT_SNAPSHOT_COMMIT_SUPPLIER, null, - System::nanoTime + System::nanoTime, + indexCommitListener ); } @@ -888,7 +895,8 @@ protected EngineConfig config(EngineConfig config, Store store, Path translogPat config.getPrimaryTermSupplier(), config.getSnapshotCommitSupplier(), config.getLeafSorter(), - config.getRelativeTimeInNanosSupplier() + config.getRelativeTimeInNanosSupplier(), + config.getIndexCommitListener() ); } diff --git a/test/framework/src/main/java/org/elasticsearch/index/shard/IndexShardTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/shard/IndexShardTestCase.java index 4a03170b170c..9923340fdd85 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/shard/IndexShardTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/shard/IndexShardTestCase.java @@ -503,7 +503,8 @@ protected IndexShard newShard( retentionLeaseSyncer, breakerService, IndexModule.DEFAULT_SNAPSHOT_COMMIT_SUPPLIER, - relativeTimeSupplier + relativeTimeSupplier, + null ); indexShard.addShardFailureCallback(DEFAULT_SHARD_FAILURE_HANDLER); success = true; diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/index/engine/FollowingEngineTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/index/engine/FollowingEngineTests.java index 8472b23865e1..9a10269df0ea 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/index/engine/FollowingEngineTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/index/engine/FollowingEngineTests.java @@ -287,7 +287,8 @@ public void onFailedEngine(String reason, Exception e) { () -> primaryTerm.get(), IndexModule.DEFAULT_SNAPSHOT_COMMIT_SUPPLIER, null, - System::nanoTime + System::nanoTime, + null ); } From ee452bd143700401a74fb93722fd5514bd10b6ff Mon Sep 17 00:00:00 2001 From: Cleydyr Bezerra de Albuquerque Date: Mon, 5 Dec 2022 10:53:06 +0100 Subject: [PATCH 144/919] Update circuit-breaker-errors.asciidoc (#92070) Fix typo fieldata -> fielddata --- .../common-issues/circuit-breaker-errors.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/troubleshooting/common-issues/circuit-breaker-errors.asciidoc b/docs/reference/troubleshooting/common-issues/circuit-breaker-errors.asciidoc index ca815dd3c04d..fe79ef57b6ea 100644 --- a/docs/reference/troubleshooting/common-issues/circuit-breaker-errors.asciidoc +++ b/docs/reference/troubleshooting/common-issues/circuit-breaker-errors.asciidoc @@ -82,7 +82,7 @@ you've enabled fielddata and triggered the <>, consider disabling it and using a `keyword` field instead. See <>. -**Clear the fieldata cache** +**Clear the fielddata cache** If you've triggered the fielddata circuit breaker and can't disable fielddata, use the <> to clear the fielddata cache. @@ -92,4 +92,4 @@ This may disrupt any in-flight searches that use fielddata. ---- POST _cache/clear?fielddata=true ---- -// TEST[s/^/PUT my-index\n/] \ No newline at end of file +// TEST[s/^/PUT my-index\n/] From 1063ddeef01a72a361643a008b065a07224bbd47 Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Mon, 5 Dec 2022 11:39:46 +0100 Subject: [PATCH 145/919] Add support for GeoRelation#QUERY_CONTAINS (#91876) Tree visitors now return GeoRelation#QUERY_CONTAINS when the provided geometry fully contains the tree. --- .../fielddata/Component2DRelationVisitor.java | 76 ++++++++++++++----- .../spatial/index/fielddata/GeoRelation.java | 3 +- .../index/fielddata/Tile2DVisitor.java | 2 +- .../geogrid/AbstractGeoHashGridTiler.java | 12 +-- .../geogrid/AbstractGeoTileGridTiler.java | 2 +- .../LatLonGeometryRelationVisitorTests.java | 10 +++ .../index/fielddata/Tile2DVisitorTests.java | 64 ++++++++-------- 7 files changed, 109 insertions(+), 60 deletions(-) diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Component2DRelationVisitor.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Component2DRelationVisitor.java index c84094e6eeb6..2402cb8ca644 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Component2DRelationVisitor.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Component2DRelationVisitor.java @@ -28,24 +28,28 @@ class Component2DRelationVisitor extends TriangleTreeReader.DecodedVisitor { public void reset(Component2D component2D) { this.component2D = component2D; - relation = GeoRelation.QUERY_DISJOINT; + relation = null; } /** * return the computed relation. */ public GeoRelation relation() { - return relation; + return relation == null ? GeoRelation.QUERY_DISJOINT : relation; } @Override void visitDecodedPoint(double x, double y) { if (component2D.contains(x, y)) { - if (component2D.withinPoint(x, y) == Component2D.WithinRelation.CANDIDATE) { + if (canBeContained()) { + relation = GeoRelation.QUERY_CONTAINS; + } else if (component2D.withinPoint(x, y) == Component2D.WithinRelation.CANDIDATE) { relation = GeoRelation.QUERY_INSIDE; } else { relation = GeoRelation.QUERY_CROSSES; } + } else { + adjustRelationForNotIntersectingComponent(); } } @@ -53,26 +57,36 @@ void visitDecodedPoint(double x, double y) { void visitDecodedLine(double aX, double aY, double bX, double bY, byte metadata) { if (component2D.intersectsLine(aX, aY, bX, bY)) { final boolean ab = (metadata & 1 << 4) == 1 << 4; - if (component2D.withinLine(aX, aY, ab, bX, bY) == Component2D.WithinRelation.CANDIDATE) { + if (canBeContained() && component2D.containsLine(aX, aY, bX, bY)) { + relation = GeoRelation.QUERY_CONTAINS; + } else if (canBeInside() && component2D.withinLine(aX, aY, ab, bX, bY) == Component2D.WithinRelation.CANDIDATE) { relation = GeoRelation.QUERY_INSIDE; } else { relation = GeoRelation.QUERY_CROSSES; } + } else { + adjustRelationForNotIntersectingComponent(); } } @Override void visitDecodedTriangle(double aX, double aY, double bX, double bY, double cX, double cY, byte metadata) { if (component2D.intersectsTriangle(aX, aY, bX, bY, cX, cY)) { - boolean ab = (metadata & 1 << 4) == 1 << 4; - boolean bc = (metadata & 1 << 5) == 1 << 5; - boolean ca = (metadata & 1 << 6) == 1 << 6; - if (component2D.withinTriangle(aX, aY, ab, bX, bY, bc, cX, cY, ca) == Component2D.WithinRelation.CANDIDATE) { - relation = GeoRelation.QUERY_INSIDE; - } else { - relation = GeoRelation.QUERY_CROSSES; - } + final boolean ab = (metadata & 1 << 4) == 1 << 4; + final boolean bc = (metadata & 1 << 5) == 1 << 5; + final boolean ca = (metadata & 1 << 6) == 1 << 6; + if (canBeContained() && component2D.containsTriangle(aX, aY, bX, bY, cX, cY)) { + relation = GeoRelation.QUERY_CONTAINS; + } else if (canBeInside() + && component2D.withinTriangle(aX, aY, ab, bX, bY, bc, cX, cY, ca) == Component2D.WithinRelation.CANDIDATE) { + relation = GeoRelation.QUERY_INSIDE; + } else { + relation = GeoRelation.QUERY_CROSSES; + } + } else { + adjustRelationForNotIntersectingComponent(); } + } @Override @@ -82,17 +96,29 @@ public boolean push() { @Override public boolean pushDecodedX(double minX) { - return component2D.getMaxX() >= minX; + if (component2D.getMaxX() >= minX) { + return true; + } + adjustRelationForNotIntersectingComponent(); + return false; } @Override public boolean pushDecodedY(double minY) { - return component2D.getMaxY() >= minY; + if (component2D.getMaxY() >= minY) { + return true; + } + adjustRelationForNotIntersectingComponent(); + return false; } @Override public boolean pushDecoded(double maxX, double maxY) { - return component2D.getMinY() <= maxY && component2D.getMinX() <= maxX; + if (component2D.getMinY() <= maxY && component2D.getMinX() <= maxX) { + return true; + } + adjustRelationForNotIntersectingComponent(); + return false; } @Override @@ -104,11 +130,23 @@ public boolean pushDecoded(double minX, double minY, double maxX, double maxY) { relation = GeoRelation.QUERY_DISJOINT; return false; } - if (rel == PointValues.Relation.CELL_INSIDE_QUERY) { - // the rectangle fully contains the shape + return true; + } + + private void adjustRelationForNotIntersectingComponent() { + if (relation == null) { + relation = GeoRelation.QUERY_DISJOINT; + } else if (relation == GeoRelation.QUERY_CONTAINS) { relation = GeoRelation.QUERY_CROSSES; - return false; } - return true; } + + private boolean canBeContained() { + return relation == null || relation == GeoRelation.QUERY_CONTAINS; + } + + private boolean canBeInside() { + return relation != GeoRelation.QUERY_CONTAINS; + } + } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeoRelation.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeoRelation.java index c940fe78ebdd..edfd730291fb 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeoRelation.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeoRelation.java @@ -14,5 +14,6 @@ public enum GeoRelation { QUERY_CROSSES, QUERY_INSIDE, - QUERY_DISJOINT + QUERY_DISJOINT, + QUERY_CONTAINS } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Tile2DVisitor.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Tile2DVisitor.java index 31d50ff3f4e6..126f80d1079b 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Tile2DVisitor.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Tile2DVisitor.java @@ -96,7 +96,7 @@ public boolean push(int minX, int minY, int maxX, int maxY) { } if (this.minX <= minX && this.maxX >= maxX && this.minY <= minY && this.maxY >= maxY) { // the rectangle fully contains the shape - relation = GeoRelation.QUERY_CROSSES; + relation = GeoRelation.QUERY_CONTAINS; return false; } return true; diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/AbstractGeoHashGridTiler.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/AbstractGeoHashGridTiler.java index be4ca89e73ba..20ac15f7b9b6 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/AbstractGeoHashGridTiler.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/AbstractGeoHashGridTiler.java @@ -110,21 +110,21 @@ protected int setValuesByRasterization(String hash, GeoShapeCellValues values, i String[] hashes = Geohash.getSubGeohashes(hash); for (String s : hashes) { GeoRelation relation = relateTile(geoValue, s); - if (relation == GeoRelation.QUERY_CROSSES) { + if (relation == GeoRelation.QUERY_INSIDE) { if (s.length() == precision) { values.resizeCell(valuesIndex + 1); values.add(valuesIndex++, Geohash.longEncode(s)); } else { - valuesIndex = setValuesByRasterization(s, values, valuesIndex, geoValue); + int numTilesAtPrecision = getNumTilesAtPrecision(precision, hash.length()); + values.resizeCell(getNewSize(valuesIndex, numTilesAtPrecision + 1)); + valuesIndex = setValuesForFullyContainedTile(s, values, valuesIndex, precision); } - } else if (relation == GeoRelation.QUERY_INSIDE) { + } else if (relation != GeoRelation.QUERY_DISJOINT) { if (s.length() == precision) { values.resizeCell(valuesIndex + 1); values.add(valuesIndex++, Geohash.longEncode(s)); } else { - int numTilesAtPrecision = getNumTilesAtPrecision(precision, hash.length()); - values.resizeCell(getNewSize(valuesIndex, numTilesAtPrecision + 1)); - valuesIndex = setValuesForFullyContainedTile(s, values, valuesIndex, precision); + valuesIndex = setValuesByRasterization(s, values, valuesIndex, geoValue); } } } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/AbstractGeoTileGridTiler.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/AbstractGeoTileGridTiler.java index 68717dd8b68a..9ae4cf1c8ecb 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/AbstractGeoTileGridTiler.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/AbstractGeoTileGridTiler.java @@ -154,7 +154,7 @@ protected int setValuesByRasterization( values.resizeCell(getNewSize(valuesIndex, numTilesAtPrecision + 1)); valuesIndex = setValuesForFullyContainedTile(nextX, nextY, zTile, values, valuesIndex); } - } else if (GeoRelation.QUERY_CROSSES == relation) { + } else if (GeoRelation.QUERY_DISJOINT != relation) { if (zTile == precision) { values.resizeCell(getNewSize(valuesIndex, 1)); values.add(valuesIndex++, GeoTileUtils.longEncodeTiles(zTile, nextX, nextY)); diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/LatLonGeometryRelationVisitorTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/LatLonGeometryRelationVisitorTests.java index 3a0d0bcd0356..bc83b1efb068 100644 --- a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/LatLonGeometryRelationVisitorTests.java +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/LatLonGeometryRelationVisitorTests.java @@ -60,18 +60,28 @@ private void doTestShapes(Supplier supplier) throws Exception { CoordinateEncoder.GEO ); reader.visit(disjoint); + Component2DVisitor within = Component2DVisitor.getVisitor(component2D, ShapeField.QueryRelation.WITHIN, CoordinateEncoder.GEO); + reader.visit(within); if (relation == GeoRelation.QUERY_INSIDE) { assertThat(contains.matches(), equalTo(true)); assertThat(intersects.matches(), equalTo(true)); assertThat(disjoint.matches(), equalTo(false)); + assertThat(within.matches(), equalTo(false)); } else if (relation == GeoRelation.QUERY_CROSSES) { assertThat(contains.matches(), equalTo(false)); assertThat(intersects.matches(), equalTo(true)); assertThat(disjoint.matches(), equalTo(false)); + assertThat(within.matches(), equalTo(false)); + } else if (relation == GeoRelation.QUERY_CONTAINS) { + assertThat(contains.matches(), equalTo(false)); + assertThat(intersects.matches(), equalTo(true)); + assertThat(disjoint.matches(), equalTo(false)); + assertThat(within.matches(), equalTo(true)); } else { assertThat(contains.matches(), equalTo(false)); assertThat(intersects.matches(), equalTo(false)); assertThat(disjoint.matches(), equalTo(true)); + assertThat(within.matches(), equalTo(false)); } } } diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/Tile2DVisitorTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/Tile2DVisitorTests.java index 5d4685f69f88..959b91ffbc91 100644 --- a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/Tile2DVisitorTests.java +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/Tile2DVisitorTests.java @@ -33,7 +33,7 @@ import static org.elasticsearch.geo.GeometryTestUtils.randomMultiLine; import static org.elasticsearch.geo.GeometryTestUtils.randomMultiPolygon; import static org.elasticsearch.geo.GeometryTestUtils.randomPoint; -import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.in; public class Tile2DVisitorTests extends ESTestCase { @@ -45,10 +45,10 @@ public void testPacManPolygon() throws Exception { // test cell crossing poly Polygon pacMan = new Polygon(new LinearRing(py, px), Collections.emptyList()); GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(pacMan, TestCoordinateEncoder.INSTANCE); - assertRelation(GeoRelation.QUERY_CROSSES, reader, getExtentFromBox(2, -1, 11, 1)); - assertRelation(GeoRelation.QUERY_CROSSES, reader, getExtentFromBox(-12, -12, 12, 12)); - assertRelation(GeoRelation.QUERY_CROSSES, reader, getExtentFromBox(-2, -1, 2, 0)); - assertRelation(GeoRelation.QUERY_INSIDE, reader, getExtentFromBox(-5, -6, 2, -2)); + assertRelation(reader, getExtentFromBox(2, -1, 11, 1), GeoRelation.QUERY_CROSSES); + assertRelation(reader, getExtentFromBox(-12, -12, 12, 12), GeoRelation.QUERY_CONTAINS); + assertRelation(reader, getExtentFromBox(-2, -1, 2, 0), GeoRelation.QUERY_CROSSES); + assertRelation(reader, getExtentFromBox(-5, -6, 2, -2), GeoRelation.QUERY_INSIDE); } // adapted from org.apache.lucene.geo.TestPolygon2D#testMultiPolygon @@ -60,12 +60,12 @@ public void testPolygonWithHole() throws Exception { GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(polyWithHole, CoordinateEncoder.GEO); - assertRelation(GeoRelation.QUERY_DISJOINT, reader, getExtentFromBox(6, -6, 6, -6)); // in the hole - assertRelation(GeoRelation.QUERY_INSIDE, reader, getExtentFromBox(25, -25, 25, -25)); // on the mainland - assertRelation(GeoRelation.QUERY_DISJOINT, reader, getExtentFromBox(51, 51, 52, 52)); // outside of mainland - assertRelation(GeoRelation.QUERY_CROSSES, reader, getExtentFromBox(-60, -60, 60, 60)); // enclosing us completely - assertRelation(GeoRelation.QUERY_CROSSES, reader, getExtentFromBox(49, 49, 51, 51)); // overlapping the mainland - assertRelation(GeoRelation.QUERY_CROSSES, reader, getExtentFromBox(9, 9, 11, 11)); // overlapping the hole + assertRelation(reader, getExtentFromBox(6, -6, 6, -6), GeoRelation.QUERY_DISJOINT); // in the hole + assertRelation(reader, getExtentFromBox(25, -25, 25, -25), GeoRelation.QUERY_INSIDE); // on the mainland + assertRelation(reader, getExtentFromBox(51, 51, 52, 52), GeoRelation.QUERY_DISJOINT); // outside of mainland + assertRelation(reader, getExtentFromBox(-60, -60, 60, 60), GeoRelation.QUERY_CONTAINS); // enclosing us completely + assertRelation(reader, getExtentFromBox(49, 49, 51, 51), GeoRelation.QUERY_CROSSES); // overlapping the mainland + assertRelation(reader, getExtentFromBox(9, 9, 11, 11), GeoRelation.QUERY_CROSSES); // overlapping the hole } public void testCombPolygon() throws Exception { @@ -78,9 +78,9 @@ public void testCombPolygon() throws Exception { Polygon polyWithHole = new Polygon(new LinearRing(px, py), Collections.singletonList(new LinearRing(hx, hy))); GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(polyWithHole, CoordinateEncoder.GEO); // test cell crossing poly - assertRelation(GeoRelation.QUERY_INSIDE, reader, getExtentFromBox(5, 10, 5, 10)); - assertRelation(GeoRelation.QUERY_DISJOINT, reader, getExtentFromBox(15, 10, 15, 10)); - assertRelation(GeoRelation.QUERY_DISJOINT, reader, getExtentFromBox(25, 10, 25, 10)); + assertRelation(reader, getExtentFromBox(5, 10, 5, 10), GeoRelation.QUERY_INSIDE); + assertRelation(reader, getExtentFromBox(15, 10, 15, 10), GeoRelation.QUERY_DISJOINT); + assertRelation(reader, getExtentFromBox(25, 10, 25, 10), GeoRelation.QUERY_DISJOINT); } public void testPacManClosedLineString() throws Exception { @@ -90,10 +90,10 @@ public void testPacManClosedLineString() throws Exception { // test cell crossing poly GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(new Line(px, py), CoordinateEncoder.GEO); - assertRelation(GeoRelation.QUERY_CROSSES, reader, getExtentFromBox(2, -1, 11, 1)); - assertRelation(GeoRelation.QUERY_CROSSES, reader, getExtentFromBox(-12, -12, 12, 12)); - assertRelation(GeoRelation.QUERY_CROSSES, reader, getExtentFromBox(-2, -1, 2, 0)); - assertRelation(GeoRelation.QUERY_DISJOINT, reader, getExtentFromBox(-5, -6, 2, -2)); + assertRelation(reader, getExtentFromBox(2, -1, 11, 1), GeoRelation.QUERY_CROSSES); + assertRelation(reader, getExtentFromBox(-12, -12, 12, 12), GeoRelation.QUERY_CONTAINS); + assertRelation(reader, getExtentFromBox(-2, -1, 2, 0), GeoRelation.QUERY_CROSSES); + assertRelation(reader, getExtentFromBox(-5, -6, 2, -2), GeoRelation.QUERY_DISJOINT); } public void testPacManLineString() throws Exception { @@ -103,10 +103,10 @@ public void testPacManLineString() throws Exception { // test cell crossing poly GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(new Line(px, py), CoordinateEncoder.GEO); - assertRelation(GeoRelation.QUERY_CROSSES, reader, getExtentFromBox(2, -1, 11, 1)); - assertRelation(GeoRelation.QUERY_CROSSES, reader, getExtentFromBox(-12, -12, 12, 12)); - assertRelation(GeoRelation.QUERY_CROSSES, reader, getExtentFromBox(-2, -1, 2, 0)); - assertRelation(GeoRelation.QUERY_DISJOINT, reader, getExtentFromBox(-5, -6, 2, -2)); + assertRelation(reader, getExtentFromBox(2, -1, 11, 1), GeoRelation.QUERY_CROSSES); + assertRelation(reader, getExtentFromBox(-12, -12, 12, 12), GeoRelation.QUERY_CONTAINS); + assertRelation(reader, getExtentFromBox(-2, -1, 2, 0), GeoRelation.QUERY_CROSSES); + assertRelation(reader, getExtentFromBox(-5, -6, 2, -2), GeoRelation.QUERY_DISJOINT); } public void testPacManPoints() throws Exception { @@ -132,7 +132,7 @@ public void testPacManPoints() throws Exception { // test cell crossing poly GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(new MultiPoint(points), CoordinateEncoder.GEO); - assertRelation(GeoRelation.QUERY_CROSSES, reader, getExtentFromBox(xMin, yMin, xMax, yMax)); + assertRelation(reader, getExtentFromBox(xMin, yMin, xMax, yMax), GeoRelation.QUERY_CROSSES); } public void testRandomMultiLineIntersections() throws IOException { @@ -148,23 +148,24 @@ public void testRandomMultiLineIntersections() throws IOException { && lineExtent.minY() != Integer.MIN_VALUE && lineExtent.maxY() != Integer.MAX_VALUE) { assertRelation( - GeoRelation.QUERY_CROSSES, reader, - Extent.fromPoints(lineExtent.minX() - 1, lineExtent.minY() - 1, lineExtent.maxX() + 1, lineExtent.maxY() + 1) + Extent.fromPoints(lineExtent.minX() - 1, lineExtent.minY() - 1, lineExtent.maxX() + 1, lineExtent.maxY() + 1), + GeoRelation.QUERY_CROSSES, + GeoRelation.QUERY_CONTAINS ); } } // extent that fully encloses the MultiLine - assertRelation(GeoRelation.QUERY_CROSSES, reader, reader.getExtent()); + assertRelation(reader, reader.getExtent(), GeoRelation.QUERY_CONTAINS); if (readerExtent.minX() != Integer.MIN_VALUE && readerExtent.maxX() != Integer.MAX_VALUE && readerExtent.minY() != Integer.MIN_VALUE && readerExtent.maxY() != Integer.MAX_VALUE) { assertRelation( - GeoRelation.QUERY_CROSSES, reader, - Extent.fromPoints(readerExtent.minX() - 1, readerExtent.minY() - 1, readerExtent.maxX() + 1, readerExtent.maxY() + 1) + Extent.fromPoints(readerExtent.minX() - 1, readerExtent.minY() - 1, readerExtent.maxX() + 1, readerExtent.maxY() + 1), + GeoRelation.QUERY_CONTAINS ); } @@ -211,12 +212,11 @@ private static Extent getExtentFromBox(double bottomLeftX, double bottomLeftY, d } private boolean intersects(Geometry g, Point p, double extentSize) throws IOException { - Extent bufferBounds = bufferedExtentFromGeoPoint(p.getX(), p.getY(), extentSize); Tile2DVisitor tile2DVisitor = new Tile2DVisitor(); tile2DVisitor.reset(bufferBounds.minX(), bufferBounds.minY(), bufferBounds.maxX(), bufferBounds.maxY()); GeoTestUtils.geometryDocValueReader(g, CoordinateEncoder.GEO).visit(tile2DVisitor); - return tile2DVisitor.relation() == GeoRelation.QUERY_CROSSES || tile2DVisitor.relation() == GeoRelation.QUERY_INSIDE; + return tile2DVisitor.relation() != GeoRelation.QUERY_DISJOINT; } /** @@ -281,10 +281,10 @@ public R visit(Rectangle rectangle) throws E { }); } - static void assertRelation(GeoRelation expectedRelation, GeometryDocValueReader reader, Extent extent) throws IOException { + static void assertRelation(GeometryDocValueReader reader, Extent extent, GeoRelation... expectedRelation) throws IOException { Tile2DVisitor tile2DVisitor = new Tile2DVisitor(); tile2DVisitor.reset(extent.minX(), extent.minY(), extent.maxX(), extent.maxY()); reader.visit(tile2DVisitor); - assertThat(expectedRelation, equalTo(tile2DVisitor.relation())); + assertThat(tile2DVisitor.relation(), in(expectedRelation)); } } From 1f1443a8385975915309d5246b70deef390e8e31 Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Mon, 5 Dec 2022 11:40:38 +0100 Subject: [PATCH 146/919] Convert shardsWithState to return stream (#92063) --- .../discovery/ClusterDisruptionIT.java | 2 +- .../cluster/routing/RoutingNode.java | 49 ++++++++----------- .../cluster/routing/RoutingNodeTests.java | 14 +++--- .../allocation/AddIncrementallyTests.java | 19 +++---- .../allocation/AllocationCommandsTests.java | 11 +++-- .../allocation/BalanceConfigurationTests.java | 4 +- .../allocation/BalancedSingleShardTests.java | 4 +- .../routing/allocation/IndexBalanceTests.java | 36 +++++++------- .../PrimaryElectionRoutingTests.java | 3 +- .../RoutingNodesIntegrityTests.java | 24 ++++----- .../SingleShardNoReplicasRoutingTests.java | 4 +- .../cluster/ESAllocationTestCase.java | 2 +- .../cluster/routing/RoutingNodesHelper.java | 2 +- .../TransportGetShutdownStatusAction.java | 1 - .../watcher/WatcherIndexingListener.java | 3 +- .../watcher/WatcherLifeCycleService.java | 2 +- .../xpack/watcher/WatcherService.java | 2 +- .../watcher/WatcherIndexingListenerTests.java | 8 +-- 18 files changed, 92 insertions(+), 98 deletions(-) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/discovery/ClusterDisruptionIT.java b/server/src/internalClusterTest/java/org/elasticsearch/discovery/ClusterDisruptionIT.java index 6f8f2eb11a80..4a823ed997ea 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/discovery/ClusterDisruptionIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/discovery/ClusterDisruptionIT.java @@ -339,7 +339,7 @@ public void testSendingShardFailure() throws Exception { // fail a random shard ShardRouting failedShard = randomFrom( - clusterService().state().getRoutingNodes().node(nonMasterNodeId).shardsWithState(ShardRoutingState.STARTED) + clusterService().state().getRoutingNodes().node(nonMasterNodeId).shardsWithState(ShardRoutingState.STARTED).toList() ); ShardStateAction service = internalCluster().getInstance(ShardStateAction.class, nonMasterNode); CountDownLatch latch = new CountDownLatch(1); diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/RoutingNode.java b/server/src/main/java/org/elasticsearch/cluster/routing/RoutingNode.java index 399618de90e2..53671458e0cf 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/RoutingNode.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/RoutingNode.java @@ -20,15 +20,12 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Stream; -import static java.util.stream.Collectors.toCollection; - /** * A {@link RoutingNode} represents a cluster node associated with a single {@link DiscoveryNode} including all shards * that are hosted on that nodes. Each {@link RoutingNode} has a unique node id that can be used to identify the node. @@ -212,6 +209,20 @@ void remove(ShardRouting shard) { assert invariant(); } + private static final ShardRouting[] EMPTY_SHARD_ROUTING_ARRAY = new ShardRouting[0]; + + public ShardRouting[] initializing() { + return initializingShards.toArray(EMPTY_SHARD_ROUTING_ARRAY); + } + + public ShardRouting[] relocating() { + return relocatingShards.toArray(EMPTY_SHARD_ROUTING_ARRAY); + } + + public ShardRouting[] started() { + return startedShards.toArray(EMPTY_SHARD_ROUTING_ARRAY); + } + /** * Determine the number of shards with a specific state * @param state which should be counted @@ -226,22 +237,8 @@ public int numberOfShardsWithState(ShardRoutingState state) { * @param state state which should be listed * @return List of shards */ - public List shardsWithState(ShardRoutingState state) { - return new ArrayList<>(internalGetShardsWithState(state)); - } - - private static final ShardRouting[] EMPTY_SHARD_ROUTING_ARRAY = new ShardRouting[0]; - - public ShardRouting[] initializing() { - return initializingShards.toArray(EMPTY_SHARD_ROUTING_ARRAY); - } - - public ShardRouting[] relocating() { - return relocatingShards.toArray(EMPTY_SHARD_ROUTING_ARRAY); - } - - public ShardRouting[] started() { - return startedShards.toArray(EMPTY_SHARD_ROUTING_ARRAY); + public Stream shardsWithState(ShardRoutingState state) { + return internalGetShardsWithState(state).stream(); } /** @@ -250,18 +247,12 @@ public ShardRouting[] started() { * @param states set of states which should be listed * @return a list of shards */ - public List shardsWithState(String index, ShardRoutingState... states) { - return Stream.of(states).flatMap(state -> shardsWithState(index, state).stream()).collect(toCollection(ArrayList::new)); + public Stream shardsWithState(String index, ShardRoutingState... states) { + return Stream.of(states).flatMap(state -> shardsWithState(index, state)); } - public List shardsWithState(String index, ShardRoutingState state) { - var shards = new ArrayList(); - for (ShardRouting shardEntry : internalGetShardsWithState(state)) { - if (shardEntry.getIndexName().equals(index)) { - shards.add(shardEntry); - } - } - return shards; + public Stream shardsWithState(String index, ShardRoutingState state) { + return shardsWithState(state).filter(shardRouting -> Objects.equals(shardRouting.getIndexName(), index)); } private LinkedHashSet internalGetShardsWithState(ShardRoutingState state) { diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/RoutingNodeTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/RoutingNodeTests.java index c40f95f384b2..15bd91d9a561 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/RoutingNodeTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/RoutingNodeTests.java @@ -102,16 +102,16 @@ public void testNumberOfShardsWithState() { } public void testShardsWithState() { - assertThat(routingNode.shardsWithState(ShardRoutingState.STARTED).size(), equalTo(1)); - assertThat(routingNode.shardsWithState(ShardRoutingState.RELOCATING).size(), equalTo(1)); - assertThat(routingNode.shardsWithState(ShardRoutingState.INITIALIZING).size(), equalTo(1)); + assertThat(routingNode.shardsWithState(ShardRoutingState.STARTED).count(), equalTo(1L)); + assertThat(routingNode.shardsWithState(ShardRoutingState.RELOCATING).count(), equalTo(1L)); + assertThat(routingNode.shardsWithState(ShardRoutingState.INITIALIZING).count(), equalTo(1L)); } public void testShardsWithStateInIndex() { - assertThat(routingNode.shardsWithState("test", ShardRoutingState.INITIALIZING, ShardRoutingState.STARTED).size(), equalTo(2)); - assertThat(routingNode.shardsWithState("test", ShardRoutingState.STARTED).size(), equalTo(1)); - assertThat(routingNode.shardsWithState("test", ShardRoutingState.RELOCATING).size(), equalTo(1)); - assertThat(routingNode.shardsWithState("test", ShardRoutingState.INITIALIZING).size(), equalTo(1)); + assertThat(routingNode.shardsWithState("test", ShardRoutingState.INITIALIZING, ShardRoutingState.STARTED).count(), equalTo(2L)); + assertThat(routingNode.shardsWithState("test", ShardRoutingState.STARTED).count(), equalTo(1L)); + assertThat(routingNode.shardsWithState("test", ShardRoutingState.RELOCATING).count(), equalTo(1L)); + assertThat(routingNode.shardsWithState("test", ShardRoutingState.INITIALIZING).count(), equalTo(1L)); } public void testNumberOfOwningShards() { diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/AddIncrementallyTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/AddIncrementallyTests.java index c78c78e7b5fd..76f0d141a40a 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/AddIncrementallyTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/AddIncrementallyTests.java @@ -25,7 +25,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.CollectionUtils; import org.hamcrest.Matcher; -import org.hamcrest.Matchers; import java.util.ArrayList; import java.util.Collections; @@ -33,6 +32,8 @@ import static org.elasticsearch.cluster.routing.ShardRoutingState.INITIALIZING; import static org.elasticsearch.cluster.routing.ShardRoutingState.STARTED; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.Matchers.not; public class AddIncrementallyTests extends ESAllocationTestCase { @@ -58,7 +59,7 @@ public void testAddNodesAndIndices() { clusterState = addNodes(clusterState, service, 1, nodeOffset++); assertNumIndexShardsPerNode(clusterState, equalTo(2)); clusterState = addNodes(clusterState, service, 1, nodeOffset++); - assertNumIndexShardsPerNode(clusterState, Matchers.lessThanOrEqualTo(2)); + assertNumIndexShardsPerNode(clusterState, lessThanOrEqualTo(2)); assertAtLeastOneIndexShardPerNode(clusterState); clusterState = removeNodes(clusterState, service, 1); assertNumIndexShardsPerNode(clusterState, equalTo(2)); @@ -66,20 +67,20 @@ public void testAddNodesAndIndices() { clusterState = addIndex(clusterState, service, 3, 2, 3); assertThat(clusterState.getRoutingNodes().unassigned().size(), equalTo(2)); assertNumIndexShardsPerNode(clusterState, "test3", equalTo(2)); - assertNumIndexShardsPerNode(clusterState, Matchers.lessThanOrEqualTo(2)); + assertNumIndexShardsPerNode(clusterState, lessThanOrEqualTo(2)); clusterState = addIndex(clusterState, service, 4, 2, 3); assertThat(clusterState.getRoutingNodes().unassigned().size(), equalTo(4)); assertNumIndexShardsPerNode(clusterState, "test4", equalTo(2)); - assertNumIndexShardsPerNode(clusterState, Matchers.lessThanOrEqualTo(2)); + assertNumIndexShardsPerNode(clusterState, lessThanOrEqualTo(2)); clusterState = addNodes(clusterState, service, 1, nodeOffset++); - assertNumIndexShardsPerNode(clusterState, Matchers.lessThanOrEqualTo(2)); + assertNumIndexShardsPerNode(clusterState, lessThanOrEqualTo(2)); assertThat(clusterState.getRoutingNodes().unassigned().size(), equalTo(0)); clusterState = removeNodes(clusterState, service, 1); assertThat(clusterState.getRoutingNodes().unassigned().size(), equalTo(4)); - assertNumIndexShardsPerNode(clusterState, Matchers.lessThanOrEqualTo(2)); + assertNumIndexShardsPerNode(clusterState, lessThanOrEqualTo(2)); clusterState = addNodes(clusterState, service, 1, nodeOffset++); - assertNumIndexShardsPerNode(clusterState, Matchers.lessThanOrEqualTo(2)); + assertNumIndexShardsPerNode(clusterState, lessThanOrEqualTo(2)); assertThat(clusterState.getRoutingNodes().unassigned().size(), equalTo(0)); logger.debug("ClusterState: {}", clusterState.getRoutingNodes()); } @@ -220,7 +221,7 @@ private void assertNumIndexShardsPerNode(ClusterState state, Matcher ma private void assertNumIndexShardsPerNode(ClusterState state, String index, Matcher matcher) { for (RoutingNode node : state.getRoutingNodes()) { - assertThat(node.shardsWithState(index, STARTED).size(), matcher); + assertThat(Math.toIntExact(node.shardsWithState(index, STARTED).count()), matcher); } } @@ -228,7 +229,7 @@ private void assertAtLeastOneIndexShardPerNode(ClusterState state) { for (String index : state.routingTable().indicesRouting().keySet()) { for (RoutingNode node : state.getRoutingNodes()) { - assertThat(node.shardsWithState(index, STARTED).size(), Matchers.greaterThanOrEqualTo(1)); + assertThat(node.shardsWithState(index, STARTED).count(), greaterThanOrEqualTo(1L)); } } diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/AllocationCommandsTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/AllocationCommandsTests.java index ebac38764fc8..ea720f635872 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/AllocationCommandsTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/AllocationCommandsTests.java @@ -622,7 +622,7 @@ public void testCancelCommand() { assertThat(newState, not(equalTo(clusterState))); clusterState = newState; assertThat(clusterState.getRoutingNodes().node("node1").size(), equalTo(0)); - assertThat(clusterState.getRoutingNodes().node("node2").shardsWithState(STARTED).iterator().next().primary(), equalTo(true)); + assertThat(clusterState.getRoutingNodes().node("node2").shardsWithState(STARTED).findFirst().get().primary(), equalTo(true)); assertThat(clusterState.getRoutingNodes().node("node3").size(), equalTo(0)); } else { logger.info("--> cancel the move of the replica shard"); @@ -669,7 +669,10 @@ public void testCancelCommand() { assertThat(clusterState.getRoutingNodes().node("node2").size(), equalTo(0)); assertThat(clusterState.getRoutingNodes().node("node3").size(), equalTo(1)); assertThat(clusterState.getRoutingNodes().node("node3").numberOfShardsWithState(INITIALIZING), equalTo(1)); - assertThat(clusterState.getRoutingNodes().node("node3").shardsWithState(INITIALIZING).get(0).relocatingNodeId(), nullValue()); + assertThat( + clusterState.getRoutingNodes().node("node3").shardsWithState(INITIALIZING).findFirst().get().relocatingNodeId(), + nullValue() + ); logger.info("--> start the former target replica shard"); clusterState = startInitializingShardsAndReroute(allocation, clusterState); @@ -689,7 +692,7 @@ public void testCancelCommand() { ).clusterState(); assertThat(newState, not(equalTo(clusterState))); clusterState = newState; - assertThat(clusterState.getRoutingNodes().node("node3").shardsWithState(STARTED).iterator().next().primary(), equalTo(true)); + assertThat(clusterState.getRoutingNodes().node("node3").shardsWithState(STARTED).findFirst().get().primary(), equalTo(true)); assertThat(clusterState.getRoutingNodes().node("node1").size(), equalTo(0)); assertThat(clusterState.getRoutingNodes().node("node2").size(), equalTo(0)); } @@ -1058,7 +1061,7 @@ public void testConflictingCommandsInSingleRequest() { clusterState = startInitializingShardsAndReroute(allocation, clusterState); final ClusterState updatedClusterState = clusterState; - assertThat(updatedClusterState.getRoutingNodes().node(node1).shardsWithState(STARTED).size(), equalTo(1)); + assertThat(updatedClusterState.getRoutingNodes().node(node1).numberOfShardsWithState(STARTED), equalTo(1)); logger.info("--> subsequent replica allocation fails as all configured replicas have been allocated"); assertThat(expectThrows(IllegalArgumentException.class, () -> { diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/BalanceConfigurationTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/BalanceConfigurationTests.java index fb0c2f0b2fd3..d4c0b2694c2a 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/BalanceConfigurationTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/BalanceConfigurationTests.java @@ -279,8 +279,8 @@ private void assertIndexBalance( for (String index : routingTable.indicesRouting().keySet()) { for (RoutingNode node : nodes) { - assertThat(node.shardsWithState(index, STARTED).size(), greaterThanOrEqualTo(minAvgNumberOfShards)); - assertThat(node.shardsWithState(index, STARTED).size(), lessThanOrEqualTo(maxAvgNumberOfShards)); + assertThat(Math.toIntExact(node.shardsWithState(index, STARTED).count()), greaterThanOrEqualTo(minAvgNumberOfShards)); + assertThat(Math.toIntExact(node.shardsWithState(index, STARTED).count()), lessThanOrEqualTo(maxAvgNumberOfShards)); } } } diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/BalancedSingleShardTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/BalancedSingleShardTests.java index e1531a38efcb..7efac54bf3b8 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/BalancedSingleShardTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/BalancedSingleShardTests.java @@ -221,7 +221,7 @@ public Decision canRebalance(ShardRouting shardRouting, RoutingAllocation alloca allocator.allocate(routingAllocation); ShardRouting shardToRebalance = null; for (RoutingNode routingNode : routingAllocation.routingNodes()) { - List relocatingShards = routingNode.shardsWithState(ShardRoutingState.RELOCATING); + List relocatingShards = routingNode.shardsWithState(ShardRoutingState.RELOCATING).toList(); if (relocatingShards.size() > 0) { shardToRebalance = randomFrom(relocatingShards); break; @@ -276,7 +276,7 @@ public void testNodeDecisionsRanking() { if (node.numberOfShardsWithState(ShardRoutingState.STARTED) == 2) { nodesWithTwoShards.add(node.nodeId()); if (shardToRebalance == null) { - shardToRebalance = node.shardsWithState(ShardRoutingState.STARTED).get(0); + shardToRebalance = node.shardsWithState(ShardRoutingState.STARTED).findFirst().get(); } } else { assertEquals(3, node.numberOfShardsWithState(ShardRoutingState.STARTED)); diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/IndexBalanceTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/IndexBalanceTests.java index b814aba17c69..efbcd04a5335 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/IndexBalanceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/IndexBalanceTests.java @@ -142,13 +142,13 @@ public void testBalanceAllNodesStarted() { assertThat(routingNodes.node("node2").numberOfShardsWithState(STARTED), equalTo(4)); assertThat(routingNodes.node("node3").numberOfShardsWithState(STARTED), equalTo(4)); - assertThat(routingNodes.node("node1").shardsWithState("test", STARTED).size(), equalTo(2)); - assertThat(routingNodes.node("node2").shardsWithState("test", STARTED).size(), equalTo(2)); - assertThat(routingNodes.node("node3").shardsWithState("test", STARTED).size(), equalTo(2)); + assertThat(routingNodes.node("node1").shardsWithState("test", STARTED).count(), equalTo(2L)); + assertThat(routingNodes.node("node2").shardsWithState("test", STARTED).count(), equalTo(2L)); + assertThat(routingNodes.node("node3").shardsWithState("test", STARTED).count(), equalTo(2L)); - assertThat(routingNodes.node("node1").shardsWithState("test1", STARTED).size(), equalTo(2)); - assertThat(routingNodes.node("node2").shardsWithState("test1", STARTED).size(), equalTo(2)); - assertThat(routingNodes.node("node3").shardsWithState("test1", STARTED).size(), equalTo(2)); + assertThat(routingNodes.node("node1").shardsWithState("test1", STARTED).count(), equalTo(2L)); + assertThat(routingNodes.node("node2").shardsWithState("test1", STARTED).count(), equalTo(2L)); + assertThat(routingNodes.node("node3").shardsWithState("test1", STARTED).count(), equalTo(2L)); } public void testBalanceIncrementallyStartNodes() { @@ -265,13 +265,13 @@ public void testBalanceIncrementallyStartNodes() { assertThat(clusterState.getRoutingNodes().node("node2").numberOfShardsWithState(STARTED), equalTo(4)); assertThat(clusterState.getRoutingNodes().node("node3").numberOfShardsWithState(STARTED), equalTo(4)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState("test", STARTED).size(), equalTo(2)); - assertThat(clusterState.getRoutingNodes().node("node2").shardsWithState("test", STARTED).size(), equalTo(2)); - assertThat(clusterState.getRoutingNodes().node("node3").shardsWithState("test", STARTED).size(), equalTo(2)); + assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState("test", STARTED).count(), equalTo(2L)); + assertThat(clusterState.getRoutingNodes().node("node2").shardsWithState("test", STARTED).count(), equalTo(2L)); + assertThat(clusterState.getRoutingNodes().node("node3").shardsWithState("test", STARTED).count(), equalTo(2L)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState("test1", STARTED).size(), equalTo(2)); - assertThat(clusterState.getRoutingNodes().node("node2").shardsWithState("test1", STARTED).size(), equalTo(2)); - assertThat(clusterState.getRoutingNodes().node("node3").shardsWithState("test1", STARTED).size(), equalTo(2)); + assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState("test1", STARTED).count(), equalTo(2L)); + assertThat(clusterState.getRoutingNodes().node("node2").shardsWithState("test1", STARTED).count(), equalTo(2L)); + assertThat(clusterState.getRoutingNodes().node("node3").shardsWithState("test1", STARTED).count(), equalTo(2L)); } public void testBalanceAllNodesStartedAddIndex() { @@ -360,9 +360,9 @@ public void testBalanceAllNodesStartedAddIndex() { assertThat(routingNodes.node("node2").numberOfShardsWithState(STARTED), equalTo(2)); assertThat(routingNodes.node("node3").numberOfShardsWithState(STARTED), equalTo(2)); - assertThat(routingNodes.node("node1").shardsWithState("test", STARTED).size(), equalTo(2)); - assertThat(routingNodes.node("node2").shardsWithState("test", STARTED).size(), equalTo(2)); - assertThat(routingNodes.node("node3").shardsWithState("test", STARTED).size(), equalTo(2)); + assertThat(routingNodes.node("node1").shardsWithState("test", STARTED).count(), equalTo(2L)); + assertThat(routingNodes.node("node2").shardsWithState("test", STARTED).count(), equalTo(2L)); + assertThat(routingNodes.node("node3").shardsWithState("test", STARTED).count(), equalTo(2L)); logger.info("Add new index 3 shards 1 replica"); @@ -432,8 +432,8 @@ public void testBalanceAllNodesStartedAddIndex() { assertThat(routingNodes.node("node2").numberOfShardsWithState(STARTED), equalTo(4)); assertThat(routingNodes.node("node3").numberOfShardsWithState(STARTED), equalTo(4)); - assertThat(routingNodes.node("node1").shardsWithState("test1", STARTED).size(), equalTo(2)); - assertThat(routingNodes.node("node2").shardsWithState("test1", STARTED).size(), equalTo(2)); - assertThat(routingNodes.node("node3").shardsWithState("test1", STARTED).size(), equalTo(2)); + assertThat(routingNodes.node("node1").shardsWithState("test1", STARTED).count(), equalTo(2L)); + assertThat(routingNodes.node("node2").shardsWithState("test1", STARTED).count(), equalTo(2L)); + assertThat(routingNodes.node("node3").shardsWithState("test1", STARTED).count(), equalTo(2L)); } } diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/PrimaryElectionRoutingTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/PrimaryElectionRoutingTests.java index 2dbcf8f62cab..41f3550da4c5 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/PrimaryElectionRoutingTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/PrimaryElectionRoutingTests.java @@ -126,8 +126,7 @@ public void testRemovingInitializingReplicasIfPrimariesFails() { assertThat(shardsWithState(routingNodes, STARTED).size(), equalTo(1)); assertThat(shardsWithState(routingNodes, INITIALIZING).size(), equalTo(0)); assertThat(shardsWithState(routingNodes, UNASSIGNED).size(), equalTo(3)); // 2 replicas and one primary - assertThat(routingNodes.node(nodeIdRemaining).shardsWithState(STARTED).get(0).primary(), equalTo(true)); + assertThat(routingNodes.node(nodeIdRemaining).shardsWithState(STARTED).findFirst().get().primary(), equalTo(true)); assertThat(clusterState.metadata().index("test").primaryTerm(0), equalTo(2L)); - } } diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/RoutingNodesIntegrityTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/RoutingNodesIntegrityTests.java index b6100e1844a3..d94016b3aad7 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/RoutingNodesIntegrityTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/RoutingNodesIntegrityTests.java @@ -168,13 +168,13 @@ public void testBalanceIncrementallyStartNodes() { assertThat(clusterState.getRoutingNodes().node("node2").numberOfShardsWithState(STARTED), equalTo(4)); assertThat(clusterState.getRoutingNodes().node("node3").numberOfShardsWithState(STARTED), equalTo(4)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState("test", STARTED).size(), equalTo(2)); - assertThat(clusterState.getRoutingNodes().node("node2").shardsWithState("test", STARTED).size(), equalTo(2)); - assertThat(clusterState.getRoutingNodes().node("node3").shardsWithState("test", STARTED).size(), equalTo(2)); + assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState("test", STARTED).count(), equalTo(2L)); + assertThat(clusterState.getRoutingNodes().node("node2").shardsWithState("test", STARTED).count(), equalTo(2L)); + assertThat(clusterState.getRoutingNodes().node("node3").shardsWithState("test", STARTED).count(), equalTo(2L)); - assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState("test1", STARTED).size(), equalTo(2)); - assertThat(clusterState.getRoutingNodes().node("node2").shardsWithState("test1", STARTED).size(), equalTo(2)); - assertThat(clusterState.getRoutingNodes().node("node3").shardsWithState("test1", STARTED).size(), equalTo(2)); + assertThat(clusterState.getRoutingNodes().node("node1").shardsWithState("test1", STARTED).count(), equalTo(2L)); + assertThat(clusterState.getRoutingNodes().node("node2").shardsWithState("test1", STARTED).count(), equalTo(2L)); + assertThat(clusterState.getRoutingNodes().node("node3").shardsWithState("test1", STARTED).count(), equalTo(2L)); } public void testBalanceAllNodesStartedAddIndex() { @@ -257,9 +257,9 @@ public void testBalanceAllNodesStartedAddIndex() { assertThat(routingNodes.node("node2").numberOfShardsWithState(STARTED), equalTo(2)); assertThat(routingNodes.node("node3").numberOfShardsWithState(STARTED), equalTo(2)); - assertThat(routingNodes.node("node1").shardsWithState("test", STARTED).size(), equalTo(2)); - assertThat(routingNodes.node("node2").shardsWithState("test", STARTED).size(), equalTo(2)); - assertThat(routingNodes.node("node3").shardsWithState("test", STARTED).size(), equalTo(2)); + assertThat(routingNodes.node("node1").shardsWithState("test", STARTED).count(), equalTo(2L)); + assertThat(routingNodes.node("node2").shardsWithState("test", STARTED).count(), equalTo(2L)); + assertThat(routingNodes.node("node3").shardsWithState("test", STARTED).count(), equalTo(2L)); logger.info("Add new index 3 shards 1 replica"); @@ -318,9 +318,9 @@ public void testBalanceAllNodesStartedAddIndex() { assertThat(routingNodes.node("node2").numberOfShardsWithState(STARTED), equalTo(4)); assertThat(routingNodes.node("node3").numberOfShardsWithState(STARTED), equalTo(4)); - assertThat(routingNodes.node("node1").shardsWithState("test1", STARTED).size(), equalTo(2)); - assertThat(routingNodes.node("node2").shardsWithState("test1", STARTED).size(), equalTo(2)); - assertThat(routingNodes.node("node3").shardsWithState("test1", STARTED).size(), equalTo(2)); + assertThat(routingNodes.node("node1").shardsWithState("test1", STARTED).count(), equalTo(2L)); + assertThat(routingNodes.node("node2").shardsWithState("test1", STARTED).count(), equalTo(2L)); + assertThat(routingNodes.node("node3").shardsWithState("test1", STARTED).count(), equalTo(2L)); logger.info("kill one node"); IndexShardRoutingTable indexShardRoutingTable = clusterState.routingTable().index("test").shard(0); diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/SingleShardNoReplicasRoutingTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/SingleShardNoReplicasRoutingTests.java index d646ef7750c6..cb100b5768e5 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/SingleShardNoReplicasRoutingTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/SingleShardNoReplicasRoutingTests.java @@ -182,7 +182,9 @@ public void testSingleIndexShardFailed() { RoutingNodes routingNodes = clusterState.getRoutingNodes(); newState = strategy.applyFailedShards( clusterState, - List.of(new FailedShard(routingNodes.node("node1").shardsWithState(INITIALIZING).get(0), null, null, randomBoolean())), + List.of( + new FailedShard(routingNodes.node("node1").shardsWithState(INITIALIZING).findFirst().get(), null, null, randomBoolean()) + ), List.of() ); assertThat(newState, not(equalTo(clusterState))); diff --git a/test/framework/src/main/java/org/elasticsearch/cluster/ESAllocationTestCase.java b/test/framework/src/main/java/org/elasticsearch/cluster/ESAllocationTestCase.java index 38ac3dfb6422..deab959aa17a 100644 --- a/test/framework/src/main/java/org/elasticsearch/cluster/ESAllocationTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/cluster/ESAllocationTestCase.java @@ -276,7 +276,7 @@ public static ClusterState startInitializingShardsAndReroute( ClusterState clusterState, RoutingNode routingNode ) { - return startShardsAndReroute(allocationService, clusterState, routingNode.shardsWithState(INITIALIZING)); + return startShardsAndReroute(allocationService, clusterState, routingNode.shardsWithState(INITIALIZING).toList()); } /** diff --git a/test/framework/src/main/java/org/elasticsearch/cluster/routing/RoutingNodesHelper.java b/test/framework/src/main/java/org/elasticsearch/cluster/routing/RoutingNodesHelper.java index fc123429cea7..4ed514fa1670 100644 --- a/test/framework/src/main/java/org/elasticsearch/cluster/routing/RoutingNodesHelper.java +++ b/test/framework/src/main/java/org/elasticsearch/cluster/routing/RoutingNodesHelper.java @@ -24,7 +24,7 @@ private RoutingNodesHelper() {} public static List shardsWithState(RoutingNodes routingNodes, ShardRoutingState state) { return state == ShardRoutingState.UNASSIGNED ? iterableAsArrayList(routingNodes.unassigned()) - : routingNodes.stream().flatMap(routingNode -> routingNode.shardsWithState(state).stream()).toList(); + : routingNodes.stream().flatMap(routingNode -> routingNode.shardsWithState(state)).toList(); } public static List shardsWithState(RoutingNodes routingNodes, String index, ShardRoutingState states) { diff --git a/x-pack/plugin/shutdown/src/main/java/org/elasticsearch/xpack/shutdown/TransportGetShutdownStatusAction.java b/x-pack/plugin/shutdown/src/main/java/org/elasticsearch/xpack/shutdown/TransportGetShutdownStatusAction.java index 806bf22d2688..faae39f94ce6 100644 --- a/x-pack/plugin/shutdown/src/main/java/org/elasticsearch/xpack/shutdown/TransportGetShutdownStatusAction.java +++ b/x-pack/plugin/shutdown/src/main/java/org/elasticsearch/xpack/shutdown/TransportGetShutdownStatusAction.java @@ -267,7 +267,6 @@ static ShutdownShardMigrationStatus shardMigrationStatus( Optional> unmovableShard = currentState.getRoutingNodes() .node(nodeId) .shardsWithState(ShardRoutingState.STARTED) - .stream() .map(shardRouting -> new Tuple<>(shardRouting, allocationService.explainShardAllocation(shardRouting, allocation))) // Given that we're checking the status of a node that's shutting down, no shards should be allowed to remain .filter(pair -> { diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherIndexingListener.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherIndexingListener.java index 26ee1a85a271..ca91d51a2d12 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherIndexingListener.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherIndexingListener.java @@ -234,7 +234,7 @@ private void checkWatchIndexHasChanged(IndexMetadata metadata, ClusterChangedEve RoutingNode routingNode = state.getRoutingNodes().node(localNodeId); // no local shards, exit early - List localShardRouting = routingNode.shardsWithState(watchIndex, STARTED, RELOCATING); + List localShardRouting = routingNode.shardsWithState(watchIndex, STARTED, RELOCATING).toList(); if (localShardRouting.isEmpty()) { configuration = INACTIVE; } else { @@ -281,7 +281,6 @@ private boolean hasShardAllocationIdChanged(String watchIndex, ClusterState stat Set clusterStateLocalShardIds = state.getRoutingNodes() .node(localNodeId) .shardsWithState(watchIndex, STARTED, RELOCATING) - .stream() .map(ShardRouting::shardId) .collect(Collectors.toSet()); Set configuredLocalShardIds = new HashSet<>(configuration.localShards.keySet()); diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleService.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleService.java index b5f29085fd5f..73c3a4c093fd 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleService.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleService.java @@ -144,7 +144,7 @@ public void clusterChanged(ClusterChangedEvent event) { } String watchIndex = watcherIndexMetadata.getIndex().getName(); - List localShards = routingNode.shardsWithState(watchIndex, RELOCATING, STARTED); + List localShards = routingNode.shardsWithState(watchIndex, RELOCATING, STARTED).toList(); // no local shards, empty out watcher and dont waste resources! if (localShards.isEmpty()) { pauseExecution("no local watcher shards found"); diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherService.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherService.java index 347ca20f7a94..9a0fa93f0605 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherService.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherService.java @@ -323,7 +323,7 @@ private Collection loadWatches(ClusterState clusterState) { if (routingNode == null) { return Collections.emptyList(); } - List localShards = routingNode.shardsWithState(watchIndexName, RELOCATING, STARTED); + List localShards = routingNode.shardsWithState(watchIndexName, RELOCATING, STARTED).toList(); // find out all allocation ids List watchIndexShardRoutings = clusterState.getRoutingTable().allShards(watchIndexName); diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherIndexingListenerTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherIndexingListenerTests.java index 985a0948516e..7e71c9a8f94e 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherIndexingListenerTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherIndexingListenerTests.java @@ -56,6 +56,7 @@ import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; +import java.util.stream.Stream; import static java.util.Arrays.asList; import static org.elasticsearch.cluster.routing.ShardRoutingState.RELOCATING; @@ -330,14 +331,13 @@ public void testClusterChangedWatchAliasChanged() throws Exception { boolean emptyShards = randomBoolean(); if (emptyShards) { - when(routingNode.shardsWithState(eq(newActiveWatchIndex), any(ShardRoutingState[].class))).thenReturn(Collections.emptyList()); + when(routingNode.shardsWithState(eq(newActiveWatchIndex), any(ShardRoutingState[].class))).thenReturn(Stream.empty()); } else { Index index = new Index(newActiveWatchIndex, "uuid"); ShardId shardId = new ShardId(index, 0); ShardRouting shardRouting = TestShardRouting.newShardRouting(shardId, "node_1", true, STARTED); - List routing = Collections.singletonList(shardRouting); - when(routingNode.shardsWithState(eq(newActiveWatchIndex), eq(STARTED), eq(RELOCATING))).thenReturn(routing); - when(routingTable.allShards(eq(newActiveWatchIndex))).thenReturn(routing); + when(routingNode.shardsWithState(eq(newActiveWatchIndex), eq(STARTED), eq(RELOCATING))).thenReturn(Stream.of(shardRouting)); + when(routingTable.allShards(eq(newActiveWatchIndex))).thenReturn(List.of(shardRouting)); IndexRoutingTable indexRoutingTable = IndexRoutingTable.builder(index).addShard(shardRouting).build(); when(routingTable.index(newActiveWatchIndex)).thenReturn(indexRoutingTable); } From f3e9041764815ac0c7b903efe773030189077973 Mon Sep 17 00:00:00 2001 From: Iraklis Psaroudakis Date: Mon, 5 Dec 2022 13:19:54 +0200 Subject: [PATCH 147/919] Make a public createRepository() (#91927) Make a public createRepository() --- .../repositories/RepositoriesService.java | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java index dcfcf09cd1b0..2f28adcebc98 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java @@ -318,7 +318,7 @@ public void validateRepositoryCanBeCreated(final PutRepositoryRequest request) { final RepositoryMetadata newRepositoryMetadata = new RepositoryMetadata(request.name(), request.type(), request.settings()); // Trying to create the new repository on master to make sure it works - closeRepository(createRepository(newRepositoryMetadata, typesRegistry, RepositoriesService::throwRepositoryTypeDoesNotExists)); + closeRepository(createRepository(newRepositoryMetadata)); } private void submitUnbatchedTask(@SuppressWarnings("SameParameterValue") String source, ClusterStateUpdateTask task) { @@ -757,6 +757,25 @@ private static Repository createRepository( } } + /** + * Creates a repository holder. + * + *

WARNING: This method is intended for expert only usage mainly in plugins/modules. Please take note of the following:

+ * + *
    + *
  • This method does not register the repository (e.g., in the cluster state).
  • + *
  • This method starts the repository. The repository should be closed after use.
  • + *
  • The repository metadata should be associated to an already registered non-internal repository type and factory pair.
  • + *
+ * + * @param repositoryMetadata the repository metadata + * @return the started repository + * @throws RepositoryException if repository type is not registered + */ + public Repository createRepository(RepositoryMetadata repositoryMetadata) { + return createRepository(repositoryMetadata, typesRegistry, RepositoriesService::throwRepositoryTypeDoesNotExists); + } + private static Repository throwRepositoryTypeDoesNotExists(RepositoryMetadata repositoryMetadata) { throw new RepositoryException(repositoryMetadata.name(), "repository type [" + repositoryMetadata.type() + "] does not exist"); } From 2baf7c4d2360831a7facfe9a011648e52a068191 Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Mon, 5 Dec 2022 16:39:02 +0100 Subject: [PATCH 148/919] Add methods to prevent allocating long arrays during child navigation on H3 api (#92099) --- docs/changelog/92099.yaml | 6 + .../main/java/org/elasticsearch/h3/H3.java | 179 ++++++++-- .../java/org/elasticsearch/h3/Iterator.java | 306 ------------------ .../h3/ParentChildNavigationTests.java | 62 +++- 4 files changed, 219 insertions(+), 334 deletions(-) create mode 100644 docs/changelog/92099.yaml delete mode 100644 libs/h3/src/main/java/org/elasticsearch/h3/Iterator.java diff --git a/docs/changelog/92099.yaml b/docs/changelog/92099.yaml new file mode 100644 index 000000000000..4b50e6caf7b2 --- /dev/null +++ b/docs/changelog/92099.yaml @@ -0,0 +1,6 @@ +pr: 92099 +summary: Add methods to prevent allocating long arrays during child navigation on + H3 api +area: Geo +type: enhancement +issues: [] diff --git a/libs/h3/src/main/java/org/elasticsearch/h3/H3.java b/libs/h3/src/main/java/org/elasticsearch/h3/H3.java index 0f1d97e52166..c074b90dd796 100644 --- a/libs/h3/src/main/java/org/elasticsearch/h3/H3.java +++ b/libs/h3/src/main/java/org/elasticsearch/h3/H3.java @@ -229,13 +229,9 @@ public static String h3ToParent(String h3Address) { * Returns the children of the given index. */ public static long[] h3ToChildren(long h3) { - long[] children = new long[cellToChildrenSize(h3)]; - int res = H3Index.H3_get_resolution(h3); - Iterator.IterCellsChildren it = Iterator.iterInitParent(h3, res + 1); - int pos = 0; - while (it.h != Iterator.H3_NULL) { - children[pos++] = it.h; - Iterator.iterStepChild(it); + final long[] children = new long[h3ToChildrenSize(h3)]; + for (int i = 0; i < children.length; i++) { + children[i] = childPosToH3(h3, i); } return children; } @@ -248,6 +244,39 @@ public static String[] h3ToChildren(String h3Address) { return h3ToStringList(h3ToChildren(stringToH3(h3Address))); } + /** + * Returns the child cell at the given position + */ + public static long childPosToH3(long h3, int childPos) { + final int childrenRes = H3Index.H3_get_resolution(h3) + 1; + if (childrenRes > MAX_H3_RES) { + throw new IllegalArgumentException("Resolution overflow"); + } + final long childH = H3Index.H3_set_resolution(h3, childrenRes); + if (childPos == 0) { + return H3Index.H3_set_index_digit(childH, childrenRes, CoordIJK.Direction.CENTER_DIGIT.digit()); + } + final boolean isPentagon = isPentagon(h3); + final int maxPos = isPentagon ? 5 : 6; + if (childPos < 0 || childPos > maxPos) { + throw new IllegalArgumentException("invalid child position"); + } + if (isPentagon) { + // Pentagon skip digit (position) is the number 1, therefore we add one + // to the current position. + return H3Index.H3_set_index_digit(childH, childrenRes, childPos + 1); + } else { + return H3Index.H3_set_index_digit(childH, childrenRes, childPos); + } + } + + /** + * Returns the child address at the given position + */ + public static String childPosToH3(String h3Address, int childPos) { + return h3ToString(childPosToH3(stringToH3(h3Address), childPos)); + } + private static final int[] PEN_INTERSECTING_CHILDREN_DIRECTIONS = new int[] { 3, 1, 6, 4, 2 }; private static final int[] HEX_INTERSECTING_CHILDREN_DIRECTIONS = new int[] { 3, 6, 2, 5, 1, 4 }; @@ -256,16 +285,12 @@ public static String[] h3ToChildren(String h3Address) { * intersects with it. */ public static long[] h3ToNoChildrenIntersecting(long h3) { - final long[] children = new long[cellToChildrenSize(h3) - 1]; - final Iterator.IterCellsChildren it = Iterator.iterInitParent(h3, H3Index.H3_get_resolution(h3) + 1); - final int[] directions = H3.isPentagon(it.h) ? PEN_INTERSECTING_CHILDREN_DIRECTIONS : HEX_INTERSECTING_CHILDREN_DIRECTIONS; - int pos = 0; - Iterator.iterStepChild(it); - while (it.h != Iterator.H3_NULL) { - children[pos] = HexRing.h3NeighborInDirection(it.h, directions[pos++]); - Iterator.iterStepChild(it); + final boolean isPentagon = isPentagon(h3); + final long[] noChildren = new long[isPentagon ? 5 : 6]; + for (int i = 0; i < noChildren.length; i++) { + noChildren[i] = noChildIntersectingPosToH3(h3, i); } - return children; + return noChildren; } /** @@ -276,6 +301,39 @@ public static String[] h3ToNoChildrenIntersecting(String h3Address) { return h3ToStringList(h3ToNoChildrenIntersecting(stringToH3(h3Address))); } + /** + * Returns the no child intersecting cell at the given position + */ + public static long noChildIntersectingPosToH3(long h3, int childPos) { + final int childrenRes = H3Index.H3_get_resolution(h3) + 1; + if (childrenRes > MAX_H3_RES) { + throw new IllegalArgumentException("Resolution overflow"); + } + final boolean isPentagon = isPentagon(h3); + final int maxPos = isPentagon ? 4 : 5; + if (childPos < 0 || childPos > maxPos) { + throw new IllegalArgumentException("invalid child position"); + } + final long childH = H3Index.H3_set_resolution(h3, childrenRes); + if (isPentagon) { + // Pentagon skip digit (position) is the number 1, therefore we add one + // for the skip digit and one for the 0 (center) digit. + final long child = H3Index.H3_set_index_digit(childH, childrenRes, childPos + 2); + return HexRing.h3NeighborInDirection(child, PEN_INTERSECTING_CHILDREN_DIRECTIONS[childPos]); + } else { + // we add one for the 0 (center) digit. + final long child = H3Index.H3_set_index_digit(childH, childrenRes, childPos + 1); + return HexRing.h3NeighborInDirection(child, HEX_INTERSECTING_CHILDREN_DIRECTIONS[childPos]); + } + } + + /** + * Returns the no child intersecting cell at the given position + */ + public static String noChildIntersectingPosToH3(String h3Address, int childPos) { + return h3ToString(noChildIntersectingPosToH3(stringToH3(h3Address), childPos)); + } + /** * Returns the neighbor indexes. * @@ -319,23 +377,94 @@ public static boolean areNeighborCells(long origin, long destination) { } /** - * cellToChildrenSize returns the exact number of children for a cell at a + * h3ToChildrenSize returns the exact number of children for a cell at a * given child resolution. * - * @param h H3Index to find the number of children of + * @param h3 H3Index to find the number of children of + * @param childRes The child resolution you're interested in * - * @return int Exact number of children (handles hexagons and pentagons + * @return long Exact number of children (handles hexagons and pentagons * correctly) */ - private static int cellToChildrenSize(long h) { - int n = 1; - if (H3Index.H3_is_pentagon(h)) { - return (1 + 5 * (_ipow(7, n) - 1) / 6); + public static long h3ToChildrenSize(long h3, int childRes) { + final int parentRes = H3Index.H3_get_resolution(h3); + if (childRes <= parentRes || childRes > MAX_H3_RES) { + throw new IllegalArgumentException("Invalid child resolution [" + childRes + "]"); + } + final int n = childRes - parentRes; + if (H3Index.H3_is_pentagon(h3)) { + return (1L + 5L * (_ipow(7, n) - 1L) / 6L); } else { return _ipow(7, n); } } + /** + * h3ToChildrenSize returns the exact number of children for a h3 affress at a + * given child resolution. + * + * @param h3Address H3 address to find the number of children of + * @param childRes The child resolution you're interested in + * + * @return int Exact number of children (handles hexagons and pentagons + * correctly) + */ + public static long h3ToChildrenSize(String h3Address, int childRes) { + return h3ToChildrenSize(stringToH3(h3Address), childRes); + } + + /** + * h3ToChildrenSize returns the exact number of children + * + * @param h3 H3Index to find the number of children. + * + * @return int Exact number of children, 6 for Pentagons and 7 for hexagons, + */ + public static int h3ToChildrenSize(long h3) { + if (H3Index.H3_get_resolution(h3) == MAX_H3_RES) { + throw new IllegalArgumentException("Invalid child resolution [" + MAX_H3_RES + "]"); + } + return isPentagon(h3) ? 6 : 7; + } + + /** + * h3ToChildrenSize returns the exact number of children + * + * @param h3Address H3 address to find the number of children. + * + * @return int Exact number of children, 6 for Pentagons and 7 for hexagons, + */ + public static int h3ToChildrenSize(String h3Address) { + return h3ToChildrenSize(stringToH3(h3Address)); + } + + /** + * h3ToNotIntersectingChildrenSize returns the exact number of children intersecting + * the given parent but not part of the children set. + * + * @param h3 H3Index to find the number of children. + * + * @return int Exact number of children, 5 for Pentagons and 6 for hexagons, + */ + public static int h3ToNotIntersectingChildrenSize(long h3) { + if (H3Index.H3_get_resolution(h3) == MAX_H3_RES) { + throw new IllegalArgumentException("Invalid child resolution [" + MAX_H3_RES + "]"); + } + return isPentagon(h3) ? 5 : 6; + } + + /** + * h3ToNotIntersectingChildrenSize returns the exact number of children intersecting + * the given parent but not part of the children set. + * + * @param h3Address H3 address to find the number of children. + * + * @return int Exact number of children, 5 for Pentagons and 6 for hexagons, + */ + public static int h3ToNotIntersectingChildrenSize(String h3Address) { + return h3ToNotIntersectingChildrenSize(stringToH3(h3Address)); + } + /** * _ipow does integer exponentiation efficiently. Taken from StackOverflow. * @@ -344,8 +473,8 @@ private static int cellToChildrenSize(long h) { * * @return the exponentiated value */ - private static int _ipow(int base, int exp) { - int result = 1; + private static long _ipow(int base, int exp) { + long result = 1; while (exp != 0) { if ((exp & 1) != 0) { result *= base; diff --git a/libs/h3/src/main/java/org/elasticsearch/h3/Iterator.java b/libs/h3/src/main/java/org/elasticsearch/h3/Iterator.java deleted file mode 100644 index d711c3e4224a..000000000000 --- a/libs/h3/src/main/java/org/elasticsearch/h3/Iterator.java +++ /dev/null @@ -1,306 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * This project is based on a modification of https://github.com/uber/h3 which is licensed under the Apache 2.0 License. - * - * Copyright 2021 Uber Technologies, Inc. - */ -package org.elasticsearch.h3; - -/** - * Iterator structures and functions for the children of a cell. - */ -final class Iterator { - /** - * Invalid index used to indicate an error from latLngToCell and related - * functions or missing data in arrays of H3 indices. Analogous to NaN in - * floating point. - */ - public static final long H3_NULL = 0; - - /** - * The number of bits in a single H3 resolution digit. - */ - private static final int H3_PER_DIGIT_OFFSET = 3; - - /** - * IterCellsChildren: struct for iterating through the descendants of - * a given cell. - *

- * Constructors: - *

- * Initialize with either `iterInitParent` or `iterInitBaseCellNum`. - * `iterInitParent` sets up an iterator for all the children of a given - * parent cell at a given resolution. - *

- * `iterInitBaseCellNum` sets up an iterator for children cells, given - * a base cell number (0--121). - *

- * Iteration: - *

- * Step iterator with `iterStepChild`. - * During the lifetime of the `IterCellsChildren`, the current iterate - * is accessed via the `IterCellsChildren.h` member. - * When the iterator is exhausted or if there was an error in initialization, - * `IterCellsChildren.h` will be `H3_NULL` even after calling `iterStepChild`. - */ - static class IterCellsChildren { - long h; - int _parentRes; // parent resolution - int _skipDigit; // this digit skips `1` for pentagons - - IterCellsChildren(long h, int _parentRes, int _skipDigit) { - this.h = h; - this._parentRes = _parentRes; - this._skipDigit = _skipDigit; - } - } - - /** - * Create a fully nulled-out child iterator for when an iterator is exhausted. - * This helps minimize the chance that a user will depend on the iterator - * internal state after it's exhausted, like the child resolution, for - * example. - */ - private static IterCellsChildren nullIter() { - return new IterCellsChildren(H3_NULL, -1, -1); - } - - /** - ## Logic for iterating through the children of a cell - We'll describe the logic for .... - - normal (non pentagon iteration) - - pentagon iteration. define "pentagon digit" - ### Cell Index Component Diagrams - The lower 56 bits of an H3 Cell Index describe the following index components: - - the cell resolution (4 bits) - - the base cell number (7 bits) - - the child cell digit for each resolution from 1 to 15 (3*15 = 45 bits) - These are the bits we'll be focused on when iterating through child cells. - To help describe the iteration logic, we'll use diagrams displaying the - (decimal) values for each component like: - child digit for resolution 2 - / - | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | ... | - |-----|-------------|---|---|---|---|---|---|-----| - | 9 | 17 | 5 | 3 | 0 | 6 | 2 | 1 | ... | - ### Iteration through children of a hexagon (but not a pentagon) - Iteration through the children of a *hexagon* (but not a pentagon) - simply involves iterating through all the children values (0--6) - for each child digit (up to the child's resolution). - For example, suppose a resolution 3 hexagon index has the following - components: - parent resolution - / - | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | ... | - |-----|-------------|---|---|---|---|---|---|-----| - | 3 | 17 | 3 | 5 | 1 | 7 | 7 | 7 | ... | - The iteration through all children of resolution 6 would look like: - parent res child res - / / - | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ... | - |-----|-------------|---|---|---|---|---|---|---|---|-----| - | 6 | 17 | 3 | 5 | 1 | 0 | 0 | 0 | 7 | 7 | ... | - | 6 | 17 | 3 | 5 | 1 | 0 | 0 | 1 | 7 | 7 | ... | - | ... | | | | | | | | | | | - | 6 | 17 | 3 | 5 | 1 | 0 | 0 | 6 | 7 | 7 | ... | - | 6 | 17 | 3 | 5 | 1 | 0 | 1 | 0 | 7 | 7 | ... | - | 6 | 17 | 3 | 5 | 1 | 0 | 1 | 1 | 7 | 7 | ... | - | ... | | | | | | | | | | | - | 6 | 17 | 3 | 5 | 1 | 6 | 6 | 6 | 7 | 7 | ... | - ### Step sequence on a *pentagon* cell - Pentagon cells have a base cell number (e.g., 97) corresponding to a - resolution 0 pentagon, and have all zeros from digit 1 to the digit - corresponding to the cell's resolution. - (We'll drop the ellipses from now on, knowing that digits should contain - 7's beyond the cell resolution.) - parent res child res - / / - | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | - |-----|-------------|---|---|---|---|---|---| - | 6 | 97 | 0 | 0 | 0 | 0 | 0 | 0 | - Iteration through children of a *pentagon* is almost the same - as *hexagon* iteration, except that we skip the *first* 1 value - that appears in the "skip digit". This corresponds to the fact - that a pentagon only has 6 children, which are denoted with - the numbers {0,2,3,4,5,6}. - The skip digit starts at the child resolution position. - When iterating through children more than one resolution below - the parent, we move the skip digit to the left - (up to the next coarser resolution) each time we skip the 1 value - in that digit. - Iteration would start like: - parent res child res - / / - | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | - |-----|-------------|---|---|---|---|---|---| - | 6 | 97 | 0 | 0 | 0 | 0 | 0 | 0 | - \ - skip digit - Noticing we skip the 1 value and move the skip digit, - the next iterate would be: - | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | - |-----|-------------|---|---|---|---|---|---| - | 6 | 97 | 0 | 0 | 0 | 0 | 0 | 2 | - \ - skip digit - Iteration continues normally until we get to: - | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | - |-----|-------------|---|---|---|---|---|---| - | 6 | 97 | 0 | 0 | 0 | 0 | 0 | 6 | - \ - skip digit - which is followed by (skipping the 1): - | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | - |-----|-------------|---|---|---|---|---|---| - | 6 | 97 | 0 | 0 | 0 | 0 | 2 | 0 | - \ - skip digit - For the next iterate, we won't skip the `1` in the previous digit - because it is no longer the skip digit: - | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | - |-----|-------------|---|---|---|---|---|---| - | 6 | 97 | 0 | 0 | 0 | 0 | 2 | 1 | - \ - skip digit - Iteration continues normally until we're right before the next skip - digit: - | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | - |-----|-------------|---|---|---|---|---|---| - | 6 | 97 | 0 | 0 | 0 | 0 | 6 | 6 | - \ - skip digit - Which is followed by - | res | base cell # | 1 | 2 | 3 | 4 | 5 | 6 | - |-----|-------------|---|---|---|---|---|---| - | 6 | 97 | 0 | 0 | 0 | 2 | 0 | 0 | - \ - skip digit - and so on. - */ - - /** - * Initialize a IterCellsChildren struct representing the sequence giving - * the children of cell `h` at resolution `childRes`. - *

- * At any point in the iteration, starting once - * the struct is initialized, IterCellsChildren.h gives the current child. - *

- * Also, IterCellsChildren.h == H3_NULL when all the children have been iterated - * through, or if the input to `iterInitParent` was invalid. - */ - public static IterCellsChildren iterInitParent(long h, int childRes) { - - int parentRes = H3Index.H3_get_resolution(h); - - if (childRes < parentRes || childRes > Constants.MAX_H3_RES || h == H3_NULL) { - return nullIter(); - } - - long newH = zeroIndexDigits(h, parentRes + 1, childRes); - newH = H3Index.H3_set_resolution(newH, childRes); - - int _skipDigit; - if (H3Index.H3_is_pentagon(newH)) { - // The skip digit skips `1` for pentagons. - // The "_skipDigit" moves to the left as we count up from the - // child resolution to the parent resolution. - _skipDigit = childRes; - } else { - // if not a pentagon, we can ignore "skip digit" logic - _skipDigit = -1; - } - - return new IterCellsChildren(newH, parentRes, _skipDigit); - } - - /** - * Step a IterCellsChildren to the next child cell. - * When the iteration is over, IterCellsChildren.h will be H3_NULL. - * Handles iterating through hexagon and pentagon cells. - */ - public static void iterStepChild(IterCellsChildren it) { - // once h == H3_NULL, the iterator returns an infinite sequence of H3_NULL - if (it.h == H3_NULL) return; - - int childRes = H3Index.H3_get_resolution(it.h); - - incrementResDigit(it, childRes); - - for (int i = childRes; i >= it._parentRes; i--) { - if (i == it._parentRes) { - // if we're modifying the parent resolution digit, then we're done - // *it = _null_iter(); - it.h = H3_NULL; - return; - } - - // PENTAGON_SKIPPED_DIGIT == 1 - if (i == it._skipDigit && getResDigit(it, i) == CoordIJK.Direction.PENTAGON_SKIPPED_DIGIT.digit()) { - // Then we are iterating through the children of a pentagon cell. - // All children of a pentagon have the property that the first - // nonzero digit between the parent and child resolutions is - // not 1. - // I.e., we never see a sequence like 00001. - // Thus, we skip the `1` in this digit. - incrementResDigit(it, i); - it._skipDigit -= 1; - return; - } - - // INVALID_DIGIT == 7 - if (getResDigit(it, i) == CoordIJK.Direction.INVALID_DIGIT.digit()) { - incrementResDigit(it, i); // zeros out it[i] and increments it[i-1] by 1 - } else { - break; - } - } - } - - // extract the `res` digit (0--7) of the current cell - private static int getResDigit(IterCellsChildren it, int res) { - return H3Index.H3_get_index_digit(it.h, res); - } - - /** - * Zero out index digits from start to end, inclusive. - * No-op if start > end. - */ - private static long zeroIndexDigits(long h, int start, int end) { - if (start > end) { - return h; - } - - long m = 0; - - m = ~m; - m <<= H3_PER_DIGIT_OFFSET * (end - start + 1); - m = ~m; - m <<= H3_PER_DIGIT_OFFSET * (Constants.MAX_H3_RES - end); - m = ~m; - - return h & m; - } - - // increment the digit (0--7) at location `res` - private static void incrementResDigit(IterCellsChildren it, int res) { - long val = 1; - val <<= H3_PER_DIGIT_OFFSET * (Constants.MAX_H3_RES - res); - it.h += val; - } -} diff --git a/libs/h3/src/test/java/org/elasticsearch/h3/ParentChildNavigationTests.java b/libs/h3/src/test/java/org/elasticsearch/h3/ParentChildNavigationTests.java index 8785050b32cc..06190cbef558 100644 --- a/libs/h3/src/test/java/org/elasticsearch/h3/ParentChildNavigationTests.java +++ b/libs/h3/src/test/java/org/elasticsearch/h3/ParentChildNavigationTests.java @@ -20,17 +20,70 @@ import com.carrotsearch.randomizedtesting.generators.RandomPicks; +import org.apache.lucene.geo.Point; import org.apache.lucene.spatial3d.geom.GeoPoint; import org.apache.lucene.spatial3d.geom.GeoPolygon; import org.apache.lucene.spatial3d.geom.GeoPolygonFactory; import org.apache.lucene.spatial3d.geom.PlanetModel; +import org.apache.lucene.tests.geo.GeoTestUtil; +import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.test.ESTestCase; +import org.hamcrest.Matchers; import java.util.ArrayList; import java.util.List; +import java.util.Set; public class ParentChildNavigationTests extends ESTestCase { + public void testChildrenSize() { + Point point = GeoTestUtil.nextPoint(); + int res = randomInt(H3.MAX_H3_RES - 1); + String h3Address = H3.geoToH3Address(point.getLat(), point.getLon(), res); + // check invalid resolutions + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> H3.h3ToChildrenSize(h3Address, res)); + assertThat(ex.getMessage(), Matchers.containsString("Invalid child resolution")); + ex = expectThrows(IllegalArgumentException.class, () -> H3.h3ToChildrenSize(h3Address, H3.MAX_H3_RES + 1)); + assertThat(ex.getMessage(), Matchers.containsString("Invalid child resolution")); + ex = expectThrows( + IllegalArgumentException.class, + () -> H3.h3ToChildrenSize(H3.geoToH3(point.getLat(), point.getLon(), H3.MAX_H3_RES)) + ); + assertThat(ex.getMessage(), Matchers.containsString("Invalid child resolution")); + // check methods gives same answer + assertEquals(H3.h3ToChildrenSize(h3Address), H3.h3ToChildrenSize(h3Address, res + 1)); + // check against brute force counting + int childrenRes = Math.min(H3.MAX_H3_RES, res + randomIntBetween(2, 7)); + long numChildren = H3.h3ToChildrenSize(h3Address, childrenRes); + assertEquals(numChildren(h3Address, childrenRes), numChildren); + } + + private long numChildren(String h3Address, int finalRes) { + if (H3.getResolution(h3Address) == finalRes) { + return 1; + } + long result = 0; + for (int i = 0; i < H3.h3ToChildrenSize(h3Address); i++) { + result += numChildren(H3.childPosToH3(h3Address, i), finalRes); + } + return result; + } + + public void testNoChildrenIntersectingSize() { + Point point = GeoTestUtil.nextPoint(); + int res = randomInt(H3.MAX_H3_RES - 1); + String h3Address = H3.geoToH3Address(point.getLat(), point.getLon(), res); + // check invalid resolutions + IllegalArgumentException ex = expectThrows( + IllegalArgumentException.class, + () -> H3.h3ToNotIntersectingChildrenSize(H3.geoToH3(point.getLat(), point.getLon(), H3.MAX_H3_RES)) + ); + assertThat(ex.getMessage(), Matchers.containsString("Invalid child resolution")); + // check against brute force counting + long numChildren = H3.h3ToNotIntersectingChildrenSize(h3Address); + assertEquals(H3.h3ToNoChildrenIntersecting(h3Address).length, numChildren); + } + public void testParentChild() { String[] h3Addresses = H3.getStringRes0Cells(); String h3Address = RandomPicks.randomFrom(random(), h3Addresses); @@ -38,6 +91,9 @@ public void testParentChild() { values[0] = h3Address; for (int i = 1; i < H3.MAX_H3_RES; i++) { h3Addresses = H3.h3ToChildren(h3Address); + // check all elements are unique + Set mySet = Sets.newHashSet(h3Addresses); + assertEquals(mySet.size(), h3Addresses.length); h3Address = RandomPicks.randomFrom(random(), h3Addresses); values[i] = h3Address; } @@ -84,9 +140,9 @@ public void testNoChildrenIntersecting() { } private void assertIntersectingChildren(String h3Address, String[] children) { - String[] intersectingNotChildren = H3.h3ToNoChildrenIntersecting(h3Address); - for (String noChild : intersectingNotChildren) { - GeoPolygon p = getGeoPolygon(noChild); + int size = H3.h3ToNotIntersectingChildrenSize(h3Address); + for (int i = 0; i < size; i++) { + GeoPolygon p = getGeoPolygon(H3.noChildIntersectingPosToH3(h3Address, i)); int intersections = 0; for (String o : children) { if (p.intersects(getGeoPolygon(o))) { From 3f2f9de928bab98ce0746ab2c665d0fd13d41a97 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Mon, 5 Dec 2022 07:47:42 -0800 Subject: [PATCH 149/919] [DOCS] Refresh machine learning rule docs (#92013) --- .../ml-configuring-alerts.asciidoc | 100 ++++++++---------- .../ml/images/ml-anomaly-alert-actions.jpg | Bin 22910 -> 0 bytes .../ml/images/ml-anomaly-alert-messages.jpg | Bin 118499 -> 0 bytes .../ml/images/ml-anomaly-alert-messages.png | Bin 0 -> 214169 bytes .../ml/images/ml-anomaly-alert-severity.jpg | Bin 322311 -> 0 bytes .../ml/images/ml-anomaly-alert-severity.png | Bin 0 -> 241369 bytes .../ml/images/ml-health-check-config.jpg | Bin 72237 -> 0 bytes .../ml/images/ml-health-check-config.png | Bin 0 -> 219776 bytes docs/reference/ml/images/ml-rule.jpg | Bin 120839 -> 0 bytes docs/reference/ml/images/ml-rule.png | Bin 0 -> 325559 bytes 10 files changed, 47 insertions(+), 53 deletions(-) delete mode 100644 docs/reference/ml/images/ml-anomaly-alert-actions.jpg delete mode 100644 docs/reference/ml/images/ml-anomaly-alert-messages.jpg create mode 100644 docs/reference/ml/images/ml-anomaly-alert-messages.png delete mode 100644 docs/reference/ml/images/ml-anomaly-alert-severity.jpg create mode 100644 docs/reference/ml/images/ml-anomaly-alert-severity.png delete mode 100644 docs/reference/ml/images/ml-health-check-config.jpg create mode 100644 docs/reference/ml/images/ml-health-check-config.png delete mode 100644 docs/reference/ml/images/ml-rule.jpg create mode 100644 docs/reference/ml/images/ml-rule.png diff --git a/docs/reference/ml/anomaly-detection/ml-configuring-alerts.asciidoc b/docs/reference/ml/anomaly-detection/ml-configuring-alerts.asciidoc index 7afaf88081b2..265b94d1d69d 100644 --- a/docs/reference/ml/anomaly-detection/ml-configuring-alerts.asciidoc +++ b/docs/reference/ml/anomaly-detection/ml-configuring-alerts.asciidoc @@ -30,38 +30,38 @@ ideal for this purpose. [[creating-ml-rules]] == Creating a rule -You can create {ml} rules in the {anomaly-job} wizard after you start the job, -from the job list, or under **{stack-manage-app} > {alerts-ui}**. - -On the *Create rule* window, give a name to the rule and optionally provide -tags. Specify the time interval for the rule to check detected anomalies or job -health changes. It is recommended to select an interval that is close to the -bucket span of the job. You can also select a notification option with the -_Notify_ selector. An alert remains active as long as the configured conditions -are met during the check interval. When there is no matching condition in the -next interval, the `Recovered` action group is invoked and the status of the -alert changes to `OK`. For more details, refer to the documentation of -{kibana-ref}/create-and-manage-rules.html#defining-rules-general-details[general rule details]. - -Select the rule type you want to create under the {ml} section and continue to -configure it depending on whether it is an -<> or an -<> rule. +In *{stack-manage-app} > {rules-ui}*, you can create both types of {ml} rules: [role="screenshot"] -image::images/ml-rule.jpg["Creating a new machine learning rule"] +image::images/ml-rule.png["Creating a new machine learning rule",500] +// NOTE: This is an autogenerated screenshot. Do not edit it directly. +When you create a {ml} rule, you must provide a time interval for the rule to +check detected anomalies or job health changes. It is recommended to select an +interval that is close to the bucket span of the job. + +You must also select a notification option, which affects how often alerts +generate actions. Options include running actions at each check interval, only +when the alert status changes, or at a custom action interval. For more +information about these options, refer to the +{kibana-ref}/create-and-manage-rules.html#defining-rules-general-details[General rule details]. + +In the *{ml-app}* app, you can create only {anomaly-detect} alert rules; create +them from the {anomaly-job} wizard after you start the job or from the +{anomaly-job} list. [[creating-anomaly-alert-rules]] === {anomaly-detect-cap} alert -Select the job that the rule applies to. +When you create an {anomaly-detect} alert rule, you must select the job that +the rule applies to. -You must select a type of {ml} result. In particular, you can create rules based -on bucket, record, or influencer results. +You must also select a type of {ml} result. In particular, you can create rules +based on bucket, record, or influencer results. [role="screenshot"] -image::images/ml-anomaly-alert-severity.jpg["Selecting result type, severity, and test interval", 500] +image::images/ml-anomaly-alert-severity.png["Selecting result type, severity, and test interval", 500] +// NOTE: This is an autogenerated screenshot. Do not edit it directly. For each rule, you can configure the `anomaly_score` that triggers the action. The `anomaly_score` indicates the significance of a given anomaly compared to @@ -98,8 +98,9 @@ are met. [[creating-anomaly-jobs-health-rules]] === {anomaly-jobs-cap} health -Select the job or group that the rule applies to. If you assign more jobs to the -group, they are included the next time the rule conditions are checked. +When you create an {anomaly-jobs} health rule, you must select the job or group +that the rule applies to. If you assign more jobs to the group, they are +included the next time the rule conditions are checked. You can also use a special character (`*`) to apply the rule to all your jobs. Jobs created after the rule are automatically included. You can exclude jobs @@ -131,7 +132,8 @@ _Errors in job messages_:: that occur after the rule is created; it does not look at historic behavior. [role="screenshot"] -image::images/ml-health-check-config.jpg["Selecting health checkers"] +image::images/ml-health-check-config.png["Selecting health checkers",500] +// NOTE: This is an autogenerated screenshot. Do not edit it directly. As the last step in the rule creation process, <> that occur when the conditions @@ -141,43 +143,35 @@ are met. [[defining-actions]] == Defining actions -Connect your rule to actions that use supported built-in integrations by -selecting a connector type. Connectors are {kib} services or third-party -integrations that perform an action when the rule conditions are met or the -alert is recovered. You can select in which case the action will run. - -[role="screenshot"] -image::images/ml-anomaly-alert-actions.jpg["Selecting connector type"] - -For example, you can choose _Slack_ as a connector type and configure it to send -a message to a channel you selected. You can also create an index connector that -writes the JSON object you configure to a specific index. It's also possible to -customize the notification messages. A list of variables is available to include -in the message, like job ID, anomaly score, time, top influencers, {dfeed} ID, -memory status and so on based on the selected rule type. Refer to -<> to see the full list of available variables by rule type. +Your rule can use connectors, which are {kib} services or supported third-party +integrations that run actions when the rule conditions are met or when the +alert is recovered. For details about creating connectors, refer to +{kibana-ref}/action-types.html[Connectors]. +For example, you can use a Slack connector to send a message to a channel. Or +you can use an index connector that writes an JSON object to a specific index. +It's also possible to customize the notification messages. There is a set of +variables that you can include in the message depending on the rule type; refer +to <>. [role="screenshot"] -image::images/ml-anomaly-alert-messages.jpg["Customizing your message"] - -After you save the configurations, the rule appears in the *{alerts-ui}* list -where you can check its status and see the overview of its configuration -information. +image::images/ml-anomaly-alert-messages.png["Customizing your message",500] +// NOTE: This is an autogenerated screenshot. Do not edit it directly. -The name of an alert is always the same as the job ID of the associated -{anomaly-job} that triggered it. You can mute the notifications for a particular -{anomaly-job} on the page of the rule that lists the individual alerts. You can -open it via *{alerts-ui}* by selecting the rule name. +After you save the configurations, the rule appears in the +*{stack-manage-app} > {rules-ui}* list; you can check its status and see the +overview of its configuration information. +When an alert occurs, it is always the same name as the job ID of the associated +{anomaly-job} that triggered it. If necessary, you can snooze rules to prevent +them from generating actions. For more details, refer to +{kibana-ref}/create-and-manage-rules.html#controlling-rules[Snooze and disable rules]. [[action-variables]] == Action variables -You can add different variables to your action. The following variables are -specific to the {ml} rule types. An `*` marks the variables that can be used for -actions of recovered alerts. - +The following variables are specific to the {ml} rule types. An asterisk (`*`) +marks the variables that you can use in actions related to recovered alerts. [[anomaly-alert-action-variables]] === {anomaly-detect-cap} alert action variables diff --git a/docs/reference/ml/images/ml-anomaly-alert-actions.jpg b/docs/reference/ml/images/ml-anomaly-alert-actions.jpg deleted file mode 100644 index a0b75152ca71f1a98f11766c77d46274f6ed661d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22910 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

$piiF)}hU4=O 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

-l~0< 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

tjHBy9}kcFzqtsYsRJ;; zxbaPD4b*>o)$!bEzKm_m-ggdOB=~5)Z?2fHw->*MU>Pe@R{|JC$RDnHnfS}V1v(Tq zz_V`6A^JP0^>h!z9G_12wKQiH($fbjcZ;Xf5y?DwlnTpI%A@gZD*qpMyS97f-V2x2 z3cc%Vp~TNAz;|cVol7;NOr))(_S~eSp^#P#4;81eyuguXopC^#%1G^Z2kRNY(O0Q3 z6}vxaO0D_&m)il*Px}t_PI>VwMR3!3orcP}`{M7@7WXF$VMaCqsD-x^yGg{gR@Pj? zpEQnyQ0#tnU4`{bWZ{#=hE{Ra%A4ojch)|Z|ACa7MRu{_qC0LM)Xy9y>23PV%Fyw1 z(nq5UL~|T+epslI(+&6OG5btaWc1Y z$#neb-s#+`JEMB!LdC8|aWo3JN1g;F{MU{?rCExLU$XwOMI?7e?1Cu++6y;t;C-%I z02t?-6r!%+tL3~kt|=r@rBCmgOl!t63-5w;sa(CjKNI**_jwwdZ_R594eamEFb6XQ z(sb~TQ?`dD4>!9-%fI5WsETEJw5Zjgr)<9W`y@&}9|88bIZ6PMZfa3B%?r22+m0*$ zWKyxYpg*9Q%3HL&^2IW(M%^U?F5|&ijf0u;pVP5a*IbPp7Bg>&Cky1#o)2(-XRX?J zfmJp@V!gqrWwXdI4_d9)e;HAbN3;72Ty_h<5qeLsJ7EklK3;GF=k(x8b695ngC;CR z9)`gG)ahXaIFPoN?vW>oyq8`1yd}pKnkD|iXPKcg}vs>=!!;{h84e^XIQJ~j}0(!Sp0@+@A7haQ!DV(z};u|1567J z6CDSaeR~`Tn|yt-w_Kr4;&1kFxjnJoxpYhVb7E5a7^%&kk|LKdB$#}Ezv@$H*h%8@ z&ZY=7OR8x}IiK0W?|4Qdlfu5`|J?j;5dcrn6~?_eXQyrArA~<={7Bt*!^+*f#95St zuo0ETKf-;I76doFHK>Gm;_hsJypUBEUlY)KFYAlQ3Ps%ooBReFVH0*EOkEQfqBVL& z(jRg%bC${PRx%sTRF(i>`vJp1rWlpkWSK#0$D%P9xUSQ0i#Hh!PFz+G zx@xM~;Qa|ItHWIVhpUu2{TE^g?`~i{roZu`G&^S71h(77Rw&K1&WsaB17H#z`-N!8 zw0OU{z)>i?YcW$~(yU`^bh1*L1OU%PM>7Seo*X>s4K#qQk(ichiL&22jxw9Qw>aPG z%~cCg2q!Ggf1(5p4gDO5PT-Jd|IbZi!0L8-i~gV#N0}P_`n~$%=2&Z7evJ%uJZcX` z94GOQAEr%xKy|zOb!x6yC^4;sA0g;-<8^;^Uk7H{yN$@rQjV?uEUuo1y#NG1A@nvk z>y~k=#*%CWZcgsPHi{Ks?iWjD(kLz&Q9Ol3KTHtm8 zf)?C^H&*-r#M9O`N&xTl%Nchlm9~ly#_cyf4867vFF%m$q3Bu`KlJA#Kt6H2nl)_D zQZ3aCDv%|$(XwVP(k#a#5i}slwrf}PjwIzs&o1))ppY1WgikL8kM?342$aO)n}YPc+qD$ zpJ~xMZIlLK#S|8hSvn(^GCSrPu;e?(-dbsL+Vet^HSufbj3Mu;&fX|)>CaRy zd!UcCKcaIa?zxC`vt%TbRLJzPZ=}#Yix&5^-nkp<=|oezz<$#-`)j7#^I?!$YQkAz_2 zGrd9%4+$p<^YP>k4-P$UNb-*Po_hp6uf?V5RdQo=P2`tayd?R!Mqj<9J9TX>v1nvI z_l0>>TzBPweAuBG`ub8lb7U1U zgrdSpTj7Lu&^BxNx>sSrYu_iD)Z%#UL5hCk#svRZpIXUf!H#oH_gNy0{Pk3YY1Y=bEz2`{4MRx({TP6X$XmubV>yuXa!A%)0wy$wPz}HCfS7mR(In4Z_l$citWd`7?Z&u zqKN@AA6YBS#;WpDdB4)?NJ2h8;LxJ?&|Uc0dW5;-9A7D}j~gx0-y$m{aWZZHNgK#m zhL_H;-yRzK{bdGES6{u{-71IGQWy}F@L#tk#!%(evk0JsZbfs8p$SrzG~f9#+3&Z+BnDd-!_m?c~6O3WrwUJz(?d*E`kh zg?xT-^t4H+8!Q=_R?%5BC^6jtY+NV=k#^$5UlOZ)lewKLIAKzoF^>7MhDttegv@sw z%v9uh?Q_k>9V+&dXIFdL#ow!U+Mbu(lv3dsS8Qdv=Dh6sMAw_6lkz*3#Dfn7eNNo_ z4NJXVopwnhQ&^SM*oS!cZoP7{d(a0O4Vqbk{44UTp5C`LDWnKyG3zu_r;Q~Mj9TX~ zay#wtQ)I?AZk_$utaH78rvsJKfSmuU`7)_)m8QAo%OM`BC9aYm4_6LC+yH2~Gc{y0 z4{3x+!+Wz}Ghe4XK+vnzJ+5EwkL0_7w_!=shn$lSa*F_tB~hd}Z9!X3OKtQ==c}6- zNVRNyM7*9oie!UZF0KCi1%k0`>CUy428obeu*s;NwOi)v^XTih%LcMVWiGt%z)LcWjMru)!iQ2 z6xKarE+BmW+-%W-pH1S#I9K>_-e(PgNvC>74_7;`DU&taP|fa0DPMNIGHUql_|7bq z!zM+0I;lx8`>YooX{aC7{971zyMG8Gu?Iqbh{xsKpabIoOIpHp^~*thmUuDaN0#Xp zlkLjpis_cxLn1PY3hig5$7kC3#Xo6NIXHs?mm)O_G`m4)(3m2*X%;hOE-i}&ePP^8 zT=+xkB0HYV9d?`4DN~8KLDQr7S$rF^gh;EE>4FZ4)N+{;I<41^#WDW4s@LW!WvMw4 z8=#*bg`eK5JCy6zM$J?>jr3{o)eL7;cQ0ek=x6Y{O!n`Y-?Y#vQO^n9{eFJCjTKfv zJYPG1vayj5S+JuA*Wu(#r9=rbC`T-~?1`woo;|jh)l-y}3O=!hs$lN2{X0pYNken} z`m`-7A$w66(<2MsNs@0X+t{9w*T~uQRk^fpH*8hE?iQ)KrsFn2#}Y0A0Tb>5{#L?$z9XY(mdkyn_|fJz8)yKlG|^qjGq zET>2!^zg}k_|tOF?-C{lQfiwXJ5j!aJ4FTiU+mJkfx&Xy&(fOKxr$MLJW0rKRVABj z);yi47hO&`X!FK0n-xYHBsFC0d>lCvUG1J$YmHh>M|4?f^M+@!4uo^q4R5d(&xxP5 z)j&ZwWuK?dvq^~6XieKF4%!KiQ%2crj(X!b@xDI>P7h$%D#G20E)BeRy=ed0aU`JD ze`3K$fob==N217X%J{5-u`H_e8MZn2FEWLr-N9Ro^cXua4&bXvx{}{iiVX59(S#)w)hnr1jDKR1DH!H=Qy_%Z70| zSt=aIHrWe{w3)Tc)m>g7e0Y0^)Kn+k4O!r_O8s_X_d9^ybaW*EHXjw1rQR0GLb)@z z!TKfO!S#m>6C3&mu#c=_|AMP39&{)XY5<2g z#K}Lq)@L*y=9Jn8%h`jx$kHQFv}Wpd%^BlsZ*kIt>rSeB^Df*Q|LzLAoTTkd@3LJ@ z6=hF&t)s>i7eWr~gK@)=KNbE`VqPg})bZ7P_@`20M~BTFl6WCK=8VRp2l@4Pf%v?1 zDMIor1XELs-PH>%R$Rw-U$eTwVl%gnh&yY=NM##H-ieYv^Zn;qT_;U`ms*z!VWDipvAfNXA@!rP+GX-qhd0ywIhz`OfsWe1nGTJ3_ZfAHO z9tn_I#dM_{Elq#K-r*D~vW#$g7O?zhv=eMIMfK%ADpd7c!)Q=dq=;d?WiKU zFw!;2zndyA(6|_M&a=mJvK?MO(dsC>M8BvJXfi@2n-bL$poss@%|$VXURF`IRlYZQ zr4jD*wn(YeU7Y?)TedZ75E~2T(EcjR9o?}l$!pWD=|h|Hms09_{$lMB7RC>y%FLy$ zP!{JqmW}LqSI5)B5L*|<{_-4GpIl>Hyn&no?#?{E4@<;3Rq;srOTF1*fkcJ$uist9 zsKOF(8*J1o-x*_8%;-ltAXr20VtgI&*+H}g5@^RvQ66ITbkURCH}*a}fvr>q+3V`! zu09=ZOZw!TZ+z6WQd45eQh_gS`FznKCx!+0?w@SUkzP)={)fxh+lI%L)r%dh- z))-HhZnihhz+Fu27Wtj>bhc?N%2sB?S*9m982fgFvC5}&LhL6q1M^4S{Q!K-`wd3O`9p$b|W;xF&#CjoHr~( zz)V%n1QpWZ^;#;WUgDK36%<3cpui##&Ii}fVE;GOdU@J7)pEnoxA?SldE^zyx>e^F zEP7Sv8#7?qwy9(m;LDhVmV+EuJ$=0$4ff{5IrdZ~k6uAr=nWqmgIfOmWUOmN(==I? z`v#uQD$s){%1>E}f^5K9@wMV@cvBy!GcN5LsBNk{NEQhv>Ui-JM_s^Wu4Y1wa&7A6 zuE~6GDV6S=Qk&XXwlxT|645%f88uzUX@M3HlLoGZxwU}X96ffoxs$X=4Z zylSOfiQ!y3ZDI^<Tg1gzV92~_iUBVU_1TrjJYfL)pOSq=1a_fY z(OnutJSuT$xm#VVMHN#)eMC|FvwDfyHkw?C(+GG@&Ul^fRPup{(A^dB_S?CtHDc9o z{GJa$okW$({fDzQXKL{rAd`i9hw!nDMT@;>t1Cg683kng;rtoO#2*a8`qv#^?#(3y z30XW|Gv)3T^uPf$DGk>1A4tt*T1w0{1YJf@4yejPZ9iV(0)6Pff@Gv`j73lP7h#+2 zD!(!}*4#SmWt++Vz0nhdf{jh|CvR4bWfRqW$5kUFM=GT58Chvv1f7qko19PX9IB(Z zT+%~N;ngygiUm8P@O{>hMviE@!E3OXe1lBd&necEC(bP-nlwpT4pXJjgvx6q;3jXEZ1?6GC0YsemL&aNThX!)@0x~%rB6M#So1F zoCe~9pZ+cwLrFRT__U_ITvIz34eX0&L5r%WPU8N}OteCFBhoysdquLE%pXG!apJJl z5u-Xw_L?lfGVPw*rgHBa#T9)RgvXA~H<}{e&1lu~*OhFRx52=ZYzA%Ima>FtaYo)E zr6+-HJ2G+X3XWjIx@5LVZZTTjg;Syh)v%#v%VERfYba^kacZG4vGIw8_6hU`od0~9 zmD14Dq2%^5W|Izz2ZmG!jeL{p`$C|uwUKN^{gyCh8yyb=ds-xuI2&%MX_f7~q^iMJ zgY3G&B+%U@@hv{Dev{ln#({lb%79N>j4Lu(1ucw#Zyq>2ZN1dL3}49GQ?tX8&8yRl zWV#wrmHE&h)a&YSG41ALIUkNz4U(R}*kC5z>N6+5t)uSW?!EbB;aBUG3|XsaGCKEM zczSj(0cz^zxmDi&w~hUmrV8MBcHze=Jjo^3W3d(u%>+@ldWEWxX`Fbk69jFMoX^^w zCw%8`^nIe5Ch=y!{#z@+0K-l-eMycyXkkx_T`1(XE6d7{3U!qQvmz(fX16I)UL7qY z1|yVGKBdE@%O$WkTh6Rb5ck4YFqRZm1O_1yET2!gONmk>s5CLi+=G6##a^6tjylb9 zZhhrsG6jF8#q?b7C1M;?2EA!|$BW%|1`2=QHrAUfGE_Vnm83Tnz~P_K1%_5dT*0D! z$i}G0j=&a9%IC*t_9}s$ZZ1CQovd4+Q9toYr)!_2R_=rIm+jU0BhPLkWvZ69T`2WK znWgi!s|Z8&6sD-ocE2(B^0W1KLt?M5?yux>0_FVINJ$ViNQ?Cs9R8edi-J|$t_pP?*7E`yo`X= z?Bv5ieLQf|GuynXa-p1O>Z%=Co71A*s%;A1bBOyk%3S7~o_>>$iH#%_aa@qBAr^(or9Rx1q};?x z4WjlYZDD^~V57(kcctwcOtQEcGVgw}(q0<5&tlj1g88ykZ7_hPL_;%X;AV^|Qv|4A z@ZX7Se*Kjc^`T|Ek`Ustn4KVji|=6ZhFGvg>rcFZu3Zq^y~;s8s%wRZ9ix!o!Wt zrm$lwZLy2@A%W2h>zvVfv4nf>;)P8Er8{9Xp$&Vo9pq;q-$GeZo5Q2TJ!GYn|0G4- z-Vn%uXP~)^qm4=X+L2t3v76z3qH!$CbUV{*R!^pI!D^9)V;ZKWvD^D0)CjC4h%*yZ z0!h*Qrpj1sECn$Ps{>B`te_b-_?~@^LCXP1FVrT_An|4g6Kv)$gklKMINb%qUzTsi zvT)?%b7({Iu%3G@Q!#2M*FqXvr%)rrdu+5tLL-)lf&Rf&D4Ikq8I5V5kKBx3RS9Kjgj-v%n$7tr0lkAGGbR9 z#}K{I?<7l?MgIv=B!4uJpLjwYb*!5zCpgv_Vrpu}p$>f9fm3RcWG zMn?UnDON0WqqStinP&tvTtXF)(%QcM7)B3olaZEr)2*#{tTYr2P7Z%A7v%{jWXzA< z@#)bVlBbkFNw1ah!el|+?R=>5QE776fcM)#I$!E|$PH8q=V^U$i+z={qw4oIOfea* zGY1I>u@NS)Aot@aeo5F?lyP(STJ3yo#8Y6-e!NXMRz_(PCQuf@B-%5E|S7m(K7yUmhfHU!ky$A6Ht<6;` z95d{K;d&LRb=Nh|g!8Vl{ymFb>W88mx)x-Y_dErA<39)UzIq5uf)Led(GF(Chjmr^ zF}SrkqaC0=W}p(XNv9Qqci{WLkxsiu1f`F`g~9P%EAU2q9ZjAjbB9{ZrjHwUOUH5U zOwFy4Wu`Ae)R%6A4G2vy(YOu$ke~kv%)q^VE~55LNjnqe&jBRCF}JVz{&anq0}?|D z2Aqt)H2kqZNZS5+#&8{7A$8;tZ^}ShAb1n&j|18EGm_IBR1LPvN&dbqw(lKwhQ7Cq z_%}itmUILkZ}nVC-yIYieinXRAwk`)P>uaQ1{@)~>7f*uYfI(xYDISWd%B^97!O=3 zSHbNjmTj$WlP{eC4(yaFf`@-~zNUV#JB4a4{#tyqj6WjhD1VHtExwHD=4H0X7n*e- zuT^pIdwvLm=x8M4@#?V;l1jBbe~FJ;tH_K~`>Dg@Hj5LKUCIdqu4tKCUkDHxPB0)~ z&IBt1p%z~?(JsN^4W948^V~GSIi^UY+IESzViLTBTahj(s1Fv$R@_A`SKNuzXK2~E zDbog9KR@1vjO>KuZxH);lY|j*O<2gMV`%{$SD&ihbDCG@{Aw}u=P81F}*~{f|Hs#C8_zLEcjrlSe6|(MiD$$Y#GnW^@_Lu|< zGMR!NKIu$IUgt})G31#mlSJ0E_t_s*%crL{bGUHC5Pu2uREkymFL3zmjj?Igo5WS5 zFZi6<#?a+&^l@bNet+CK;OziJHF*(%8+Q`|>7rS#vUfUmi2GkRMT0XQsZ4&N6eUMj zS2`fAlm6gpP<|#3t=JI`xCmy)87qF58mAWOM_J0p{WP7nEBNWrW{)^r?o zBq{~#^7J?-=vFaH(qT_iJlY&&{2>Ju`o!^Y3=4YJL}qtUJVbTg;@T$?LHniL#P`Tv zf=&xL!^%8-=5SUrnx=1$b)|+IlH^z67&S0q#!}X_ot!dx76r+T51w}uvBSGQnvx6* zPd*!sY$bDCPwfNV{@b+0UNKeG-G*k;$`BIhvUnuEfmq3&e!iL;(|DeO7C=0ZL?3HF&Sq z)18TSD4ot#e}3xcsIB>IHH#b$TSUe4epSQg_xG3E6N?Myy@CRJwVC&Ves2yBxvW2$ zK5D%x4!1FG?*>RR>EW>e15rU8 zzFfCaV-?ju0)#9M#I;b=)zj<|fwxMP-tW!MYo;5@AJi~-O`L0&X?@|~khi8A#V`&T zMCig~YwVV&55{TW<2%Z{f~@OGKCDO&ZLwHpkWoDMmZ`5G*^nmQyk zI5)nU7;O9W7cjLlHug(~dI^!$FgnnDw8%W#qBs{)FWVBbf@QX zS5zO1O@>73-VHb3=A(;F`1i#Sp0ug`Crbh=ze$cf5y6sC=a)WR{fJ1hzy9haKB1nw zpv<~$NPt1H^0~-^V9qxUdR_lvRhaMfe?{$f-3P~FR8>)TKYT2WTBE#Qq1XpZa_PLI ztgYFMzCm+$(zSIRxt-sC8b@(lxE1Htv^1piI#8sPhn@9&H13Cfxa?;V)hWfDenx*J z7Vv&Mn5045utv${IwxjtB5pig!E<^!AznYH`CIZeI69MZ;fAbtuVwGDO}w=3&sTBV zlUq;^?=Yqf98#_)+7x$_S=rMyp5KR-C#SHog=u^ zPga?e%dVG8Vp_IV3I3mtxOFqbniY})r`HSqv_93th4&V3q=WO1X7i_2Dhq_N(H;A! zaGKXF@Y?QJu&VR&d_zjP6>6+MCm!%b92fr4G%9ADdz@qmJ|sFH*htg1fvNFvX^>8% zPS8c2t<9(rt=!5NO9<0DWD@=%=+rObPOp=AjYV0-a|Wr@L@QeZ3e3Xzw>ntWt9j%z zv%@?f$9#&H!gPKodE{g=0tnS`qHdWKR5Q^6V?0=~BDI{rPJ4$*yI3oJo%9i`E|+4w z^h-lC%l(#2e6{?3MSJ)#2Ouvh&Y7OD|0ACF;Q9d_!*>N~uRs@N!Do%t7!oTzYqiZ@hKmDc&u<(822X%CU6uKKT8|Os7gQGEB~4hK_OAm|Dm`8 ziLJBRC1oNKPWq~G9a@DUYKiZu5<-+%H{siwC0ly@+i`?g;}ZjXvrW?q3IV&kLnDkS zp%8-M_jgoblMO~`s-Ry#N_2iwmSd=bF=)v`qSEssg%XnMPlBrGd0roleXP?sN)u^d zTUGO|%ZD6A+Qf!B*85I@1y3v4Fid&($dD7!TFx-89jRwE%bf91(&SGr+BSEg{AoPQ&D#&o^if~tgr6<8g#&6aF!iP5I~+T)9u#aLNT5$ ziT`QXQ?A@HY>gMz2-A6Y4si5q2o~QTKYZaTQGNl*Bz_7_-Jbrm^r6wxMLqTNAX1;a zvt0!CuV{rvn^$qkpc)wf=+gmKYtIaRw0vGr`4*~ZFKT-%{59sIhlxx)t6#xyzW2~o zr6(4O4;*D`%ZFao78usSp;-O136GaS{w|RS6VVn$iQF{G5bjn^=-6myhft3}>Gg)U zJWNY#E!U*f)G$&kOFD5A?H;%LJ^7`<9$W|07_%<6Qupjx*wp>LA_(@{O&DaSyoyRk zLzkrYcdpr_LR)T>9HsvsdvE;~N7QVM2Djjrpdq+>a1RiCg1ZKHcMVQ(cXyY;U4py2 zBtRJ49lp-_UOVSL_Yb(wJ^VTiOn2{HyK2>{wNyT+KaIQ!fKpCZeZM?z#)C4*)n4zE z>U|QQg;zmEc(hNY23)?{Z6cs%8Gk~DpbNcsYfZ-QnZ?kd8WWHO2E#o^{vGa(px1eu zKJmMWu|fyUcx(rU;M=K?1-*|-okV%-1S+FnMP|8U(IZ;_g6!&|!*Edufigu6kltGB#a7HuND0E*pe}h8F~XY0 znCB#9+9PI9)&2cguF3I98GLqadhQdq+@IGwVfw*CogKVpBX3)Xe_q9d_6(^+96dq6 zhz+)rs^T9QoN?lnPqFLMNq?|$#t6Ai&x&I#Kuu|QMk6&M7!|_qAQ>7GW&Z%eMUn|2 zm0bUK%FiCo1@8>=zHi~sNGSI>!BaU!BVq5N0;z?eYVqJQ0j(kX4W`_3X%BE#VpYd(`h#aX7; zmyH`)@jc{Ozn=v7N)^gJT8hoPW?4VL0Y{)d0WlK6sbnj~|8W z*Oyp)epKol0<8D%A4_9PzI&+Jh?qTI#)+3WKH^cd=V&8q-vNkrr3PmTeZCL~O}M8} z8pnXMFubk36T`+!pskUznPX*mFm=Y9xk3-IFe0-+IyEC06;h=UuQBg$tok=67fGpv zw4@_6WiSzIz6;PPUp0}HwjNKK-%Vz$6+(J9*(bcV8R!l$f#$St9|Gr?NB zGLgx@HrYL1amG4WjABN5q)kCC9{$BTP~40?_qVE%Z0h6NAl@n+bpm+hqm%536?&k% z;Bw09*z>8m>-yv+QmA}&Xh*$l2aR350w|yo_2o+BWevL^eyRWxIcumI_EwfzQLyj#`To(ZTG@O=ruV0Z_$mMeSy+0Mtn>b4(!-(Utk zekz2ND$g$p^|;3ZMHTf=G{NuHc%K#}d^}jhqZd&BZErm9O$fe(!tMR(<-U_7?6%L< zA0%Z|$r47q$1GXN$NRgVOQTw|RqS-T=y| zA|k&~0uJ9nwdY>R#Bs-h8VM5_M3yC+y?2Nn9#3D*7mY-7x8yTuausOQ>|UP_Cv@b_ z(Pf5m6z_%}+28SdT%~Z_O830E<#nY(>h3_rQxtI*LK$)NTfHH-J518^l2nyGf8XDqr2o$)Vr+rO*0|H7rnB>WN;$%3<7$Nm+Q|H*^kqzxDWobK>WXu*MCszy8d3_6D{z+U*wHDkg?TLT>*Z&a}WItG8>HqH^ z`S0QEf1kF|&#ukOm*x7{#bH3=n- zr7I3CYP!4{M_F-srqOxYZ%x7%aNsCQF6Hok$C#xXg8kq3$A2967Ju<0i{754m)WM* z>7{yLwukHWz_a80H=}^teNhNCF>*Y#LOq@5*^I4?qgVLAWt=;rwr>hO|4m9fjnSA^+wqeY{ zE-BKLAR!n%)`Zr@3uSi{Hrbx8;?6$rM_Y|H|NHCz^B4Z{LN}X9L+p{g2Gc z{{xwR^#Eerx_!Qg)K>gmEUk2Z&YIYgxo1yrp>p1DwH4dlkyFw6?v=*llH2F2_eNed zAWn=Es~8No^PM0cpDiNO|7c(QV@uxA0k!qTJUO{HJvS4){B83 zXHWuY!s+cu|Bt2mk9!UXctc+p5MY7WnEE+IdN}oFY@PcVkQzVye|^FK00^Nr4*$O3 zbKi}rzGPdSMoa-i*ar%c&jEQO*zON|aL*pEd3aC!_E*gb%=T2_xU^~1a)A-8wwn*c z-$OA4epv=zLgp>QiFg!CLM?YIS@Vhe1;TNL`I>STtM#y>!~zlhZ@>85Ur>ri63lz0 z@ay{CUnH7QhYzl%a-HEhI<{3UbPPZ}x!q-ev#}$V=hRxB@&rllz51Jz%>&km5A84QWMm3Ib5(N;C4${m_)cG{qy{6 zbFI13;hNpKQS7Q9o%{*hSw$tE#VdEXXkCDU&oq79^)eGaQ>Y-5%4Skfrb0J8NNTEF zu2qD2X?nOo?DTSr;`Fk1>hx<*u2MH`u9U|2;NEhJ(ntiz5Y1VE3^7`_aVb~ukpDOf{ zBAvt_zdMqMiOjq>!bGFnXtgfEt-4sfIkgf-Dnw#D6r=kq@UlMO?oSMq=rDmlpbIF~=JK3VE4(WL^JpQ9N_c_%KUe*BlHVjpp_!dmJ6H5VQgyTs&C94}KcMH`U{mk@?lm3ly$5 zs}1%j&IdC+rC7`?W2fsK<(YxK-wbN=-Fe7l*z@6JYdW-bU!{cDw+HPjGe;IsJO!g{IF7Lt&aenze^ zlveqY`wSTNg?8mi(q2DBvbwd53~3M~sn|U_O1}6Q5TiY}*d3r8W3fxf%1NEytn)qZ zzBdZsl*#K4yV@3yG;vZ1Bm|b!I)`zH?!mK5t>MoZ#!OJ7y!t9X&MQOk+nd(_-{VQ4 zQn_@r*7;B?lq3mT-LBkU)bb+?kmYjH@ob?8wgQFXT}nJWy7%nHQ!{08WV@22UmM=} zJVz2udnVVfrx+~bFpKcRblG-kiGhfs3Atvn4Utt1hqMZSji+i1^3A66c;wdyT%OPJbJ71S0;*wuRPD@2 z*@-67yFy!$gW?j8R-3Zn-x5Kp5v|wn52JkAbA^uU6I1YXvqT|&H>MQ(RDe zt43Zv=}JsG{S#^;!`B~xI@4j}K;3avuK^T8PqA!fVRXBDeQ<)ZowJ&J2ESZ7m+fL% zUw)2+^QlL7^D4Sth3o!_K(FJwZ<%LQ-Ip_4BHlqk9tMn)Py5~edViuVeJz$R_n3=- zS>7<$D8a@JsJ&zNOeEkuKJ6Ira1S&d@ha^oI9=M_x1h#V0tl~iwT2p6xl7L5z<@1> z!*BI-=QI3{5j8z;ktInt%(1a#@=mY}!MpxYqPuszofBA0_A?#UF9DPd--g*LM>#E( z^IM%QmtB09FKkhfOFgb3Q^|Tsd2^+T=6cjw&yURG>1`k&AHt-e-9l4scf%A*9~I}j z7$6$S6!i84-B>NtNGA+wb^ryWQZ&i46Xjx_xQuy94N(9NgSzk|8S21z%VifiEeTbd zV${)cLj%_poh6qr3eVp z!c5#->2glX#aZnhbm)Qc2J`w7+ia4R)_KxW3R>Ov5<79r@-C2o`$a;LTy~+BXfLC~ zXwoqN3A&e?H1L<=Sgw`$yPyqK_uF+5GU2e?1;8ygn6s-?Df`c$h{w~RlfKC1OGMAs z$5G2j5c4(W6YKP%sQPPwR4DX>E;GT6@VGD*sHQ9vb9Ps z?=BTfY`~JWBtcYGjF!~v%wIM*wXp5iYQyj;x53t3%3s;fhjynh&;S(EAqqZYvGJ(d z!KAvwn>)Qg`%FGxk>0P&1mbEUyn;z6Yj z>I66Kgha&=DctcZGNOGe*K?*C%YBJa^alH11RY2>DCkkR^oyW~y~qSro?%!~Qm93L zMNvChZx2`M_O}2ntjuW829`3uEueMjXGM(YYOWkxKUxafdHzyVUj}y)t7`1G7@;s< zE^3Wn<07}xxp}+zPS?|{s`lQT^iXvPMlFt9j#8z=y*iMfFs45nEjG0r87zz1?wu|5 zl}h^^Ku&I|iV+pL_&r>z*QKjTOoO0UBwMl)P0&cQAoSPpvjnC$3~MQ{dlYO{P+oz+u=AIxdv6d2I`YGD)J8QTv15 zv&C*c^G=R6#1T3mnV7~6P+_WSf^(fb?b(f!+6oKe(2;@t(C`Bl|QAofYsXEy!$h;Vb`X%!K&l zam4jtbM1i-ZeAjW&c+>*3_*MOa=9;8jMGc0Fp|beXgpJs!S2a+yxb8)z*SbfKbhI| z;)@y+@8cNd@)I=uyjC|6q+4V5fRF%SvbfCDB zWrYh;9#4p3&K<8yAXj0(Z<3#O5D`qJtb2F}NY7n20>QQ#ZfSvC zHf#lKiNTQb;f~Gq@P<(=()3kucJUQ&R}~tryC~8!kj<18Y_U97CLOc?T=3)luTr(@ z#$CbOZ+}+6S+S@xlCg=7enc=x427txy}P`hobaiZ=4X*c4RUR`<^@)ovgpKJ`Ogo@>8W2 zM7&sB*iNN=K-+p&g+O{1a2xzHztU)3*cJgvJ;ff5qnh858h-k|WBHITxlq+vqEu_> zj)^D~Q86I{0Br5DTQi099uc_o356Ca^aMDE9@~$BWjUlAHTZ?{#B8JjW6wL+F_eB` zfx&18lH+Ng3W88f6feF>i8YFm91-Iw0vYFQMcZ$anwP%OhczChgGaf|>9lzn^O^o> z!@Q_!V_fUy>Q_*jpivcBv>HD-i@^>@tHX7gDWSc!40OtGYRQ|5Ud4_G-KgQ1BUn;F zfYHp<&PX(=!DUO3{q?wU*NbYa9F~99E^vxwD=(f^41Qfsg06^4fyDH1#I!I5GGDJ% z;<{{^C9keuK>l#KtmSbtVc=uqY*weKu38uopAd|1R;1!fxAS=*9A~`7eEmn^!SZUd zrtVniSETE{`lC%>VNxH!*!j9a1zJ@IZa~(??mA!%>-B>&KK0lhJQvr{M@$Z0qn~e+ z>YsQ}N-C|7ev%5^wiepVQ9Nii*N!ZyED59T0rTp_#mu~<-U*`W2$A5m$w}lkO>mh~ z26rvvAsMzkDWG-0xF++{%y|oXxz*Wm^}#%&9ps(B7Su_+c9sE1g{0B~>eh`abNEeN zfu4nQfV=2y*O+KaBIW3yMQs;HfSPnm9?eMXKI2}Uw`&sc@%&@-LzM}SxZfq91$7t? z4QwOsL?&Zzxo^1I8UT)f$P4W%PJ~4ZhvGvQpCseO&`cSSBc@pR6slP8#YQ8Vy^jPLK_@Bks(m+ddYsiZMQV_@{+}AH-V3}_@$$jv(%`V1lE#vXor$Mklur- zQr0#Qn*{TZkUkBgF8AnxV}c}V@a4$;#ohvv{tmL5BH%LpOd$M2r2o*+06I5OxCA5( zRrTYQ;#U*5l5CJlP=ZGsWvhJz7XP!_XgCHv{J_{z4V%c`;b6^(y^mWy|8u5pJZnhM=!^R~rxsn|*2+SG6w3Jg+gIhfi1%D< z#$$h!p?+I+?Xjs_0OOAn8P+XX2ft+D3ln(vWK*GE(9u{k3%b{*6T6M&k?%gdV04nC z!r`f?-q-1_aijTqs_i||#_{2(J9s*mPQcztcqOWcd((by55lDccRRC>9cBrgF00W~rsJYoZl~^7mD+_=ljs zPB+PY!=8gCMGsK4i|+THUJikFy?uSSw#0;Nr|x}3cfE|kpn=)! zxSVj)?ey9O@59*Ep55?lOL>Co%KV!!kKC_HdR`1U%Z$WjhYn_`K>1XaSLkfmRI-Hu zn~c+atR>f#Gu)DxSA>%v5Z}K!v@+cpPRRiOI%c!gu}rykJZY!wwHQ1#YHE56yJ;W5 zuwStA4Z|6}Z^MLi_&oo_&?W!yD}dAE_fV{M7P-ahA3{Ngo%b8Wh02DQgMzI-p# zL;SjI{!^d3?-LJ<$n7Oruai?4%d%7MrJR>hQ1BGFaX=jF@4Mo%js?uP*}evv@ULfV zXm0dHJ|1k3sa&1hJ6a3Wg~}u}=&|dVU*8-UdpOu&b$DAeJg{(>X@;dWU<{0`j3*G@ zsGTbizP>B?fc@UAW9Z*AlRJ;yz! zm~tCNOi5(>{;>+^k+sT9xjFi{HjK|3ho9l2DZ(Hu5R67! zYxK5nYQhkc?$9@Z=G_YImNdXG_A?+w7`MEVd{@`odLv`Nb6~aN;0=QxMz{k*XgEv6 zb5SmskF!&@74f>Co$N&5MCeV#;TG5!VknMc>F?uvXJ2gsMgI7saiLl(cRux#HySbC zV^2gWT_`IBrE67&VbQ$p@3O&Bu@K)v=fh9a^X0x%*SC{z>}W?*$@vnQU%_}+>qJXS z7SwmIP4=^R4_|*Ekg}c$!HcvCVb;>rn>?r3y&Y4X7Ycum&f0?bIg0c^aG?K{XB&9=8v~%At2acKFO6xpUB?t&cRjVyvn56 zVgXKyT*?6S!VA_tZ)PF$hlo|u@5t4S0Z%TXM=a%1L!R^6{?`y%jvB{(n+|uY*>Y3U z6D^c~VHKw47LRD-`cEv!{}EG(LD)#3Fe@1^;z95KufPveS#K^R}HPr8K)hiEu9 zU!gIOzdr#~lrkWBl z?kMIino?4sek!dtm-tUSQ5nl%QMR7C)xP&RGza^S28d{A|#u-+!bJ#(nFvg0k_GaW3Cea{PEo8xv}D8XvJ@0Hd9t zHsI)Y)SVgaVY%}hznZEk#KqzPLb}+`P;TU;MgV@x{2GErI`0E&98Y7NKDastW?w}! zd2f}|r)KQ<`s{nq>+jP{W50itA&#&TZnYV4b}nU8z8pWuMkmT+(s>s2NgLH?{7xDW ziB7O2Ni){TqBgydixKnO=JQNdi96CZZA3)?3BV07`_&{zQ~NWLcu(Oz81*l_Y8#?n zYqA@oYZmL*wsZB~@e18;P$A=@IHA|dTd_)~P2PIdd7-lWTc`U0sswx?fO zG(zSD-QsS3b_{w&ZF}{(mxj0FaUm6+2Hg9wlewO5=<)5(d0YrKIIqg5vG`En(mthX zT74nU!miRN4NVH-&Q{3ct$fajIbHyN4}Db|R$ZoR`bCM27!50$sfdg)T#?cn`F4Z` zaVF*+t6})T&gvg$@EGW2|K1jD)4e>Gf8{b=-3;6ay><7Z{DD#FgFQVxt?Lc3w)4)Q zBaxn$!O2Qx{%Tv)nFy>XKT~L`iZC!_a(Rmja0{z#FGB9hPNSagi^(PMJ8SBI?g~aD z;tyi30bpi797p}f7NvTp)Ap<3s%@*;`L{T#=P4t^c&f>{N|U+Bb1XEt5FB+7*m^a! z69Bhn@i$-$FW{b(djzDNW#rR8fk_<2=_EmDVZBqF4N zds@K#uw;8n0%LYj@HNRM9Mpde5t!1$uM1BJ0`xTIevu7p{Kn`fJdgVT8XKxD`6VV> z_-980ekTk!xY7U4sPG78Rk;`Im3iBO*@z=okYVwRNH^cZxTR(Hub*z~1sty3S~f~l z88j;MC15+aSdlX)p|3+-sS%@B2Pl3o8S|FaG;Y*dcZ_<}iS9FXT<$pZR4j_;|Ya2+!xxf?IkG&U;*zG zirTTfO&>lfo!09G-J`QQFl5zvuGEOIV=WU_+q{4fA3{z7`t|dgCc-W^ppa(m@RN5} zq(l@!H77#hV1)+CnnU^H)vVWCt(YZi*cZ)l0ukr=zPFBdJ-j9AVQe01$xK=K#V~Q* z5k<-i2A<7D^OZ_szWzZGH^GtV&gCD}p`I^BztX8z#2C1u2g%&UXLD&1S@N~7)uWxEeG8q4L%XS4qsb>e zB``m(#}Ltgw;)I8BoHCy8m2C=7w++ZNsxaRHy1Z&uGgK?2#}!6_q&?6Ayh{YSpi#1 z0pMTmpXuj{tkZ~Je8vzf9ZXcpv!8NWrP8|>E`V~5`Ah+pEbsJ^eS8Cd#xL1O-BKTEP6QNN*4xz7=q|7K1EaysH>Tm9VePM9#6Kl`{f=;rTjhP^|HCI{jA-xZoC*RhNAuJj`Fh$P?h% zj~l7%A}lUm0ts=9o_MLh|FT_sWZ$qx05c5MLiMzeHRTFWrbf=rOJuSwB7-$+d}>*Y zs9*x7PFsVdBKBAUr1Z!8!&|I~V~kDX-=QKaPJd>c-!I6y(Mo_UXx&8Go$kZ6!P=rN zoX!{Tj=vXp#VRotGH@Y(qdKW(4SZzB`4R&VRza60sMpTFbD7;z#ffNW6{S8b)@E?| zhP^mMB>zqv#kXK?g1p1vG8@|Rc`$L4;I3BhJA~7f9!qahKgCWXVmB!fAq(PHw+E!t zV5M}56tc^UkFl&*8!(XDf3}JuU+3r(Fl!f5DE!V8xaGrZeY%_!^56xh+)LUmMgQ?U)XgV-+imL6^o$#DFfoq+CF@DA+#OfQ;yeDRpw15pI8Vwp;1j!vJ0 zcXzkJ70Vl_rtgpqAWg+T*8SKgrjJwzUIs+@+6qLGjR5s+HqH{gRlD)Bq#cL$8mmC@ z>;&5QK}9%BK6$!=nCNAB(}@&e_sic&^KV5SuE%b}?nV$MfyPWq1nMDTBlGSGX9#86 z*x$oh4ptPbh@6v~qRHZ$bhb-c@En6yj;AQqwvdZ1TkBqvRiI*=V<%(y&EiN^0E~sB z!OGU4TrcX)8&KmBm#6t>h`)uQDkZ2|t1EVb-X8uNam(lSa9}Ng?vYi@@6M*3c00hB zBhe%R64onDId!{bp#3TVL@3Nk3Mnijef#oe0SF94&fw>XHLYrU%?3q4&1m<6M^Rjg zkZ&x@auyYdVUeYDDb3fsKZOG_IMcK5UV~B7ubmwYPdBnZZ_SWi9mRd5X|F~h;T0S{ zzSHCf+8)I^S$wvbDi!LPn&1}+SPa1PZ&7P(y2J~V#K+PmKH>#SJCQz_>qo@z*4(qB zywHcGhl+syKG)O$zQ4hI=FefqUkb_{;?d)QpN&)i{$2v@v$lVwhu@n{6g-CchzlBK z36H(U&+~M+X6q}vu%l%C7d;WFiN8k=n{7)@R8^hF-9dR@%0JSO|)gxkKGE5hU<>?zy7l6O|x2zisUf8!C^(wa9mx$R8So0c)T@M)cP2_(|C+V z4_>HDTh>$*W?iZaRE6q z(Igtafg;2iP1lfxn0Zv0C)+aRX@gRjrmrgcU#Tc_BT}i)S;sQ7ttKsyD6KNM7s5Al zzI~fd_S@^xsn%YuX5bpU=Cbi-5ON>%Z!!;)cS>%lbXn2%_(l6a-*6HVh>YqLVh$deu#v1$bB z0d((*(ukM+X``o}aApQi-Ei5}^Ijzr?yZNhJPQ;l1qLKhQJ;Wbm0|peP;VGmiCB!{ zhSE(QZz)e^0(TZlOBidTyj&?Q>Z)mUdk#d0m80{DOOGz_>1F7{wal2rf1KQ2&#gb- zc?ir?9`C~-fYsltdQZd(wA}?9HXq!c%x)@5!Jw4*z`$GT;hZbHM|YUc=c|}8j9^~g zpLp~>{%)C}LF@1RE8=j@y#DRF@ECmkw{b`fXqkz`uvW-c`o>BFbtEaP;~{o_(T*lf zwOXV2wABJU5{w^Um6t71IIi3$``tl|=2Rm3+;LWz1cxNF4VR!mw`0YF3q9P8Dqk{s zu_$7JeUlo))^K;k77X4TP_w>_$Y@G2LP%*8$V_?A9N8ORwW^$h&_8zpBLQ+LEaL2d zpR-|Da$zd_{vW2yAen_W-=TLtomSYi~-BL}nlSFgM8WbPdnkYQWCmM299T@QRd zRxA6)ntD7>DT-7wi4V3^ab6LG&d5JiM2m2ol0OrgOpOrK?kiYpy-t?3kf8}ESfb@H zz7GhJDHZNfl=l_zL`|4yOxD0DH**XE7UG7Q{=(rxrMag~o{oCSHnDVgy%F`{5M=@{ z&~ZsF2j)G}e3`TO_;G=}@ggkRX2x)44PPZSwMVK1|2pg9Pwez4)Pl~t@`RlMB2q)+ ztk%zK9Ohx+ha(e~q5^2?O;*JOoP@|t8`Hse>UBD6#V8RN9~2?;w(^;6R|}@w3HR;a zpmY6&3#O)|PV&DC|K?_;H7LxB`thb9M?XBPO!H0STfX$)sznn6LLna4>4?}@V^=%# zGHDzJ6(nnfuTvgZe?(CUKFz|TcRA3QW02BDlmPmfPOKd#w7fn$1yJ&l5EWWLz0`xG zi0~&mGSX1{eS}&5Sr;?K)5vV0Tz)E_Ew;$}+;=gGU}?%en5;3$u^&LII=^zqzeMwT zB|%V!H=OZ>Cz8m3BR6@~t*&3pIhl$0iq&Wp3MRy2q~*vEP*E6|+R+n1B8Sr}m}6cU zlH^-&b1iDUCK$E&D4}Y~7`y;5W zbqyL`xdDIOM}F*6W2)uWeA1db?mC&8ChsDMqKEV_Xye+lCR^jtYOAY#+}2+1dVNDF zo8o78!eOPZpqkHb=y5)hNRLe4D5ptc;=_NlYH!aV6JhzGu_og|P3g-9UXN8F4x^^} z*vzlTB&)hE;Het~bwfC#w#M8rt*Ca+m8*VlV!inlB$LehUH3qdvcx{W^|utrp+Egz zj3(d($EFSRb`7uAnS}?z@DHrA&8Ca`_0~(5**S8tT&XHsi7mBMLbIBSa{xK%2(_Ik zhOu3n<9?RDtCrH+GGr1*y*X|vPyn~mQc4qQ*)^a!eJCgPwN%sm5UvO|EB{*ti7C@D zsrNVA^$;qR-6V~J(w~mXl|`~T=PPCG$%{GPeuJSxgJy?_laf>1gmGcR@f#r) zNnKZmi)cZR>_yv{`Iba_H5W}L6E-3~mvEQOZb{iEz^lY6w33RDlwLamMhWMiS^yUK z%oNNJq!wnl~~ z>L(e9A%t~5=}2{Mi9vzwf8Fj`taxPdf=k3NeMpxQ%lA(f`@=!>qh25ME!8s5jftq z(8}lvK}{WqMd-Ki?o6WRtsdi=IQQ7Uj+mzXMe~a712|JXeG}2bzu`D{8X&q3Grc1zOvqsl!z3{r| zUT1p^x0u-bGu{P8N$psv=*P*kUa+AUT_Fj-UGsA4BPNeSx1Nx4VFLZGR0g+w1N!H^ zXCb#ckZ&VvvU?p#!Vms~kn+|O8f#X*!UTlyh=MUkFaUAF5c52g`@EnN@Q0g>XZ%TG z!?s4a?if^g6<~bT;-@cVGuq>Ko;M~KfoSv8Yb|(EC#inOsWvRvn3{w&-Dd z10S=hyRWW&+PyCmbYD;7wr?*cL_E9-=sP^0=N8*euU`OUFa#bQB1tRsFf?@oAhz!o zYU5O3gR=Nc;pZ#0v~DxG)OzuI$-X0i&<%*cU5jI~h5{KcMFRA-^{QX_))W~G;yFdd zO1Gdv6&jqH(Y9!zkKE^F(#0x+ZQPy|eC9OzBA3R-GMOxf_dsXYA6Q~mHOTCzM0ke! zYHg~hjsdQuh~{ZAsjZafMulv3^)X3||wQ)%Ms-52y3R>lq~MHmZ8-tnO+= z;9bQXcX`2<$XSW`x=ckavjuvfjP7KuMxO0G~&T;(wt77>}$@ScEB?O)WUA0RwXU+?t zSZZH4RebGWtJLV2+pHFO=Jn8VkNoqVgkp}h%*b^iP*Y)jWBHRmqtdUE6>wpeYqf4? zUAaj*x>{YXG=>lC8@z?)^JRI~okUc7B^%OqCRp2op2;;s%Pp zNbZnVCpw)@>$&N1Q&IRmlA4qNgxB0i;cg+7z{XeyUu&dx&&L2=A#UMNF5HcRz?!FS zbaYNP^=M-1q!jC+cUIbL{)(k4qeXKC_Ap;>qgZS^6(+NV@NK>#A?f8}QA*R4uP~hl zp(`o=@<+5@!{6(&!<5Ak>SS@0&n>j(Qbn$ji>E?uAnn8v^tnjb8H^}yF{7P_&iuS} zZQE%sKAz5nmGsH=AS=ZHZG`P^%k$y`f=dmFShg|WU2D9$-FF8W?=STb4%x}u<8^bD z-^}RG4ysKKyU4x9l>uGH6>QoY=+JjPnQ!)gik(y9v3G2(32Q_5dpJn8 z4-lb;f<9g!ugdhxwn^D;K1L*VN?5 z%=AoOGDjJ#BhL;+wM7h#Sb}Ms z>g6Hbt%o*Wb$%8GA(Cb64!L}uS&&(LxgQ}sTCphS^Nug^%dK#2$APC%vaz=j7dGTo zYD`&eLi~sVtkH%JW0AQf|7)N@N{QUJc^L=C9`jrT9;$oybp(KSXcK%g8QMxTU%Rh= z;(tDl=e3Wh?<}?Nnmf+4{$;Kqk^63d{ku~Lzug&2;{AhTblTU!Us1TRI=>+A=06=n z$TziqjIl;#9Ai&XLcpdfw`q1fi92Og6{fmA59YdMN^S6!+F(t3pe-I{eP-*t3M{LV z$4d!<$+a@CHjCI+2wNsYsc(e4RfF#<^F!nAs5j$|voqM+rmt7} z`kP@kTgF^%DQelabCe%$<(b`AWF<`lYTDN_Ho~|2jH+&7SKn5wXfV!={_=*nko5OT zDtfVyRX^6SbX&QUDiSdI7*C#GY&4j|K&GadG0tm^s2LI!?<{=(oPBM14>n)V$l>UH z?qjgSMlVS@WlX_k;6O2SEoHNYxXHaQ*&Du!FTTM$0NqK|)-5C%_p~TKI-NYZH@Q?! zfQUc?!R5r;eE_Y@NB)q@t6D{e%%omimYnarZ&^jHv z$N7}?l&j{jOccwSQa)jbew%#@nzm4*KDZH?c8?C1r1^?8o?ne68z*09gB9OnUo`%#Xt0^n43z7(U zRM-|%G3mBeR4K0Wax&m5mN-52(~9XRr~=+AMQW1z-_&YBSwgz-vB zd3xrcAY{B6fZ2&k1A9#2dBfm=neZb;U^?}0tKYlHqvPf~ToSwc?MY_6;cA_gbhdt? z)5Y)itF1J`cCJqaC4-uK?v@&@PDmZ40PC&gm@3#vz@V4ITZ;nddg-g*ej8Z*p)BJK zJJN)$a&M|8IlftAAJGb7J~jn(@!P39Zz^6O&FTK=7Gfd148`{HV_EgxE|*)aw@WS` z{NQT*l!f3+q`ljyUh(k$RKco36yr4eNfV9L%Wi$fa)9VB$_`{hL3{b85YLP__dnmw zrsY~bvxei(UFK0Z7Z{rahJK}W$m?}TO#T;F-wz!-lz3HORQD~J$=Ew4Ni0cobY`uu z`4Y8j@XXigpWw!v(NRV1$>EsGK^03#JI79cmcEB4(&V>%=(V0EG;|T~biZtzHQCN{ zRaGz4Hxa1+5YvcLzZ*C5Hb-G?3DkL@NyYyBXGM5ys z9X4Qg{2jo<`30AyAp z-D;@n_-|jUdQ`RW0@ot6*?aaaGG)27{;q{_sf7x@J5YVp(eHRsVbCGgDAFktQQh*s z=aH9{?$E~&A5BlsLuA)^0!sNWrNIqTgrS*WK5+%5)Q>5Wb(zm&`Bv*qD8>--s`MrQQyAQrg>REIbC0JvX=Nj`XZeYT_75c(P& zS++Zm$C{qUUg)=xBAy3SC>~5d1ud>9e3LA>nW+>~%@LzED>TTx+j69@jy8rdqOqj? zH@x8^Gy*2?S9b?w)Ed}SSP^$E%ai1u=C3*W(tofOxMn1Co2<89laC!CiV(@uIIK1v zTG@e}o4Pg|PU$Af*lAgCG`2IQA z)26+m1*xGulz`p`*hA=yt;PM)C0E&zCR#D{eJu-Lk}?s8#hP@)`lb1c7Y>gD1+O!N z;ThoREoaP9nq*gMm-359!Ta-pA~jKiWb_5)!r&K28MT@S6SN`;7O)@BNtto{7g(8w z!s3fdtnMv7Zpgwjy6oNA_kq1!Uvd=Y%fsFuEj(zDiIjdfy1Z7?1FbdN2ky>wHF)0Y zdOw-fe?HIC{6X!yf-XzkPo2(h|JM0vO-x8aD7`Bvky~^{HNO2q}TQWV6$ai zSdvMF8G+NOTyl66Iy=ktveNRsH9E;3)ji)I6u|^hhXXc!e}mnzP|kg5$9%mcJ^}a8 zX_{z8b(&$4k~YjiPyl8;*|tZE*L|4*J^9d;Nxf%1ySe|KQ8kG6 zg9O~{T@x0={Jvgfw6pq>a_?ma; zY6+m{G~TnF=~XPkKggfP@+PEBnBorwT3spn{o?8gMUkMyr=l|Jf{pIw+EfjxHZ`N( zQYP}b${UENgxmYEx^=x?;agOfoHSYHBWC8-h6?I(;9%~fqK<_hClKKb-3 zQ?!?0R_76Jz+kY{=cN^LR}w2$qIL?UnGSN%91sc+wb=U+rdt~q_bh^OztPc(m9RGP zcXv%ZU`(-%Jt12581xPPkvm#%%08=)>-26_8paZ@3~%5!y*0a(SgOXk_6_(4zD}e- zms`5H_MbvfflAOw*gqE~eO~>i3uT$zvOzlbC!ubiU9i;pCDZuL-}ZP9j(}m^`-0JO zT&#&}4J%gI=p8B`-4DRmlA7z#k{q8IDnnaCYI8mob~KTjHe^aY{0s(Rq#iC+QY$1S z;4!B$GI&V8FVmOk;|kxYvRO~_c&#%X6$NrQ8ap!gyBB&kv71vpygtRH4vjh0j~V+;_>SfkUz^zLr8jpPwaWX# z=bxjGfzJO>>>ve4)xU286)hxM&3mOvbiBS?qF_XFTwvNQF-Nj>Nyc0qD2|%A9l-l% zzM7sgY&FnqaKC(!c6D`?c9lZ;sD6h~D=dzKi6PGVCcOp&3Iz%qMx;Qb6>x8A`z_QZ zV6y3cdsEqVyR~zB$K&b({M_)>dmf!;xtpxW;^r50sa#|wjqhGBdfW@3DET$0P2K7J zL=^n}{Qkjw^982Pc=VIZH=3MwX$Kmz!K>B_S09=|yUOn&Xj-+&@)_KPpA(#b#-HkN z66Yvm>Q2C&if3Z-g7Z9@_UwUp;7umKvnIwIZ;VchgNn=1Qfa%(%@=?K3JcXm)-67d zM#@7MfkmMB-Y z>;th^iAgb*Lpf>G54nX7rLMMnjpZEe5S>q)sT(f8B-uJL)&5B@j+R zO_4ufF8ZSAM$6Te>lFoOA6)_tiv!9p&OQ)UWW}n6l=sPqpoDOlj)BIwf7$c;eLU;E zRFDkEbd;~0+BFGPg0ZD5PCbv$7;IMfFJDfKde|!MmK|rPem_sst@H)yR9ys{lMEim zpY(*YO{6}}nQA1iUp_ElZ9zQkYZsSH{%LsY4=eY^K3O92NmjK&oe=;jYj*w*b8j72 z)z*CvBZ8!Wv{(jXup-F@g%>L@K8(sAemhkCd7x%cYx`+T4O z-~Wz!-0SSQ*IIk7F~^u=Ck4Pxh&~?KyH`(@Y3t*cnXPi$&Qn-`bCZFp+pG6sAkSa_ zyCnyKOq5*2>u9m{mBsjh3gD_U8%pi+qFker;p)wg3A)ve>H#DdPdjO{Dl8+p(v}HT z8yb}VSgT)=t>SNdWgyz&8O}5#R2}{_#PRE2`WfM4Xz`k1(UnwzAu=LEDFr2Jq9l(% z_W%6a|9HLzdPsiUug}?>C>ojZ1ss(*mX}EFBLL$=zh@s`<8{H3-NpXftTZ72B|ZAZ zDih`R6A_C?dR;87Nu)+3~lN{hwF=H2^U=FhHCOZkF_a2kIXe)RRcKx20E8 zSs;S{F#zSS0TSa(*nh8wKYt>b(61+v=amfoKTS17;2lGN0eB{qZK(gA5&zE*?Wcjq zEm~K#KQ;J2Eq0~I;;?`LB>AGoUj1{^f0#G}5BW-*OxmaI`7+x7b$yWo${P2TN7~JH z^)^eLs^f+&WV@3^sl)E6$ufK49~@AWQe;|u!TkRk9!F3j0QOKcFSzYvHLP){WM0J! zxa0w+_QA64Z~29%`w*N;m%v-kqPO*7-^(5}as>e00$~4{=lT_CzA>pnn{8=$D%RFvCf7+^_j(cip|?z~Kn6#JrYt~mq?QES0@#Y8 z7lwl5V{T5|GYUhPw2~!Ui)^#DdUA)IY7>CDqBTcrgZ=wirX$QZS(3+23xk$yHi|3R z98b~rvF>XhZtLUQ@o+p8e-5WET@d~=JISb#^)x5Xnhxh|HGLm^8H*Gu%Vzx6M6VBh zsIi%VwS>|P@!bA*u{wr`0QF-8VLLb2`E!2$?e4z*e!l{ZL7?S-y0pKab)-b$uG9diLS~eAP;(@PeW|m<+NGG zV_Jpa>X;__-o0hdsdSlsAK1~HVCUK>VQ+Yb)LCZFtUixm5a#DP)b>jH)R+zy@g~+O zk){tcgw`7d*;@0!n6p7A%N%bB4WY)hQRaC@pit-RE35MHp$`3a{0_$yCfk0K7j5GU z9Z!d@dIVQHWTS~5dC9zueF3KUci8~DsnP6_FPrhY+K&da_PhLG%!nYw)8RW?NaAP) z8-gnsILm+ds%rjq*%xVZy-0S3_cKjA>iHtb@-YfD<*Co2d!Id8d!0WmKMuDB@*@EbEv>>VV{Cc5C#z!3s~O zt1qndn6}_V!SV*nY4HJl6k>Zs(eeJSlCNQ73qP+;k}r`Q0h?oBWyr|hwQ;_(&(+gd z8h>bQ^j^5sKi%%9t@9mv=}r7@G{HdT!)uDeDYc0dT2-EM6fggoY{XOWl(vTFlzP68 z<+W)qE*`0QBBR7o(fs1(SB%6x^x2kPLDOu??yi^2ErhTV^NV4Xb`4H?lF~g_d}n=h~PyZS+g>0vws zRhQsKb_?*=Jb(Gb$_2M!Ku=#e$wCb%%P1MMI8hgz39I~0Tn)!rRa;d&wOih|%4uxu zL_|p=JVIVvlaC6;1jKHphc{)4+mhkUegup8AXP)J{Q6H)jwPcxB^o+XvO_CY0e4(kKv0s zMOAv#brOFWPX<%sy%qkVX~+|Xm)OH+W&83Gy1-Jco%IC&yHsDTk_BMKL4{z8_#EfP zJ~q{Q9m_D!^TeAX^QSBHR6Ubs5wQ2&k{OdqU`wir3RU4$x(}(Bn2m{mikL6@t)>4I z_r|;HVON9gUT>q7`{2_)u>Wuu9hqJAOT~7-V-jW21bpf^68=ZJG z>$*I@3?tu^R+4Zs9{)AA{gS(2s7`NKeRlm47K;!u+oHA{pvtB?!)Hi+E2}Xay_6es z%Ap+dc+9d3)?(0P63f{{q5tg*@@z|+!&(74=|u-DeZ_9-NV-5j0({wwt#)ΜzcYKd*AYw_!J#O0S@Ux^ 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 literal 0 HcmV?d00001 diff --git a/docs/reference/ml/images/ml-anomaly-alert-severity.jpg b/docs/reference/ml/images/ml-anomaly-alert-severity.jpg deleted file mode 100644 index dc6582ebbd84f35b40654ff16c84391edf7cdc70..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 322311 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�YEH~ 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

4J%U?g| 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%0A1jGZ#AY!R;1{oD-QAgr#-I^H8 zx;<-fo5M+q%^RgO`ko)*VfX_UJhO8yh8VYh{GD$?`&J-5kHKS1Jr&x8;9)S0O%&gI zdYsf574_qYs@%97k?%>u^-+sRy|U9r8O-s8xr?f$-j`=@XvOuuL^}dZFB%b88 z(Jl==$9Zs*FA}sVS<{Q|>l{((muxXSot5uglJ%7KD$G5d^_n?b{7Pe&z>AkSqmw#vqNe%-mtK8N@L7O z9T#yuM^V{u)OkuTRspm)6jSh2okIm_x@1;Nu&EFRB=N@1lNBhrTXiFKM#dI{#A45u zHlP_sJ>n!heAr$lI6z@_wP_xjh?xB`t4oZ2Qd_bU6$m>U@Im&5r*1& zAZZh#_pEo~Xb^H!T%ClDE3PnDS}wMeO`8#@joc&faL1*6N^cSuD>wmzW z>VJpN>M7#qlW6VG#~pdUT=n^X`YJUHO*$|5H0fZ~H0RlcFWpmiuit)}dsBv%n~{8* zkmDND0VFoBl07ko?<7fk8feyS}$F_vm=Oo_}||&wW0sJ#CpyL%wQ?`&xXV! z(LEBI81get6D5R=RtFuWC^YrGgW*JT`^N{seqZ*#YDfn7q@B()i>E#iTwBM$qw~Q)+n(&ymX}fZwkVF^Jr59sC(}<;JmerV4+jZX-~x_0iZ@+py6}ybv+4&!C=5?|ejbF6o!_kGo7OBIocfWM^1z-%`%Tcip7e zmxH4g7Jsd0WE4IcMe&4EXT16RUA~|m?%UpCkv3%3>ApYl!s~Ivy_;5|R}&h1QtpH< z0FMuZCT=6Vh(Q!Gz!8T90{`y>E|)BUN>AT9WNDL7WRX#sP-G=}uTl9-QjF0U zcMA(MQ5iYZeM>}5hJ0f!l6ooh1Dw0|2A+c!M20tQNyj|OI+=@H6igdxeRaff+oYkz zxz0D&L_)8{yQ)qF>p_5|Z+av0aT{8YrvDtW)i>l5tRGl8P*TZfxN+&d@&PZeW_9g? z9e%egx09x3s+v~fpNjr~-D@Xs$Ee^0+;>=p6Gyc_s|`+iINb<%UG`$GqoZ3d0Cxmn zH624cK!MLP4jxsn9c)Izgn7@I->Mv{sL?*=V)6BqoskGnh)*nR>sah(5VDxs9(;~8 zP?ew~vS=Z`uXn}rb{*sKO|gmc!iD6P5AizphDdj*65uR~@bVT0aNNRheDCiuB)#q82z^To=3_k+vI@q;gfq|@nrkSbg~Fx`R%lSy|i03B{P8VvY0U< zUd+;e!>aDh-tQK-o%Uk3YleGn!P?M8a!{kg!J+p8!d@x3 z;mJusu@jMdGfttWRUcEm8U5k{^!ors$}LPo4X$_X)*rZ)$$hE%WA>1j+2kbkQbl=M zP|)RKNn?3~-n(;pGkSYRPkza~v(+8TI1h+GL7m?9Dh%2i6CHY-J`F*8zmz+ zgL~v)ied8B%ol5kw4?MaG~$URFToKOHa>pH@gP4mAk zDxBs2fG3s#N^2K@jtnJ>em|6lwr>y&0z!0Qa%at8MS>Z}w_7hiDENq8XzF-%LY>_z zX&u)L7C}8Qh2aIEYK$HmX`(yHsNF$^EFH04XwiR-*@hlB-o~!>j7D_T8S?bZ zu=OLJ-D3w+EPRDsgLM5(8u6nTiVHovz88SC>EzwbI_lItAHc)HEDTm-A8^AFBe^Nw zI&W~k(xMqB_$@mFvBY&MyIZ{~i20(5XznDbyy)5~3F={Q-f!f|&9`kmQh9t{JtDsI zs_VhZr*E>`*;aRo#)7IP^9QRtCGu666a)zHJs>rM97(769$TcI2-YRF-%S`@zT+Qm zS@CIC0hdzPjMc_2avSOmAu5I4C@)C5oq4cw4hXi?kBf9vi$fC=Yezwc5r}SUA z{Wsd)GpxyITNg!X0@9n(qJV(XR6tZpRHTWBfPmB}h?G!7y2OMcMXI2HqJlIjg7hjR zMw)^Unv~Fq2m+A=Ap}x9)3w*WckO%6+Rt;&x%X%C@R9k=GRBzic*i?fF$HbP-IxaP zbsTBDI~{R`ngXN}BsddFHrv{jt-cX`{b9JXX+A=|wbIM6pXfcsruoPbOr3usQgs%w zkA&-l@Ota(jHG}6Kw5hL4t}O`zhTM`v&&DuMxEC7yO^9zkd4&qPTU<|R3}E!j?p3H zN=yeV2_}M&TR;iUu%G2AS|UkGSNH|;Oc+w)G*^C$!;KRfboH9nT<*QSE(YgY62 z1l~utq=}>Zx!qO(H`735-vEBVN>|;%S?&5Bqei+95IGgOHBMm*|G-r{FY^&dSw zuu`S`df>u2-B&jEf8K+dtW>?EyVKm5pY!+-qQ!_S)EKh$CkgTR;Q5}b-?Vt|_u_lE zj$=<+$iI_nmg97jlGsM^(XUgz7XPrY@+Zm&k;Pt=hk4~^eD@y94zO^&ouu=fcIi}_ ztFO+>5DvZvH=?G8U_YwBDf$jhV=_IMQ3IWSTS+@gXQ$Tr7tvKler@^kTxlH%&i8#Y z?X+rnVPwu_#<3-3__&{hcF0{5w}dCG;nUBZwm660LNx+GWbRy-c zUEHRNRb1X15#FsqIeHUk1be^E%bNQf3x;&xe|TcoFp#!VfZ>A9$K^FpS_o-WdG%paE?C#$W6Qv&} zUrS#ks&V1R%=*RlBxymfMV$-1j)Y!caK2e~ZeLXQfimw7J+VZRvm~XA@m$xKERE)) zMl2e~R?$IHe6lg#zrw++j3>!=Hppnw{q%0S&%?E*`^0ms9PMV^z!=+gr18##^)1TO zxW8GUn4uSGmd&YSc+u@}cXvaX?}-~@281?RWH-=B^B}Bk;{9{7{^JReaOvOhbE>qXLMl8{MTt6bu zw<_cG73eNh3zBt*D#0Qdtu%}{Rw%~?XLwU2d2aQ*0lP9X@9MZD$)8dPvd*6Q0Qdb5 zD|nuEXa;;Zz^sIq?Ou50&g_?e8NS`c5>YA2+tn%dMDN>3h2`bqnQ5Yp&G!ro7SNNd zcNW5E!`#M+QJ0X1sCESHs0OG|Tke&gXV9|MIlD#7`6$KHVw9;ImOXUhvaU0MpDT>D z!!B2Y{G-gHz;iF?F8ssV?l2yIpvN}Dp1VM4+m$`cfQiS{v?2FlsO5iH#M&gmkkHw> zAx$|L%69gI>hd$8bW`Z-rNZXJHg^wpym`a_w)DLt7GS=1oj5Kpram>G!agkK(zkra zBxz&w=G4{UAL6tsks%Gavh`jQt_Digw_bn;d1I8pC9->Ic8FFqsB|BOdj|C1wsqGI{S{z{v3NCwLt7b#`#?^`2( zSVUDzcTfzLaqF;f@9*`ExI|SXIH7MOShmkA6-549yT`Oa{q62#1c`;N7yd-;`Hima zl~*mwPyC}1M~Klc&%I~2Fe1Re~uzqqNr+|>qHudJwk~Qr@vo4na&9h zE$w$-G-*3mw%Q~UG@>yeSow=hNb|l>J|e5<5?3i^e%9Qd;`vtrh~2A&+D8kdR@v22 zf0n|}SI#EAotpqD{m`np-ROmqS0>VGeP(A!0}y^RZc-BboS9&;F*MfW7f& ze%jMsuOndMbB~f`>Wb!8?egH&Id~l}_I2Wk_jm5iN8UqP6JadKfd{xA3#tl)F79E# zDAwnt>b2>dDJJ*=_&hm;{cY2bCQ-pGB>YG7#JeBiA-l7^xxpQ`FQ~D5`F?5HR-}vT zFm0j3plGKCXd7vmlkjfJ<_h%pTu5neZ&Q_sv@&}fWiLNLtlO{accQX7RR6EZ^+HbStNvwT zfTTzQq+Yw@?G3VCQ=*o%hEe+H$+#Wstg%UjjyuBpKaa3nWP8IVV2->G1}O5kU@Qcb zdx2roF979pZ}o%+#FS)->DH@uV#(R(v-00PFKlVf{SlF`W@#H%x0#js0v$Z{8ySVd z>R{adut*}OQ6xC3Q#}gCGf#shrh(st88;r92@HK}2ETvAxs&&Ifv$-ui%#@44H!EL z%Zr(BWpaTb8zW9qqiAZ#ky%bMN3nZAWq*bJ`)nz*hhF8omiXx@XM9z>Km ziE4aXMag5O`cY&Xn9?3!=5Vg2LmZX-v>;iA+0V2m+^p!m*T1SDr0 zNjr35<&QZPE(fW0B-*uMF=qFSizKAcMb=E}siEJYhW6L*E`IN<$%qfPkuQZDr3+D$ zXdoMot)C;=@fv@G3F;4(hn)$RYxqV;5KVF-b&Wonn)?*LM7w-eEe5LcMMaWQ_jJpS zF4aQXzhlU#M*!Q&ZbA)wJ4Ias*(q~vgHdZsL$CV2q<4o3Q+!MAbWi-Ml#6{*V}0cG ziQk7iR;BqL{bBK~nlxh`=qI5lUUk%X+5zT>1144f#L83qhOtc4TW1Gs>91f@`C9*K zEXDRw(<`=dp(TM*Us@oIL8>!PWAelTg_rd?Y7eEHtX8L(i1BzqzZle-S&@Et+iZsQ zN;r|1U@ld$I<{k3^p*RIT>gF9W3AmG@*0yn56m=gEmgf_6G^AFk_nClReOBM({Gpt+3&;jLoybT~Nj3HgdC6vwMqVN`2UW3KAQH%Cl zRXi)`0Gi!YJ;4heeBvrw(y@WAiMAhtlKkza?fW1-Rdh-4C|e6v70lBK>LlSD^CTQU zn}C!wE`m#-C^{c=5yKV=w*LBuYx7zmp9 zzvACnz{!F^{ahZkCNV$9@6CwL+<$5+D5#W~9BUdp85ysODrH>Vk@)W_JvfgMjJji> zjX1_ZJ3&vX)kAnaIUfkD^I~@_z*k(J_uodUc^WfMz%OnnG9%&RYu#=7lVq(I+ow@0 z9PL^E-lG6-15MnZ>JpeH{vUBPzBS061v}y*jTgMaa2`L(6*i#^YPf1=Gf8QdWXNM( z@nXGb7qhZ1xSQr%dwIJfhVoHHdldyGF@(sDycGgP^iQ|&9uJ!-G2 zXJzku1*8rN+X$&$Ci2sOg~8sipo>5|TQ4`d2$8yeHC3+Wlz$wYnDO50)D}q6u~q!x z9+8##%`Aa%;r+#QZA1Xg7x@hdEKzg`3hUGW_n&fGd^X32=3R04QCRFX4)w_0(^EGd znH&F4dM}#$VS&;P``OOwOjg8Oj2g<*KZ*&c6VX?}knc3qGg{E%K$Mg&Q}!bb=wI8K4o zi#05+^(khA&u-!)oQ=&|8d zV&t`Hlj1i04>}1VSmF*w&5E1k`S@C%3BbNhaiC8r_8)8ZS$R#%~=Mo7FqE(3;@nFxe;H zj5;57CGBjVRDe!ouMH1m6h&32NamTU2+R~9tqePO!FaahN72$e4BL(y zoj9o>Nrf|^)~$}l(8)w)QT+SWS1g6UV<-Tmi+QK14p=&*t9m>X-w}jpd0uiZKWWI!p8&%cOx$1ZHz%Lgxgbmd&Z(S>`gz?>DJulI-?@57&ArLU)XGZgh0&R# z*Rd)0H@)*fJI@6aRdp$^(;^Nn{$Zp|F|gdrH>7UgkzrkpqUU3#enl7D?B8AvxiNhM zr}X${vX-552&cvSpCNO{H*B64+~zG(DfHT;nZoTdq@9Nt_G}n7PzEn z!?1Yf0Vqx9AFR5vIl5H~8P)Yr9NU^wKelOqyhftCb?Bm0f&Cq?yPJM@Z0|m~>my{t z^^u+nR`A&$mZfv8+KiYsV6+Q)Q6Vsr4Q%phCkDty3S$<>*9r5_gyFeI2aC=-sa$$o zRqbN}2ywrjwg8Y8jUEi^S+wY`!YlVYKxY?v+MQCJ z&EMx(X?^2c1MlVs&2h<(2i~D=gYO2)Mk2`&B_@bHYePFx6M%rSvrsi@v8bJ}Ouj`5 zecYx2FY?Z2`}NW!!z|oqR9WMh;N?ZA%BdSlHpuJBYjf12{&ca>OW-GUscDN)EDjID zwgQc@KP&>%hALxwK>=^e-g@sLsdrM%B1$4}V<(loLu=w`~b+5$@kuX1J@oTM|hzI6vjTA~{b9wx4A?T?W_lqYRGn(IxEkeikKnF>l ze|y*X-sU+m=s}qTcw*ujJndin&2>)kzdp#@`2g5L=AL>W$Q|Uhu0LmO9rmzp5w5X% z=N|}5gzCRRSRmLYx=b3V+l8%f{9$p)1=!Hw7XCYGz6rJYK5yL)d^ZbB$|%xd4Ku$5 zs&WB!3AMLHHfF}Z9hU?99ay)_pg$}it3UyDyM>!$eRtHB6Hr(^aBsm#p|BRrEZou~HVmoV&TRU%+jz6g=kz zwjKzY*)hLB%soOWgne$JOMqb>HwF0Udfw{Xp8f4BJI$3fyW2EVVBpR%h3EkFiyoT* zA2i$aDpq&&`CG|Vv2B|8%#KgNM}6NX$!Wyh%z~WKAExnVHbp%N$88yc_;eoIX^5UU&oS%5@Zj9B#)swPz)`BZ^ErNz!yjtt36%OT_7rxomST#omAZJmVjcVzD zNa(h(%#0}-2QyBWIr@i1sh_0Hct+It!(#5xz`GjLvI`IR!*aswB9PhH2PXgZyKQCW zLGahl?oyYTy&-^=^|n$0eUy5XB)P~*1TK{WF-sL+2!7Q1w#8k)u%g3*Wk!C;3wbo+ zU1Z`ul~1$h4%=M3>&|^nYd092DJXbj8r6cycP^|KHGQ5G(X+0WR$K=|Qogl{q1folqE;jt?n6mVX;We} z$|m@?t=>T7vb!pQcODu8Z}E#M=vo?S_Q@@D(3#uZDF4lCNQK+wv(1%IWkCDe8L%M1loR5n+G@##@t%{*B4}Ws{O4jYq^lN_4nV(C0yUp8SMP!WR#?<)FGH1Bywu*GRd{zbMLv|0 z4R?1n3Whq*#r3k$%rx=^67nxjaJA^=)>R0CHLDHXF| z4}$UPIia;3G| zNHDMd_MwU=SJf2i6MxJGVexYA?&?xW$>Q{NQf@ohg30?0jBa=?tOQ_@e%tCi6E5Di zBzd=9<``+t@QcMLEV~adR7rY{pYm#XW9Pch)Wsd&n(A{;yJ&{3H$UELR;XjJlcOaNugE>c ziMuq(I!)uJ$S_*;PcXp*lYBA6bEFc2(L4PQ6$s_wscUtkYDK4~O^aO$Ons%^ch=Rb zOMR}e*sD&WpMz#VNS7_B@=%~C`u+_1Bq{rG8!C7n6PLTcwV}N=ZcH#x^~bx?ZgNSs z_N#{))l*D?0%2Q0m7`s=;U)ET5Y^>vH zBxeOQUzV{;72@KJmrE8_rymAbntTMIhY=S9We5fuuD_1%*-oKJBKuoCfS{6E7=&_r zsKGw?g9Y2q0IvX8zd)Snu*$*u%Q7-~AT=T}u2U#g%o1{)(K}+D$x%xja1$*SpBCA7?4N!a@}KqfC!TJ`5zeNgZ~br+^kB5>_;9z4x+`A zun-7BGM`E?Y-%z43{I(l*Vx|ca;NT!q!V5DGmfO&%os#9bKK(Nsh1P*dRt{Anc1vFH8=I1^^q8+$MRoFc46{g^DpWm4Ws>xzvOYMkeL2-R*l}SNT z)DM^#nwu_7-Cf|MqR6;UInd<#EkLpU zF?suR<#ky}8lN6$M{gZ_@r&bS{>_Ue4qQduJDa{k!hyH^d|S@`Xwv}Ex$p+%FryH0 z9k9IT7Wf|_>?^Bne2uM#RZBhuyCnYBM7k-hA5IY6W&l|O7&nuju1GyRzoIXL5G4&{ zE=Y5XW+h!4s#vUPF!FJKbiG`G5cM=UcYCQ-eBGHQ9R^z*p+6+yKKXO}oYl$twR{~V zh+nZix!NBZj={Z}m6r`vnRVMu!vt>lc77ip26D`}IlvyhgPVj=(tw&gxTVG+Maaal z7m{5b+6nb=Ju9=Y??-Ao$4{A9-i$P{+9#K@rkNK7Wz&SjnvxU^NPh9|^I;3{VA7UE zaN7f`PwUQwVu{f#$=_?7pR1Uz!oPXG;g4+tmY3}lIMDYI+TLV(z^>z=J~oZO&Y1i@lg z{5^CYvUjKo07=K+@VH1r$<~u;;m|Esmk}4`1>CNL3bXbvWK#a$LMFO)1nBxC?L2dc za^^gY$P2n7hEb->(wFMuI~{D^mr1u}ut*m;KL38;E6c$f`!5`Rzw;5vLQ?#MJ8%=B zPjxG}85Ve9xNy^_ent(iqod=wdF^xlV_Qw9jCOoAx^b1Gm?)n;)btJ;oioP<0!h(zvKPwn3h$ z`9(j)#>R6UgDv#rgot|MA$sptzvRa)iZwM~CpVwy?Kl#HWs`X``s_L<_CYpX2*>2A zst9Xu3ol2olMe3HXS|GmO^4nI?Y*|RjnB+b?SF510iucs{VNaWmWD^&h;e2VAjQKRyIW&x!vkF45;pBK z!^VuR7lWV0(^@4(SLQgN9=Iq-U3Q!#76rUrj3zLhYD_o0XNbVWr47dNO-oj7Xm~Ar ze5GI>F-}&boMNOQHNt*D6Jdw+MZ$EtkP5M`Cfg6)F4dwjhLJyxs42!S?bqmKnLGY7 zanCAIiS>J4BC1+@U5DJ$$oTrXs|d+4knyDd5?!vRkrm(Y{OAtZjr!Rwstcfwrt*ew=DKp3g_ zeCH|9F4406N`G5H4P+W6K`Zk9Ql-2L{!tG4|DsBRu zA`LzcN)fgms#Mz|>Rvn?chuKc#au-8?Bf*O2Ita{WIpA&(l>mP3r!vTaTpGSs5@zV z9$5Pz9CujY)!*#7nT2>mH+TOZAFeQ+1Wp*e{2mg5FI4HwHP!kKJ%DoEm{_AA$X9dE zQn^Skv1rb*A59U)qnf6`gpHHTnTHU)6ZM+|y4ZMlxIJhB#2fOmb ztO?7BNZ~MTYw~Dl$oS7iol5R*Uwgj_3Qu?};gg3@$5KfaZAYOCZ^Rx7?j-2_bY2i3 zYj8$mJo(vIT+R@OuMxCg$<8ec-M<4w(I4!MC9>>dj2J1vdPDi>&L})j z2Z*uSAZ~<7HIFuw(9}wZr8hA!sG{zQznI{;44Y6Pea67yD0yMf7#F&r{t1(U7H9^3 zF}uS7mZYg%ak!r7I-tB}e_+Moc;;YwF5hY8!O}PU;IIBBk8a^2WmMC8Kpc05sB;KG&g$I@T#pFoefPqUnr_h1{ZvNZg-fR+Sv1t)o6ZDG7fero7dQc1if?xg)s z2fZTOGqy~4ebo|ne~}m3*u%H{^jFjGPhCJ|BSnpo3i=j^d?BPo=5e)2&Oh{nht@wd zxG-1|H1)PAJu^p6wf?BEwJR=R|-fy(*z<9+x6b8d;@1*Zq&#d5`+l#-} zEbGX44w`()4aw?`up8C1yr;44t9WI+zAr`CsWFi<@as%snBqlwt8T)<_Z0`+<)RN@ zMM>Rxz#YB^?$FCi{{&3p=NwDhKC!nMkB4tBtU)qtbYjYP4Q*oXA7Z)q-EJ0`gLC|c z==vn_P74kf`dMmJ{?EOQ_~1kOeWAsxVnZ|b7M%3o8C;@;LGy@;q6j-Kk{boicn$U7 zkz{C4!bqI)y6nE?=glI-qD@U!soyL-OY$J7a{I_8#UR}d=s+ey~tzz+#1{U*wDpPU( z`KPLY!QeOTH9LNT8*`N`w_UbSmG{rv(Nkl0{w4uj`w~2mn9!~ydzHE8#gCFcE$bl%K#8q8F*fC8uokb zf^>1y9;Um#fSLKymsv%wRKgVh%SIUe4;AqLgN=aHL%1_akmsn5Odbo6a=bAZ{G*A) z`IyfC&KsYbNeMgb(blM~@01&8bk`Z}KrfRm>pAip24~KI&IfHC;0bwZJ0Q&U{~SSF zT*%{q=e&OrB6#eiGv2;W<+M+_y(GL;$N9>uThS+4LReQpI+he=p|$C_@fm4njp_ zsT!o1#j)55YR(S>-iD0NB%u9Dy0BSbtRa){dRLxL!&rHgf6zg>s~+IW^SMprDQm_H zO^m>l8DU1!+}!oIVMkKJy8IoRyiD%T4wnzxo>kdbk;jJ|1x|WA3F9dpO0gl$C9YTL zAEn;?q9tv8DY*&nTX{zyGrcxHASog3pvi2@+Lf>TsDd`@4oJNR7Ak@8CM&M5Ex-h( zU}57}3T4M(EK?v*^_iV_6<22CI)`DfyYnP=;QY7&IflUB0gVKSQ&=+OP(xS{gx52m zqG^0bGWoh(Yj<&DoS*Zvd;AJXC-qJw{^l1&cv79`(;jKVWaxGkShsnasy5B_;k661 zS8hHN3NK=D^!vm=gKG%K0NBb0H5rd(ijovlb*%|{P>Ug^LQ>#D(w9DYhlJKgPM1G- z-76nD`Eb~GpM|{TUC!YxyH6-K%zO!?8bXvN8=`qYJBxSuVewrMXy$Ey4dv-@cnf-9 zpgf!XhM24vLP_zB{6k@WB0I^OKyg+*=oKyzDfKQo{t6DRAw-oiwj z(^K-=z7QcTN@&d{vXcCbse}?_|AQ=yzj$iH!_($>amGj6c70?+jGx~QAs(zacMI6B zfw_8Z&}^Q<#dt>d!|)=Z^y@=FD4n+ySMs54N1&om$=yWAK;ZW0)9mvxgNZ61Y3~zv zaXonrx$qCv4)VJN zkB|Iu5_#RId)QjL-|l+?;^2yujq*yHCPW*Z_eGrJ)57*M{S z2v8O)e|+D(MJI1S13x!Hy9MY=N>!h{c&NtNXdv8=CFAUvscw%Uv^{hxEt^$q?-+TR z5>3^nour>_p?)B*xx{q1NYrH=^1!s%c@~CG$lq>M7HmmiAsp76b_ijeRH|ZQyn%YF zgR}$1jv6f9g~9W5opBK-&2)XW4S>yvc^2gc-%SzkvsJOTe10p4ZR;IO65|DnLe<4b z67>}kK4jJRQEyhoLnvpO0{zkzTUyyVq+GQ^LhconUcb-bWgqoR)2-f4_66bzt-w`1$(Lq2uZ-`%CpgB{|`$LfZ z$WtS2w~HP6zB`%SR&e1m`D|KntM+o|eb(}fb5fQb@Te_S(cNt_=n+-}eE%Aii+mau zgArq@d7g(pR(&hBbH-fxt8elA`s+nwlS6mzT|Yc90p->kry}O{;=?FrC3~k z*@=`RD`ey_QcJV>DEeY;Uc-dz7RR1&q%7cD)aR7oS>T~(DD0(eRc(qQ)t_d8{DeLm zXRh9b5vcbS@TO#uank+CPn|9X3pkd(^xrso;-=CqyCtnF{1&I27+(v}tR#?Q)?sqZ z5m3owBn~Y?ZC{w$5W@vh&iW8qb`@GwZWu@{T^71>)AxockJv|+4CSAHSW+PnBoDJ6 z#{V990@O8}$yC;EVeLD3|g){p+}XH{Jd)MY$dtz-+3L_S*a~$&MSsD{m?@d33)o-(AzCc zbtW!nfdt51Ze+fcyG?L5acFbvEBUh8#dE?xfDpc1jp(ls6v#(X6>am$l_-xSXcANA zX;*(ZBe&6Ye%Chy)+eCaJy-X_j2^{SoP>CDXED_dp18asDq?AMvPh3t_2`Nx>%Uz5 z?EZ!bF0ucQ&?Ztf?C-75VyEbDq!r;DR`M?~!+66#364nMvaqfoAf8Lht4Vu=6l4xV zc(y4cy3%jMcCL=J1$$7!6jIj=&^{(#H@i2)I%K*$U-11j3XDolqpD>Sv&kKl%Z&6O z7}Y)Lr>g!jDy>VxKfa!`u_^A?!{J{!22vVD>FDibgWfX-3lE)@9hGc){vJ6p1bYUp zwIDL{1TgbYTA{?>fWxESzze~Kwq`XQw@Jf|)|)IPuB=}uf9pk@C_fSPJF<|?ZmBVi z=Et0qVPgP%-wl#9CyGFBETWS0M1mqLgq**)-Il;D^jj@ji?-M_nGoz6Zccwr%-G+< zAC)6|MjTrHKzSYoo+RB!qbgSQK$A|fUYYWq@d|n;kIR6Nhqvf{0E?-gDm!Sq=`G9_@mQR#;?%(}%1Y;MOVqGF z?K00DJG=!1jeu@GEfD$n4@;w3S{WQw2TcpYh;FAkH(rP~Hjdrd zmvBTl1_lmd9h5E}K?$HP&;kL)@6~pm7&U~S(>VAIxb*Wk=y?8$9rzGlZuBldSbX|L z>e3@qT64Va)3KbG&_O%sLqMuNj&emMoq_N&Md)7CN^%_~DrpfNwO}C*=b%|-e4OlA zX!gmJfpc>6oQt}t;Umd$udq`7y~eh8B7Knzp^FljT=@&qC?gW?vA#yw{ej=k_4Vim zyVs3L5!8$@84qqNkVs1);5-M$*cQ>h$i|)g`>AqR@jM~=?TSe+r1S{UVQ<;G^|fq$ z;-p^(eOnYamZRK>7DEorvFV=)YwBXMQtcO&eckLy z09&TO5>yuXYA2jC=8a`rtE?r7w=;c9*v3ySLh%~&KibK1U69(jI9)IQ5}@Bx&lMc; z@@9e8FU=`aQCpoRE3`fLupHanzB>Ui)^|02UQzyC$K>iFT=b?4Y(K>_{+?w>IWn!PJfXSI?laC5{a_vvvBYzCRSOB%AT32vk1Eh{A zNI(O;7WSS8DoD2*(mmzn9}pnZQMQI}e|CD;v--{D@S?j%*e^?SaEfj%10DnC{M`D_ zH(qEZ>SWavHNs*{OUGBD7yhK{F6P6`_fH>0QloGqyh3C4{F?HzClNV!Mxo_S_rk#w680hA{U%bHQl+F(}}8s zq-x?0IsU9j3d{1djc`*42_G~mQ@KrP9A07dJv+cF7Sar{u!N;UNhWD{7y(68U%;d` z0nX}n_qaPLz6&Qvg`3=sKA_)UJW<&a&2l95nTWGUBF{zO7yiz_>?{e)W3GrQ#;ED)#}rwi9nVyQ3rv6z@S zu^>H<&L%(C#u|Qk-oPV=Ct0s1JUz4Ts2hmVm8Y2M$Vq7J@<+&F{&=9n&=yckh1s6J z+`93!wD0@Al1ohMYj*L(c6=u$kv97f$_aNDkgCIWk!o-shH>mp-Q=naF zNOO9=_UK`l{3j z^QWC5A$zH>%I^6kkA=DgX+u^P&8t7R6{S78{aI_f9oLl?+X&-@E(Ey%-Jy6aXz1r` zK@0e`=&ZATEq@GEFD4jnXmX3z_#`k#g|-g}82+!WP?tU++{{U~fiyFFKGlt;?72rU zn3qN^P)aIcKV2>#R6s|2Wt>A04!;N~SHjn_pZzX@s49iLzhU=dnJ!GTB!KORhgBWnJo%sy%r6ss;y zze8F|OCYK_4E*41R^W+v3}T1sYc_>FwFGY8I|b6$^LUMUvs2pd`S@2HBG z+1ExbBg};K7?Z~Gd?|get$r=Y)~6oaFYk3T7JvU_;DC`v&aT;CHL&Pxfc-c4r~RPX z)AZ@rhUliCt$-;<9*?QWW|4u~V8s{6E0O!%svr3*q=xHQe|akvJUQ?{f`9;C<6ofS zxaR+o1Yrp$W>0IuJMuO)9Z>CnCF))IUq5@Y;#WX2MycZU0}$$44{ij_oFH$}-X+K< zN|t_eo=+c*O}D{XU4QcNysW4?=q2h0`7*Kee(3F6dkX7&|98WOL@!#;KZ;4G6Kp6I@Pi0|7j!D zp+4J%muwRY$gNE2i&{lueSF#OWqn>ton%9)r|za!!*z|mFQ0^ZY)Kk2O7->VV0EG= z;h5Tu$nD>7QpZAQ76!*Hr#Zo$~-fX^MBinu*YYztph?zec^T$s2D_w+v&t$1eaWMq2e%gb?E zRyV$9@FM@Pgo!U!byz&c@U5fO5PIY5T6sLHXSz41Z0eg$edVJ9TRPtmKh4K|*)=&b zA7ialzs)wlOr*~YV&;MUCta-)Mp(qpQ)0-49iiumT_5XX#X`h}zxFp+s`Vz_Rhd?s zytZhhk?sP#7P|)+R!x{Olj|FN9x#_B;*tUg5P`KEkE!~08#Sqpg}!VccJ_VK{#?o; zzQt_-X?-ciZ;Ypq;*vjTK2ntzM_v)Al0x-{^yzR> z{%#2MxHxD+1Et1v>5EYN$?7H~O4)ox$CvGi z(o)1t{wB|=r%ZX~G+@|iW9OpK5JnODI0#k;5u%HIj*c^umhB-)MAGbK#Yb2!*tyAq zTQsrLy=~Qpv?^(ufR$f6PJH}k26TPCOYY7l@I-Lfd~iH&k~`hpyIXA{bhG0YQ7!Te zpGJ&f?)*b7!+UeWAgUFfpX*@27E3ybS5zS#c=n3y*4Xvk&H3n~Ss{3?k1~-t^kA?u zAYXk~UxYb>Rv=q^!bz>=h~*JH>5dg%O!48M&KA4THt6kxfd&r{BS&Tl1FFA|e7YC& z!P3{69XEYmd3TMG0s6+ya#2xL%%N;p(@vMUj>!m6c?<1yIYg?k8|o7I^44pZ<%xl4 z#U-!Ki`jv-A2zkMWidWXb^tiBL%piv8$s#^%|gxX=*pF834N>{^z(A*u{6oTfSnA( z&%I|4?qf?e6iRY@tTEwPh0FVUgSV-oD%ySANPLPA2FcdI3qy2sxO#e^hkWIchU``+ zGYQS>&CFw&qg4pI*#`b|jHYIz!OXGOFL+4A#eeP!U_c-h#_KaO;58gl-M@sV}0x zai8+2$2aK0YXsEehP(p^RcgyZzXQl%HRSH?imhCJ4EO%<G!oY z4L|;Z*8;2rR4^D&G=?j*((n;I4jwThH%{{?RY~V8eg{RaO0sr2hUR@OnFp*s3Y}J} zYp>uO9kq*Q;vk@!cAI7&b`&kg9FS21H~&JzQ%yM!^__nlj^thHjpr989&Kzb!(Z$^ zP?gKnMqvSfKA(%K%kB)<7o>YoYw|sF<(tx7o2$KK!c|{K-m2SGTHA-e*jXHQWTyZJ zXUAbmtG)u2Re&&MFO;ks)1;@FgSn#Pn$E@!<35HxF-Kax)KnT?SY*u`QP#EdV|U_& zl;qeXD5wbw$^uoD7K9KRVIHBYklEuJ${lV_pgtM+msGryjw=q_-zR_Q8S7@_y#C$! z`{w#~@K{_!X_Yi}5)>OtFNyq?eJh)OWlXZY;<3R9KO|7~pk$CWm37XW7$37Aa39|r&C2mBgt(=!GpBF-hc7ZNQ1|`%2 zOq`3Xr}6duvQt-2a$Bf)4_)fLcF1I{e)%l6XJk$o>II3)(&IB`q0J=TIEm=W|vjRysqyw0*=;yHvXO-&tl3jRk`NouZNYXr+O4xp#Mv;dBIoy!qTeesJpN!1IkTCZN222 zAoDBo?1=7d|F^ZOi}o${#N-!~%NJ#fYU1%zW^Z`w31_uHn{^>MN;Y+lW=8cVRQ_S< zW&|`$B94*+JCPE@hu=i=*Lj5~``wB>AeVgh67e^B`P@>P$gWF4%N`N*ze@gr0BEBu zvF96fOY6PIWm*#RA^a^ljh@s5xdGsbv6k1uztnfP-|!7jF<=A-OVpKqV9D&LW6xm2 zkqZCJShN18)b0OvDuHuz8~9K~(BlrXxJ(yUpAU_OdZZAdsL^u|C;lq3iy$N?FBM&7mJzsX*%p7;hmP9oC+ zuiwWg^dp6%ZK?GBmpeYQA8JxMD&a)c+nOjt6kJ%pBlwqd1_5N{?!x}E`rni%s=k97nGAs8FySdVeFkClI5{_ZV z`1nu6Gr8@_`}k7xgaNT0pl&_PI8L$g^)4 z{wJEaSE%_%=WTFZ8@vc_u;(7H)zo~TZV=ipZCuT`7$?D(E}u1K`TO(cbia(AfJe?A zE0gC_#T>~LnzGNxg5v&yfM?>-8;qyGE#~nuzhLTM(a^gaTsX=Hbq+5v{H@kg3Ff0U zO20ksut#eSXJ^msVBCOq32~4FA!?8ex}YrjipKJ-{7LnbTT{4WjaMF@p4auc zxc{=xd7%RbIW9`w2#lb2G{*mc5;5gojd`R?P_en`fyZnZ?#dylrrv(7XQqcZ(LZi+ zmdRoiK=~#n&P9-lqd`#EQ%DgibJD+FT>uuQ(09EN^+B(q)%|&v0+v%q;l}(j2fDl& zyfr8N0Xcph6bvfRVT^Kw@gg*?)%_`+3co0EIb_hj-uu(w?0M(IRj&1}X`(F|N;^i+ zKSjoDfC~urBx*s8#J^rSA4o{618OoFE}o%C6y+{ZoxrR+-t=uG(oAUTFPHoVJSJZu zCcX?^QNF9}1V}PG{v?C>9c(G!ozA7exY|w?Q{kOC;@z-=o@1do&J`hkIS)TgU!g32 zx$)JolJH4}<+Ic&T)F1}K=!e{52~NUo-_-p@)%C#%9Mlsfn$kJzMnoLQcNG8S+xV>|WC$M4{WM)eiWzs*D0qQ>>v zkW+X&;v@l=?11UC5aZpyVLJAGOeOQqsG8r+^&CZ0`UjJv3G!Y(PpWs#{CBI*rg$*9 zPSKo!%61{)AqwyzOSUayX9cM7@60Q`b9jHn`c_H8rz*m$y!1NDhoV_*xJ5HS+Kuip32g- zUDjCBgJJh>988B*LP;(R>uU{QV`Lm z39POv7B&AGeH8p{2wWnytiqG2=2f-b97IfsBtLQR+VESE%lW0*_S=8*D8H1e z{pitK9XcUd#gdgFx64M6Gf*Muo!8o*=4!#7^-n}`Hdv%VMg8hOYw@#(UFp7S=6bs> z0jIa9!+4Ru@8Z|NUKS(y6P#M0CcBh5l!YQWs81R%`gik3;`!GZ5mkADUY7PNdkt^P zg3BBXTy4-3W2RCUU4{4Y*@dbyK60+l5ktW$ei%pVq&>n2BiZTZZB%zMZ2nu+NIgt= z>L(dypjTh5qG7C8^qy5YR_FeDxir>}GmV?X59FN)GaJMhQU`?qFe}f828+-XeaN&6 zsLfirck}0O2MexL$p{nQ<;(S_>OOCNsWmv~&Tf6z-pJAnIX?uU3uiN}*=fP3@ntaQ zThTu-rkMFR05A*25QAwV2s6g(9MRmh&K%LN55fX6osC!&>)e=2UjwukoZ{D)Xaf9` z0Q1CB*JySit7GxEeJxM9FCT0jQxguE%$pe7#HK+9p5lHZ`sY>`CzBDApJ`(SR%0t( zkPZ+6JZ|u(ssiX{(t;w|gn8n!!ek{w651T>$-3uLWBt`07vq#xcxLlMd6SFU>ta?R zZ2H=>;E0NP zxs-vo5n+@wPXpHeVKOJBGf!kg2F|OqE<5U(RX5!itX12q@Dyj9UdSC{*x(UEf1vhP&Euhd z-?&jFgk&e{REV-wLLt*;OVVQBCi@nXWXqT-`z{IL%Tz)o+4p6#lVr^@mN6qK1~X)Q znB{x!e$R8x^PJ~(&U4P|oaYbmlHoJ==W}28bzSe}`U5(>i}z`Lqlg^8W7zvozmFd9 ztEvzZa%1nnEo3TFksY8cw1=lQadmCP*}*%M2|}U5Hz&$lPQIGI8sT|h#LQRZPL66v z_s^z&xjsP)v1gSBDqZ(r^uPL6b@X{CjaK^|NW{~od>4Zz+p&_N^*8HA8D=86nstq? z?~Od(OP}4fKJ?-FTd&gR<^S*%odwC&-`#dSG>Qi9B?Cdfhcbei-=l6*`O{F^%#=@B zWhF%|)pIq&Uv{|V1JCvzG`3PIyJnNXw*Ly7NMqpu?(%Bjrg=MNAI6&ioe+=IKY=>I z1i+U&Q=66u-6g47-}zq7@O^FKr`D*MGOhA+ed3yLj_f(9Zv<5rmu|Ov+SY&kz-HK> zV#vLV*r|FV0Pt|GUhAqX$+mx-NBll5<=Q9NEMzV|pEqoHZX42hs>Mx0ny=x?T|czl zZw$q-d`5#P8&i%t%*?|0njzaU2e((7o72e3t(*kQ@%xLdDq{wmg)03z8!9*dZ1%}l z9_mg)ccc42JE9XlRZR<|;K&1%_?vj~n6mjJ#m(;F2}`+#x!y(YmUlM4tS|e-NPK2v zJ89uv)(!)Gas=!U^zdJ{8_b*_sWpAPZ&F&SzHoJx?QyFx)oK5PgqO)L`+vOOjVM)Q z9X<_(6C>`@4pGcI?M-byo+~pYS;vtJq^{Xm$~Drjw|&3GrWl|X@)LI*guge43ZN!i zc!k-osxQ2vf4qf)u72&SRd9S*bLL3b=x~*m=VX-*Zl`hA8(6*!0kRT z52}NktX_SNzr(G)aLKM+`YM#{r2CB9AtW662vH|;YUo`0OrEj7YwB6KOqVUop@ZC4 zOx~Qe^Q|ENtq(Z%m@hD%>M|6Y)_gd48RTK2vYgkGy&4Lb2Noo?;jc7R&{q{Thrubo~eJN6JXWEyR% z+0=q|7#{ThR;t<3xncNCH#yVOIv}?S_c9kQ=k^~n0RiYexblOL1pT%bp-KV!c>}sy zi>wUA+Ki$c3!Rdfob>w|3f-;s)A-B&F>|M@E_@q)8`HaOhDgEy4&%a0Sj{6$%`(9H zeTHW0X-~iFtFJm8OngLH&+#s5Co<1Ym_Dst%964^sUaY;=z{uEloZ2;ZbV&$;s6dh zfAG7$0?PEeUyT{NL4A1XD!pE?9m(_X8~O3RyV~LM=P)w!hoKxQI}yeZVyDrm<((30 zN54-lYA4*!3b#Hpa7cVd(yy#5aUhoCSmrk)xpEm_+RSCJjXhcIcQEZ|%i6g57X~?6 z;P9yDzqhMBxm&f>y?@G>uIcI#-~n6L@)}@DyZUy!+N^^y!1Qio&4@0=8u4CiRaznN zv(!*FRKM?44u#k1_bkc5{XdTpvwt3X{UX*xU?J8n6St6nsAXXIU^Ig&W?9Q+@z2_Z z;Mk15|0}EP1K-IizWDLv0?+IUGQG`;kljDCIgzsJJ7wCq8$&linNTOY36Q2WaVN1& z$vi;qRZ~$`KCLv%+bwI-{JWa!y@xjI=VRsB_3|5~1rZJKdK}Pp(_|8dr8^Dd`6V{(cl=1yh=lWf*cfaYNcL(rb~3Q8f) zz^1cFMr-CIK2KVBMb-ht!Tz`QMjuQ|?9YdO^!T3MRrTh;C7F>YJZ#DL1RBRy{jT)> z#xjJ3N&G3SQf`L_NA=AOje+u$Dit5N=Mofh-EW_i|B=;S-c(A`A5I`se}eht(n1?pm;9st-^(oi0u1N&{dWk1FMhRK@4|n0jP%jJs)|qlQB~-3 zf=A{pdI(M#2gi_-M z9rhc%8!5I**oX1J;WdE2%))QhAcheD%j}&cAH-30nWMJ@~#= zTC5Dq#8RAwrdJn2zy-pI_1%ORCm|r-C&AbtdS}$t*3plo>a5WdFQRzPor*TdG@KrL zc(UhGxdFIenW)>$>M$f;yB0n1jV&jl04bSTms!_6jGGc@)3b=J@`T;|g5uq!&C z88h(9_cO>M%%Kw{U1=#`s84Ye-G|kW5mReB%&-`#@t|yX@BOY=nXrp}>E@kvvaHzM z@eBDTZT=vS){!RJJKfLg=G(dOEAuka}eVGm>Evq zrCYY#?IV#^>&u}NNtfiETY5PzuAv%}cO^Z^*#uE9FdOGo$9Ls7Nl3DMmg)DB+H38dNa6`?y9V4XYO>AZF8kHZCob{$v>D`*{ z`_f+5_e%6ahu(FD=n+_cYrGJ6a8+8uOx58p;9OaKI9=rd# zDl}&Bk66`A`HRFGeuqBwz?&OIvGl_xz|}@9=@bJX1X02oczks>7(^Yg{}COE`zLFx3Os2lKr3Z#WaR6 z8RZFO(sdYD!2woJKZn}a=1Zt%Mtx&V%png+h3x*OVYcn7>zo>TB~(q6m3P<|5VEts z2^HoHaceUO`~GW_=M$Xw2G5Y|d$VJkYp(%3BVhcz@mF)%_uH73tqa?l;7%Wi7}kWP zwgD{*X5pRFnWtIw#p3$+Mz19<6U1ZxSU#T^3Hu=(oAP;K_h3ZaFGYr^28+9s2tb)~ z@|HWr{ti_*6mB`P9UkFoIFQ!y##*Ux_OH;Kt%uUjW7J_avny@S&F#@K?6T^eK68ZC#uWf3@Kl_ z4ocOu@y`Z7>I(F-K$5C3D|Do#?D(qP_>b%Wr{L_w(}C^3B9v*MO=CdkMfckAGlT+r zvvRp2AHfgt$L%?2&%>H3t7GSVP2&|(Zi`tu_O3~6zCC1;fi@qCgx_i|frs`juMRbr#vF^CP)5K)S6K%$ zf%bYB&bf{QrF|)_b^_(PU@v8rWH6R-slL`ZNXV}Sdn;c}k3L?1-}u;bM1g^o+LflSDH(qJEd%(K(hFhOXCbLp$c!J8vJbwJTv&*_H$DRvEgEIP%;g8bOh?-xiZ*%c z9T?G9qZr3Hx06-JN%gBqQ8dlHBW9n~?5#q|_}=+IT?-$i$gq`LgyuYK`@9rwD!kl~w?!pHn`M=OJ2N4&} zP(jMBu(aoB>Xi{`bW3NLdvCsW-qJ2&^ntO6x%NGkdJ$mRrh{n*q3=F+ z2G${_Fa4j$XVksiIXSV@BrV1|Qd3zGL%Oe5uSMJH@b?I>5XLzo34_<4lz$E`#%C*bu+2j&O!Xhn2SZ{%oAgPQ zyBYMG3^gd6f?)BE(3L`mh=yt2#2JFrZ&`I<#7Ks$rqvfJ$L5VuOUb92<@B!#nD;$- zE_kT~JR4(>0cDbw$7}^nX#`%ImN2}gXR-+A3*EyU^cGk>qq;$}N-Bvp4fKB=YZKFv zoOQYHZd9xv;T=o{=K_NqF?Bob>N83Zmaf4PA={BLfPLq(g{#5Tm7xc*HDUcf!Vuw1 zi@ZHltM5`8-jv1dOucK(EUv}O@Y1-T-Vlm*&%8=q;!L+-arWWLVqea~as{Toae{AG zT9EjK?R~cSIe{n2(Aal2yE`-s3Jfq--GT3zx^_2WdW#lOX(so1{xpR2Br&ZGD`anC zoj>-5$uzY*P?Jj@5U%#S1)p$36|#517b4-6+2mfT5$zC^K!PW<@s6SZs7mP0WyY2S zzOyq3(6FXueoFG?3!PY-@}E!qClnY~Ur8>aojqw=X<3t+f36kl+<4SG%WBuP^T zNpSI%2T5U`siM51Edt6qod@ncc>S_~^U8+qZ)idTJ<1{@zr@UeDI0`5jr=@5}C<@#f&k0RJyzJr8+SLt7 zv+}z4|G7xrvQKm`QTZUXB%y1Pr{CUx_^rXkEUcHjOtl9>Aov1EYGNwTvv{$}Mcm=l z$7SKdb5zUS)|LJD?j`y*6{CqnhI`9mLcF5HpgCEJqiyWA#RL zVNjm>j2*#^#6Vr>s>T4l;_r{tf7SF>@*kRg*88V=$cA%94&g;T1w0=v* z9m**~{Ugf8s zWm(BX8bIq=V)HNCXi_4T09?YZ!Dkd{y_A@=IQab`{J?E$7-f6Abv=#)7ptmr|5#`q zzv^IX3s4*tsJ!+6c1ZW-Ql}u|!@MRl9!ES4;z~lO1`Ptc4@(9`S3GoQs}nYba3Rpr zYWw92F!&Z?H8Llqz|>P>R^#)Ml>N96oKt@v%7{AI0iWh#@iKNX^ zt})fuGosI5FqWr=r{|W@(PD!z9Qie6mG&}^#l1`{e>#5z{L%1VXiW+>38m>95I{N! zMm%_t!cqMs~Z53>IJWll$ylvuyS?gGP~NvwfnE7;(>f-5g;8ups0iOfRv2Zgd?a)=8*e)gO5=7rMh9ibIJBv@6+~y7`guDi-eKi7$n@(MFmJFp> z{%~TWM!my7f$BKJe!QT@6Pv;tA7T>Xs4i`w+KQCG!+WkVUjg_bH*o`#3`n|2aL{|` zWeq>NGi=LdzH{SwPed>~8Eyz%e-M?{aiH_|ickgUe(q%$YkgUrRXa1^4`mfCzRF1# z7W3T%a*h+8#cIjAFg}HSBG8nX*?Jf0;^;4EZt&HaaAuJok?*|SMM@l*;M)-F?XS2Q z)xWe`t=nH6?l+gx``+KX_p8-7LlvCDeqhsMr*V$pD&4PAxw_kUM`{NVy^_VHV&#GF zZ4E*|bFgL5Px`=@GJ57U#epM^Uwb87%(F4=Iw*I#Hi`$l&=@`5Ce3{JrnL#Wynsn4 zP^OIM`n}y!=*o47>^cAOb?8#!rLn<)3iS5Avod6n%JMllD7eX2K&A<+vPz(n z*_oLZ6cNZQp{av-p|MuoGK0eEEcy@51pbs>)O%E(%aR564ThHShE>6O`KPu>C5fKY z6A9GP9=q~nh32|yd+tBe>Vu`7(#}N6p{JEHEX02e^2HxY=hc%UtqjC3^HZRiw z^)RchKo7iI1iFtPpy%kvL8~kcEhqLjH)kn$yW8hUxTTv4m>7D4; zG~-uU+*qPI-kQcnDIvp2ND;^1*=U~15n*Gh>Wh6I_2V_?#g6!Q9a@gvS8u3rK>3;c z0nU%9bRFAH~{#@nn-DaidPrQHJV z_^U*&_9XTFtWks@oai0;2JekW2*_w%JXstw`*P$}!kmUAqxmUE-0godHa8d|axCsS z4Bg$A#Rm0Ku)UKwUi3+nT2oU4Wg~t~Rj%!t>353)iM6+GLWR+6?iH^pPJQ`Wa(>#> z>Mop}rRamGNybpD_@}u@_1)O%4UlLSw@zxB$&OxBx2hc9s8zPEPgOL{00TYWp1E#y z+9LgD#o`n6AoCxbXIdoT2xegcEN6W$R#Z>hnWPL-V{d&yNpo5Dh_a7*d~CKxE&y@URZI~IDr@Bo#l8OEp=S~IMhq*DfVBi_2ZoHyrC6J z0?hw;uhDtKhj#WUpWOC|yqe->vO_27{Ap2ydSp-9j4Q}D*IZOWq*c zThz|c(k?JIv^^MMx5?l1{c8(zYqOE#R&dY?@#VX^=m|=ag+|GeI4~+2j%PfshHfv& zTtI0P!|h8j?zb*A%8yQ9$Xq^{Dp2#UIkt(vFpC0*{U!iv8dM2LBzsYw~G15f!MArOpN6)oJ8;goW!+YZL2L{nuXi$Su%;xUW zQlWYx4pg5=C?SFpxTqLYhA@lj%F;@BcQw4YSzIf-?qQRWo^yiRtnnSRJ4(svDh)IP zBYjcsfD~^!KzBofTvbZbJQ*QyYz$#M+F{fC=C@tM+qc=g-u`P+jk8ntkEe9KP>toy zf5>R*z+=lEr{^LhI(|7p_FwycV!|5)aLmMxt+B`ms0pM*)$JsRjfiI0-|}rn^AcMWc3pU?PCw z(F^$G`p5f-SV6Im!_$;(hF~F}1i}{8 z@g=k`5MdE<$QwTyKe;HLaL!n5W9|7;8F_TYHs|;j`vD<6Tnm0*#0RNkhI(|GVN(+` zgLMi#J*JG4bUp7yvoQvjpEpUJ-&^eJKk#s=rIF|7eww3RUO>MPc_`F-O-d@f@~wz7^_EnW@=1qEn` zKR3K6BJ|+ht5Zts1tzCVG)q>vVb2JZ2O*tcpajEa6ypJlDbS2s)6q^qp9{rY$HgfC zqDOa@gtc6HqjYj21!Oa-FyL>c?!X#_RnBJ=L5jwssnzl6S8+sN|$;PY>N{g0!57>)x7|I7!cu^<;In_1|a!hiMBDig@ zH&I{zWh?8-!!&r~>deXdxLRB{eNdI`#?6vJkK-t|8~9RE$(qbt`_MFd$YEZ3Snp)X3N@+W zQLFw>yoqFZXc3=e&6+^E<0ZGVCVg8sF}L78IM7BP1AUm!8);l!gefegShIQ1lMHvE zo71xKg{g4$!L7ngt=+J>svFBkj+Fm{^DT!Ic2dy_tWi_CH>(>Vs@8T8W!X~UsO~U+ zbnMOd`?aOUkp~UFfBagRBPaMK;mG-^M6IRE`=8gAk8R7OWwyvs#y;b!uncKA*9FXU zuUXvXVb&?sX{;sHDy~?mzA|_O9@w_7r0qUc0S$ZPvm?X0s-(qFRbw{Bz@mO$5! zOu=4eKm6*O<1{IncAEKC6F0R;N@Ga9CY1vt{ymE<0{_PrZJl3Pxj9*+9kWbzOaEh@ zjnBQUu4O;|$M}mO&za=+<#_CreW$(@WtkjCX=Vw|>Ek=soCJr9^2j49POf=MXb(K+ z;(A!Q1QY=>_daGpn+$;JK7q=k>00*C!rE1nma$J8`D1IFgQ!83di%QjLJ9Vd`C_bn zKJg@5Wix~aXg8Rz!23bQOv1U^G#HK-SAFWZ;2GY>BX$O@GkPv#OGIs7bGaW56d;Ll;tlONox^m%EELOgt1GHZjeoE1m;gI}J z;@WzV&HRSl$nhAPh32HU4-3cF43V!i_mK2ZbT@$ygWQ)vko%6@X6~;E?gl2|RU)&iym@c*CZfhz1p2Ob%;L&tI3-VWw!5v2PVA{RlE`ZHJ z;QnK(et4x#HM<*svr-MqW6wKaZ>n^_^i+?C{!mSSt1+i`1D>BOy9ocPf)WIez;X~6bpl@- z<9#OciE+5y{0>LV(D^?<&_PBt^OlV&0 z${yp^biuFq2g+CkBxQi5((ueQeeD(80fsXvLZ45gi!@`FWK}y(th<(|xLKSfZ(?`!Uwf;hT7PcFuK5o~AQkx;#t!#FKnl3e>e6VyRIKA7R~9qEAd{fLcTOu# z7b#z}Hdl7Uv^jEqHrGGXoX!%_jcz8=&x6TlgWG>R*E&hq=^@}b=Sk)w0UUBLiCDr= zgK_n~R`{A>qEP#mU*1JRmiQz4k%FYw;kBCAY|l8tKmBd;-eXp`$%IPTFmv13SrdE9 zv2BNZHl6s%%K7^zfMTt_7U$``kaoxWj{6pGqgsIzuNdA$o*75&WpVGL>!ZLE5{V9V z1zG&3lJ}vA8x;AvNtWcK>a-g$LsY$ytPVqFRq3UiE56m{bR+t;Azkx%eNdD| z#cJ{@Bd5m}248G`7!0JQb#%xp{&XpQ)XP3~7_+wx4i7-<`+TGsZ^@h4=iaK+s|JtSb}PP|w&iP7Zhc5hcB#}02k%V-+NSQoxK;_Z zh&A0$s7qAX)9pdsyQ7{>BZc*23$@h`x2~xWn{MrhOS$re$fvFdq?9$wt-@w_S={+l z1gU(@#1%;8d;X${Xg-FinMi2(-3A>C$-&g3l8*&0D$j0y`LJ?n;Pf+rgqS1n7_G>iVE?9adFtV`cy%8$y7!k2R}Ie~h{Z&AdmMQ-faeC#s`=olC za49${B+;LMu++(ldO%@&iH-M#WZa>x3F^taz6Dhs9l_s&&6ADK7cmM5B! zp#Rj@9I$Q9(!Le?HD2w=pT+227>>KtA~&S_1rkpF02lS#0os zH9_kC_4gcxl3PR<2=Vh`Z4~V;)e{3wBbvs^;<@O;{f>6C*XHin6dg(}X{tCnX=cE7 z4|468KSD8zThmim7}S|G9<718wCb#G6}7eqmK&s=@A#lZ7TIS%e)|#_8u?#7j{WlX z=D&LBe-(@4e`;I!KQoB^zxluab17F+*|KmOPFVom=4@x^lJjc-9N;0`+cra7s6 zC)YIA`o`3N{EKv6a)k)lygB^2|CnNoL&k5%i)bYf;Un7kL!*GNs(Gu?BA~#|kcOh{ zdh!?NWG%n_B3#c9u9VAvkCcC&KK!!x!NCCQU`}*9l+jzjMe+NYUjpH5D@Q+RhD}U# zC=UQs^8M5kxfsW%>HSAJ9G)AKuq2ptsB(vPZg4)S)n&l6VM0v|{A6=H@xT3~98aU- zMP;ZT1l2%)+>wIQsMa)J+;cu%8VWLTr=lfXcRPISYP*Z@?MGK+&OxZ{Uze{O zzA*I50wMeds6{;)Vq7%LcZM=6V*wY9`9*6r;|%9c;M6G27mP8d4O(gy4LLjcus zDAf!d%KA*;Zj(d>GV3u&Obp`4Ia6~%D9{Q_EV0_U*_LdIJfEnXa7(A)@T7tBItSnm z_#w!16e$yC>OJP0nhhTBVQ9q<=`@OUurZo3dp`T**p?1?pLm*&v}N8O(V<3=nC0Ur zx8GyD;7|26p<2474@-g)Hmrv!gRH*XJw4|4wr;E?UC=d1cm3sVNq`j_HAZv?sI>lN z6Ar~M(}Jl+$#Bm(*K1KuItt(C<39L|u8)ic2OGJ;|@ zG_6f5rf~I`C&OwsVo21%VJL^>2s5i0U&=DQs6Ty3UCzEkT0Zc`2EG3hp;lo3>4$`A zUZZTHIzt%4;*p^%d1Ys7>kC7U5mff;6tec6nsVQJc)y;ht7jD1Jr-BUgUOY}3!li` z*pYJb5>>HB5<}hqrQRLphnBmI-cm&EOuu;Ep`vnfLf2#KF03!+ZjFSD)f)|7(fs zgPEG?c*cNJL!$NNLNPZ?ThH2BmaF)GO;WK|m`m=aDS1YG?Q^ure5ge8p{FiyyCYua zE;B?G7Z86rv}SZZU+u>RWi|T38C}-0C-Us-Qzv*EKHgu}?pkv-{p~po(qGET$=^)4#YGJ~J`B}N7zE3Jknd0~7eYz_BlEwHW zE~1?g7>J8R=)fip&pDvfsQMWsV>04Pqu(i-R6FmcR$~modx7#GRFkocX}^T?K*nKf zLm1-7g|v2TVrc+8Mga*I z*y|W@RlYb6K5LrMh%4o09@V9@8`S#e@-=gHr5b8hHM7Ld2wh2+k6-rR=k;t+H{p}T z=*GQ9rWC-YNNJt$=sg`Mj)aY`8RE0i-SrLpFxTzLjc&gB^~>AExamjWeGcdXE^{LLI$x(Y0xR7UYIxwG{N?xl~}_% zik%m)!?{DRUZWI8?$xCf8u|0%iyBirqa{AM-IfpU%2E&p62`vTeylhb@1EiGA;C7s0br4|ig&g1!R=&mu$UQe&tQXe={ns^D z^K#l|+=ba?K8pFvcX9jjygBs_Z+mmWdWlbcZhY0`l-ab@ONcE!p+b{)8+3D5daqCXDXzZM+d~zII?ZH>&7oYomW(*$AFIy3ZJBcHP5m#iD>}LXt z&+Y%^m>}MFT69)^OQYTykry108ko3Vb^FVrL zXUx5;N2XBi0ekW|ib2}6Js2R!O`+2=na?H=QS(PBj#JtrzryFo9L7>d6!K^9nAZEg zJKBdkGi&Paex)$czM}Cryc!3*wC<9CUJOTzW~S+xco!QGF)>z7k`1GZ^@#>;ncGQ9 z>z?$hHDyvU!N;@=UpgIL97IR|CJ5@oR%O*%p@_GuO%W5V#XSnmW6n(}8%LAZI!$;g z(?vF%Y%lG`y(=e`&}P8`MxyMQk+_9-xH_v6it?q#Hh{4GA#mECX!0vqHm9t8eC+u4 zAz|*6GB&d$<=CO-L&@6ypHs~z?9*Zihv5s^VB!TP0Sk2S-6U;lZz|*NE565clGMJC z@G_srrSbs?{|G2nykW~FCFvBK?aqbt+#+-56X>6_j9}=P`(ig_8@8x=SY5c8!|~X* zR1i*O>a*AqWAvsW=lu5OGD9SsMxz)pgzIS@;9>US7qYcq-Jz-8ud~}XE?3cn6L#oC z9*b@=9{iCIomQvyS$gq8`40kULjttRc`lr$p7hyCftd>TQWso*6&@@bG7dOCx@U}hwE?YSQEaCgF96m6Y?Np3p}c22#HVlQk4-aeUylhEVy^(^WP zA(=|32IXn$EVX7BUt4)z>S$*mZ2VQ@sBXfAexKZ?hCtZFatH82EZ7Hi63?TYDH}Ew z6MoXC22NmuWM&_Hz3tF7BzROO?#|`*b}s43S7zt5hFiNk;}?^~JY1i!ff>1%lz##a zgc0o#5CWWI9IvLteTMUTvxLVqkBJop`ITHdmg?K+oFePNURu@jq=0CEw!*YO2ttUX zt~Wzm^n<@8=a%En@uwDOnJIQnW?eIq$6w4X9P*Msx~7pM?gc{?k4o?=yA_7G-KHxp zn5W_npilbL+lhCoOL(E>+A1X)>qobx!uy@s7MG3lbHa{3a%5e)^= z;pl;RMao1RRhVR+u=eP}wyM94d(!2q%&VHSlx8B=x9D^4zT6EKskDQ@$Scq?Iq-}5 z60=YMuX+Tl$B~b{D(Qs9V>xSOPOW$B+-!K6wCda3@Uf__$@Xo4PSdP=($bv8YXzJ4 zli&euLVv?mW4a0W89?^VBbl@R-g07?Q_J#8;+EcP_;AtqSqM)07yz0mQ*R`>vcV|BD;1^&&MemyG|BXgriPaaNKL<<%ciL6d>ot!b0$ zDL6s6H{Vs@Ql+d-{tZf2_Ff91OL!h0|9X7=s*hb!)$wzl*Ojgh)|v|4VtllyyOp~) z<_M6yq>jy%;n3`EJNf)-XZH8gOc zoB6Gpcu~jN_Z``@p`tO}A}ppqMfOO&n0)4qN=5n9++Q8be|kUtjblWNL_1}%YyRx; zpACW8Oo87ab}22|RpRvKdm%zv56{FbYi=4bL`rD=P#j$X#XE*Fp;)&+hjx0=Y7=YV zcwCbQ2;vO#YG&?IMrPzitoGODoXn6{3X{9W%)Aywx?t!A?MCb(knTNe(??lCPpLfJ zwGn|NzVeu**5~W=+nmT+?@5gB_8h8G)K0R=zU;Y&^g$VqaFGnMSM>Y@L zhpj0Aix~@}+_}j0SqB@{cE*>RxTbDP(aTOBC|VWQMa8QLQE_2Xy7xK6i*5Rh4-$s? zFdEoOOe|Uf1P^d<`~EFkUJ+oL$DAqU3*||ge8AN$x{ULC0joblCx7u-NX1w8E7D-6_#$8`Ayp6UW$ujrqS-Ip{CBd8F-^ zvph1FP`wAIgrw?;QpGPNbtCwVfy9 zHJB~Wxb3rpot{wJ%daZ?{&S(=ntXqP!u*w8->lhy&%DkGJ_I=LUusLoim#rECkls=X-7VzVN#zb?zhQ(M=s}7&5VMF5j=!#~R z1TjGVpXbhZB&ycABkauCQkF9J)KfrW4BlvNU}b0{(i zQ?iQQH-^F#IoLQ%F)Q7~X_YNK25z=jL=1BJZnw6DNk6wB#{}-lQq7a}lo$LsF(S~+ z-I|d<1CDJ$OMm*22mXhAH6R-%&Em#G^Zc|aT_aGKfo9|v8MJx?3UIkgnt676?ZIx5LU&0{GK6c_w;hj_Krc~hK|9nx^?~_z zW`Sgaw>DD4IK-v25PY#o1e?)kDs z*4$Sxc-qKY)+2sU#NR<{x6R7x)Kyxz+M7luyBAER?OmeD9cZ6 z@^*dh=GmNC$peAM7r8DtopD^pk!09Aa`jlpsAk)3dCoq~#f?pw&as-`&VKuGD*2y> zzK|!hM#-ibg8<}g&W`Q{QDu#L0g+fBX&R0@1P!(*1d49@WEI)EH#a2yp3b^+m{KVa zQ*W4fW${lniw8*|&}x~*7*F{0-NlX6HnCN#P>brTnagF2+fabpiHh-c zwhs8OYWrfWA9&!cea~DTv-6Se{gXzalgdH#OR{swUG69s@cxh3S@h@Q0s@A4K z3f4(*KSy7X8C9;uC8Bv4#FZy63VC6{G%>orvqiyC z^Sl?cCEf=v1P4y9=)2NiV7m%rYALHw+fNq$;tAcr(4DeVNkR3N^X_MbD-6Yds^&K= zpD))PhkL=F?|8;~4O`!#*Uch*7fW{m70jB{8wm&e zuzK@$-ST~*|FV6?a6iKG_%@Yy&cNa&-MeF!Y72^r2zjN(co*U*SK`_I11H!wTpMTf zpVI$!Bagy?Q7wXE=%+z4>W1-Q{GqLGS&j%Vs9=+18>D<;^n320>TKQd*wJ%|N<_=e zFDGQ|6&0&ZgT>)!jQ`M#csr(|m;9Y%j_2=AxA)QTsw)n^c-(%9>@n_UWpL)ow@;>Q zGw~L#Pe)rdXaT_EMV2)NhB-FuZF8VH>14TY^vN>Zr5YqXN5d&l}!2F@P78g zP5%A392m@7R)+ZwvoOgNUbOCY)ZrI`N;;f`8(4kg8OVfGeAn?`<0vZiFP_3Pg^6uIpH0@G@B)#vEqIG zq`K}?Kt|_aqiV0Y5)sjRK=4P#kkmOL&n_V-v4eLND|LOu)}*9QUdTXD#?y0?f(z|| zIrG=G)qh7HXTXPpdMQ24pMFo+y$JSuck%7c?OvVfQc;bU(@_Vb_VX@!>3M2GnIL5G z{>#>1#5fOX+%h7P$@6#@3yfK_^(9!77|Mgmr~0BOk5qs-@pMuiUNpZwT3BFwbbMmW zs_l&Z?C$+C`Tc>?gA%&7Zycv3INlYvQ9S>|Ex;17hxCe+e%TE28fh8Lo+xxp!4H^1N z;00;u`T2w3b1>^G9=EoSWU3s2W_6ck4P4TR4i>j2`M&h~aXQ;Xq9y%wI<04l{U1+F zYgz%cMpf=1RDP-ljD1mRd1RU1ZSE>TpWJQ6onI3v^3QVK7S92h5j48zuTT!(a{I>%;a7Wq%p$+4})mF7WOnd`2Ebk6|dc`KLS=Ji8>+tS?X2 zzyynnL_co$?`S>q9x>CbHj%Ivba?B*edU>*w)>Fg4@3=|A*?~OrYJJR;((zsfl_ah z2Ld;rzH+EPYp0tfI_6&zXZAhsvhduc48y8JJ)6DUp)BI&rijLw*u+*p7VskwL!W zh@UgQW0{bXc~J1^zDt(7bx&KXccUm+(#B^b-jkLG&CpHJGw|w=ZY&;VlhM7G9jB+i z#}x;&94$A17m!kM51dx#8 zD`I%7Jg?VaTm_imt{~6mWOU5J?PnBAT)N1W_}$6-0d{$(?t zV6Yd1zU*~S8dcI=`qbB4!;%yGs-l&WG@ccPqc$(V-7)RpG*}WsNW#ey?*FnC(Y~w6 z?kFKlvBi>};OL9S69B=Q=-@T4G^)lhpdsop?w#R>vT^Ran1~8m1s3(Ld&6Wr60CvaiMJFYPLg=UVX--4`uf_2~gauvSm+yzyIgu#=2`&op1l>KFzO<(<1JDb8-u7m$S~F7ybGRC&A(m z&F-;=A4cht2BmIM`Ch696wM&oRmqov)b}^0U)lf$v?*vjek0}YXe#KqIoV7*Npt2` z5J3=ig%X>mRxPqd`}53hMXU7T2^YlBzH0@}3iXWHT-pYtyz^fOK~LcxST@!KymBYT z6%1CYf2JSj#DciZlD3@_gge7DgP!w3~pA5uqAGj-o$c^#Z>{jK@UL^?9tFza?y!z+SAiT3pp8`+TQ_%}pV7d(0jO6@p?7e4LlU!e{hh5aJO}>wXSkr=P9JwT$+B?b=qi$GhVoNs_T*%`qdz4E5h!y zl^{_#Gr0&jFXdzNiGZ8Loh4)tfzKojgJL4eX+1Y-?cFh|j>WF-8bpW%uO_}QQ%W*7 z^yP{1*aE2q-JcW9plD$DGaO-ueu3ryM_|J5g?vdi8z$RfNHI!!Rd~U)?O5#m^jhSZ zc-obaU5;cgTCbPKC>hC|gI2hmEf9^<=TUOid5Ot{4zO(t5Lv+D%lu3wZ8yiqyJOK^ zpUs)e0*(=_(~dndAIZvB7S7OpW?TchB@W0?O^FT!4CS>dH|Y-XcF&JK7?*itx%myO zNp)RK;?}nC0?izoW4guzHyQ!vCAbZu#SDHCz%03eBNzt7=?qT08;7-;{-WV|)^rVs zy9y&XVSiunKPW&Io%w44>b?UYM*z%L12ON+eM(I6YhkYPO*gedN4~q> znU>ua)tc!Mh&g5YU{Ugz4r6Q;c?bB%f>}V7VsLgBiwsgfLKY&X;nJ2EqXJY@w_+2P zi`bTLSC5i4T;9cH#7hK5Cv+T*gcMJ5a)?{2iIThl`HT}9zMb+o2=*y(9^p~Bis=1;0gE44+G7C;= zX|)QE>fk1Le48;rscnj3*Hs*gGcKi?VRi0p&7YkUyKviwp;?LY7U9n1J{v{gK`jr0 z_7Hzyw8x3NcX6v(2#4%PGx7H|vKKVhG~Etfgo{Se#BqLcmwo(l2{eqvL2-JEp}9|mmD$WA`3(E9R-&yv|!*DPuW&JC*$7zxN-4WtnS4S2c)Pa^*k z3-Q!;A-<~x&NcdDk(*e7K}95(k1xHHonP;i^NO%tZvj%1Q$nwwh&FCBE;OrBnge`E z0Hk{!IOj7_gob8ZR}A&E`BZj=Innt}>_negyzW@w^GGez1?y0=1segEhVjxvube?n z+L2{O1%xUrQwRfM8;RMcuf@VW%Hp`2kb5w zH+`rB>;{+<$S&J*0AnsipYa0?Swx-6)2YDXmUHP)2^lW=EOEHnrH;b6pw* ztSihjU0a9fnYXdXE>}t1IW>w&yW3cUIQ!|A2!oS|6HDKTtqxrwZr+dJtH0ec^1UGJ z21Yyb8Irxju|@p3*a81|MC_Fl55151s0ut&Aa@{IY@213!kdNReWla+_{*$7iOop78^@w--qhQRxa$?#FCmUrS_KVZ;HLh_0^J>!ZeRS4_0%=;w~8zSuqK{LPH)2__GVdJF~uarU#y3$Kzk ziIWX!ZFjrBpti^2E)cV->PTySMa3wo1T0@f<%?89$np;9$XD3G;P8R{HzP6Tqm#ENmvRi4LX9}NFN%$(HHl2m}g$@ zjUwXu0zI(>my8YR*vf0u=Wr4dPS!)5-IDM7>NYc9$8XE3!lBF<+TkaQI0vm9+zi1n z5@^6lbKyNO4yerzOWTqXexj^^2y5rMY^iGEJz;;O+C9qSR({+Y{O*M;ND6Jqw}V_A zY561H0VEf}Ph}zt$Itv0gBeymw}--S*H^!%k4t_V5aggYhwt<)xekEmP}9<|yCV*< z>k}BR8@Um|onPBdCnlc?Ql@+Cyw6Q+M&oq!{6V6u~9V8<+sq}@HlCB@2JJO_r_Ec@l83R;#xagG??HI?%2ktbnKax^d}g*LX6Ul zVNa*%Nk4@`nOiP`5hCYw(C|waDl&fF=89>#_!CXoq?844lbGTC_eCl1$mX2YS&C7Q z94bc6G{tthYH8XUamcKHDLq~rqIF47dH{6d-6rNOcWs_72ymvvko7TYN}VAY*SmPBvjf4yK1G%i~IClZ$qG4&TbtilgcMAye$m1tM917MK zPL3?>Y1Y?9t5z$q^|7hGLSgJ-uj6K#9|AJOKT;52;*or2{*r_UoiWKNY>F0EY-;eg zd}9A5)~d%^M?E*Z!`!k9pRdL_I}<_+EEsU=faj9fJ2XuxyIdk`|pMLvshCY_F4d>(Ybk18+!$+qvdU@o|4 z!$~fL&!LV~B9R&Nz(O98dGLbs<4MU_OMhv>tDCw0z6-rp(zm%;W#ST_?Pe~rp8CR| zpiKa+89}!2UKH5ImU0#=SX$)*Q?FC$a3!6Hts?eLKa(hL4_DVhr_^(m^(6TgyW=Z! zYkhAIDbetdmlf{!do!WNK~m#vfCMpnG9TC1exM&dkcyQq%UNjidGHjKq%S%9a0xf1 z%74U7z@G$84YD_<^tmKi9nzDEIQB9 zC#-EoA1Ut~b6HKrBxX5uo5|^nlM%abuI5!q>$vag*Yr1lI8*}FFa^IzZI1!|oz6l| z>qzNN&R~g*x8-%0FyKYvwB3o~JDWK; zeZqC39?96;(PI$LZiRD8FT77IG)!4*ituFO>4^|Bkpx@>oI#hyugN%GINAe^nnAn- z&|~PyXM^GD{6v175_WRS0Sn`JbUC7l7?kRgd_HQr=hWGp=7~!I)u$r3=G+9z>`oa= zh|MseX5=Et&P$85UoZF+cJVbO#pJXbsIB~LwVcg=a%feD_pX~L?VxzhQv|M#%7SS1 zQRhOmnr0zdZ-0u*1`x}dSIW?@B=SNa@PH89_sRp?)LeyhXKmbeqDL_?Xq%sj z|N4bN4-erEjvI|h-qxH~wsj6QHfT^X5e_@-jv^iR`SFusRmWwEM?T%ReB?DUn^t$R z9CF@PdbRq**uZiuJpu}?aVKpcVz>av2ztwuV>Fzh^1R6 z8(382PDqGqFNB*iiN87bjbCm6f-{E^#TGZqNTR4fG#}cIa#WSsoht70wC$icF;7p* z{L%RY8N#kElZv#S+r!7@HXdx%SfBS-9nEx8eP(wKN{9*FV>_F!Jue zdTYp>me{V4W;sNQ#WR-aE>BpmEY%!-MX~3Ui~cl^<`R@B1xR^O;ovcBF5}%nLrMvc**c$GJ1ECx@Pb8LS@Ft< znU!|LS>ce82Zzhf2Qr+rH^1Poc2*kY*+v5OpB9j$b4G`Q(!T;~3es|)0RSG`0EN#= zXRwnJQuE-EkVt*SFB%I1_E3bti5zn#7gDcktM^L^8(0#ZsIxW%5+&)Q`lUyH61WAl z({t}k=NXHh)tut|2QBt@;W<2@}i3O9=)|MJ!>kQn}5+_wau7@#10y z{^#fVp89^6U($d6{K@QE`X`LH(y@2xA2^@1+vZy)KP(z71v){W9|F~8#*6=68`w%I z8u7)d_8^ZMTJ`xW$^$W2MB|mTxDIr*MxqW(T>lgfeod?JComoIYv{j)i_VKBb7OWQ z)EOt>fOV+zi1kxH9emhFk-~XtN`{Ph(%99mIgamXsIK7 z)ydc`09qUK3#j1>sOaY>!h6SNEm}_3Mv9p-$@JP7bVpQ53p{WFg9#3_^m+l50fQWU z{ILpMooe2<@p9p0SiZ$o2j9GYRgAozl!)NlC%o8@_$#)rj*x;0&5`#uW#A5=B!#en{;m&lw=D9L)rm!joOSNzGx&unY?Q-@U6NV+ z=$-EY*{}MUWzGnhFB~e5J=fXnLj-V6x=G>+1{#8>PJU5!-11ud4k2bNK-U*j6YC!= zSm3Of&~l_%Y>ub5vB38G^-mI_T&j@SNQpJ?S~Kz~Jc`V=x4Z`CfE{WIJizBkWtbo1 zXt1Gh`PkgYGnaTgk==H!G#2(Of9P^whacL&mMw*dNNO$xx12}qM1k9NgS-GKRTwT4 zJgd=?ANsss7lbqPEX=J#ik_{$>i+OB)c%6t@&hT3y@#-zA4b1VEDaEp%akyLtE>ZI z63CTua1&_okPw$fha93cjB&kQKl;uZ>GFoY)0bLp(<;*hzaFuhWR%D4tPRGzfQZ+*gw7zLjAcw`9YQ zJp5xIGT^fe{1n-n5(ag14}w+=MnYXmh+-h{k`wG|f8z=gk8Jq*x%9K^+dZna?y}jH zLXU2qeAUy$edbMttrUfpL{A+=)a(UOPx*8$4{rU$D-O0>Oh(?}_lF!Mx_nku%F;NY z?!$#K{yf0JprWPjQW;W?&Kolv*m`KTzecDh>SCctRCyS%%@_AsGG=B?@g*;-iWoYb zJ8i3D*X~p#Er=z?iF)vO)AwebyNA)iqUos&)K(PEN^Yu;;64JiMNAiB<=R~J+vKb{ z9;ME}1ohkYOs`bUd0h|{RxX;b_my4nG4I^U;gBf#Rv*bRW5NW9rGmIH=xTuS5Mn9u zGO+F2%k4_scZf*?x-t6gM%t@7iVZS7-h?(*m)E*)vZQ&RFIdpaHORp-&>ZNcH^61E zK+M<>R2nHDo8u@K)qEJn^iA=k#kc;P4lg-{no?Uu=Yo4ZE&(5GB;u8WxZ|~>8N|g)ei$+%yHfH2G8WC+tN{NErQ1~JB33W9fOdIx+8L)2q7CfID46#yHPt~(AH+bpL#(=ct@s0pz9d=1?-kw=GQ;=@&H5W#1$uXI$Y6B{z|lF$g*Ef)=Ub-x zZ42j~|Dd{ASJS(aHP^jS$E@LnA!TU_fLUoomZAiz>Sx0j(Um1zym%K!zcnz+WBG2s z$gy;y&rZd>AN|NS@J;S?JZHG*OU0r1o;v;hO8k9Rk~E@4hAJCy1V|Uz(em;HkY>gmj^!fM@hu@}zrX~wKX(NA*aS8tH=75ha|%h1>_bVXiW3i!bjwFU?yB1-%rug!W0T}; z@Ile?2qw-#KRG)Fm~s^?m&0hyy3}+9q#$+)Lc}eS;UZa@xJ;7riP9MY*cSfDGZXR; zOR`IHt-79lNN72&8zey+?Lvd zD)a7O30$l}0+tj5Vm(-O>xR_N-}CWu$54ldXd%!D0 zIv#L*EM;Vf;j}vkXKLC}{A(jW-5KFSH6XsTFE2~AueE}1)^JU-&ND_TXnUqHtuzMn zDl?$XzUB=+(tQ!+8ZFjLHUj4p+1A+#MFK6;V5^8}vD;>#JH0 zJ@iq&9$$nRo^)`yS+}PixJ8~3YY_pi?Gy{(q3*zqE;UhKkl@5+9sFMOmx{XVV29Yp69Qp_GO0**r;(KAa)iJxyxT zywv*%&H--$45gqW*n0_p+eEscHL$H0%gE!zdQ#T{JMv~7^15ZXdWTQNZ2fEta~VBJ z;M^xx(F=gF=whz!`2!U7s0N9btWODn+VzzL!MJvQG-(^7+Wo3a%fGSpDbxm04e;+&O!6uOe&G&Kt1}Cr23ntgy9*28!!Ye65oR;{cxv{ z{Fuov8j^b3XDgO#A+(2P_06v}w>7MuF*sTHeS2Z&;Ed)8*Ft(^2Mh5MifXWd_ul}+ zNl}|J?RF;dAzIwXMkIMaEUWaADnUFBXIsCl)|`|fS6kDh&#nDlkFR9hP;pIAWLgh)_`gKzje59;X$2~=h z{`oNJ8N7>ngwg=D*Jq{nH(zc<=|-8EXdb zX!QAqAad=g+3&~_;~tT5tc>zDz#VUc0RqQRrZ)N=q^fFa5HWXkf`RVxd$T3;FUM3r z1)Y4NMRxF>K~y6*J4n782wai62)q@>*_n)mg~h2=5s=tAhv(ewrH#7WDIb^~*5%#1 zcz9*0Vw*wH_z}e*6Q#n5Y(q|$5KdBRRS_g_M-3HU z`Ae96_T3o?>LtF&N?p4%Z|E~$1$s3EzN4RN=Z2!68G?0W8QtbS6-=0HqJ&T{1&C5U zs7rM$4@RJQez-mvIldzg7aCd_+gz=QTo^r{5X~yA|s9$i&-keOmDl|#(lK-YRTg*(&%&==`nEYY8hD>+x#svAx#k3CHDdi z5_UX}z#-B4ie5@V@GA5j@$C5_sDHC|{8@tR_}h!~^MjsrAI|9dikkIxTHVaux)e|}nTfsBl^$}7r<0{W zE-v9fR`Z130wBdi{gL#IrixSNAcx6kaDy04YH}XTQC)GkQO859NT%%CM5@QD?(p9} za{6x{c{hnusDZ>2U;sD1^P{#=7PqW1`E35?gs+{|HFM7! zqLCtpWMtW;lIi<;U!Dk@{Ki$7Yrf%UH4yV5-`~GBgZD05c6MMPb2RHKzeu#E*zDo` zJPHc&07{fu+qA)Tt`Wp2Z`4l(C`DmRXXS1uVw>)a{3Pq;TEKcoZuGeGaFyQ^QMr7S z>Af3M!IIq9FfuFHCk1~m8+1P@$OM8Hv991->OkR%x)1Bpo;f=5rK;Swa@9H=-+PF0 zukqht%o3_Ph|@Gm^E)a6K)@0;o59>$%OqanBEj_XyWz8{dhTwn?kUU>$Hx=fO%0L_ zu1eU37~=;Ao|Ua(TR zRbN&s=hN@uI(AFc=-hUHfNo*JuR?JQG+SOmy{E)`G@ zQR*s56j{|X7#JOl=(awhW5*XYKd&bfj_*8uo+r>FepK4wlF>x723~wq378ol1=$P* z(gMKb=$i1{K|2A0c%!>%Aj$|)^^;RAJpKfvI0-dShSJi>Mso*ucPI6H(Krhc&8N)m-NV}Qfv(Xn zsukA+>q_b+az5{POzn$wqRM~PbhuAF*(IppSN5j0%9dI%&V940wV_%ST%C@=E;6iC zPiAhfbDwM@$r`1UAjDE_4Yb@Ajl11|gan)&ZswnHyg+=y!e%zR&Qd(@AO zqfW7t->WpUBs15>Lf)70$A{WfwnesM-iYUO9@uCKY<~+uf~bus+4Tt(x_A-|4ik(z zsm>qZFxt#a9p<~6{L1IDKQ8(FBf{;lEybsnDiV%UWyiW2F3@|~&F*uOY{)#6B2i$` zLDcR^Sk+!fKLpFz{tyq*jc;qs;Hk1ygX#bo%Fy)#{4nzE_r8_90gP66w-u1b)u4`n zghKLW8_Ag{%o2vKVhr9So99~G9JSbAz2Q(DH|Z>Yu@&FX`moK#qv5dfrxFH$x>-s< zl|nYJVyD$lQwKnPdh5p^YY%%HrO5ZG$A<-BoxPX(o-)n1F>O3STmp)kckScZwFgjD z+CVt?A<$xMY@{?pmr|+XI6}%Ygv05Tlh8TbTJ#<7Vb3=o6Yoqso@ncj#c~+Dy|8kn zP2#NR55!agApqRptEm7_Jj<3O2|?Bym9rhI{Q9H+;->``B_>QC%}1WOxF>Wmj~{En zZ9-Mj12y%>>!p@+YaBuH^U=m?2djE!O_S-%hl*vZc9?Y^@f+5U^ACJq ziG`gBH?TO|FnA?X5hk-E17FRI)Qi>_n&e;B-{&rdEio5>}^g)t#ts3@tyyOI; zd)SHk19d=K@Hg+@u{i%Jgh^A<4vGCmQ@Xa*kJBd9;kCB_(vz+eFCMa7OH4RkPV8&F zuVk5!GjBC5o_u_zGAX!U=NcQYQs$QmE|(D4?7C#W86Q;c7tMnm;Cp~=#~s;dhhHY_ z6Vn~Avd$fbZSFQ0=<$@W&>xNuy9N)S^Y zrzB1ArM#w|f`3ADs_HD2um)U)9oz7e3sSH=@9SPb$+~;dg*v1alXb&%z&apHB$tNa zYByyKI3m2npYMUdccT#&2v2(Ek=d27Np*J4#fVnL)Vnje>8tmqyZq_9X5PoqMbRPo zx+7{iuW4!eFt!g)=>oP}(k(=bIN6hQ1U{U)7HEr4?;>!aFh!QPQV*mhpHYO@2fX|V z&i0<^N%J`CmxUK;jJ5O-S6&QGK zAL8_EI^^^DHecswFL+arj+|=$t)209kAUzq3DzAwVO3zmg(_oxZj|gP2x^om=^`|0>D)1diRo8JN>BC8+s4 zwJ$@C2&m8CW9p)SJsPjQteE43C@Kki=l@)IK!r2$VdUJ658ojRWxj%(NSZI(-W2Q1 zWRViXKYWQ)h{S5$`L4}>GxGM~8ViblW`2Ja$i1Nmy_C1| z;1s(AGyLM_Xg(}mQq?YdFfZq;zS8FCx#Ju;N@sELQe{?ckLhe36!yI_gHwe-pA?uF zQB1rYI(3oodZO{D_eUt;g1NXfZ>f_+xRhO49b?0i&kd9stmXILM^p@$Mx*9?B_$Q! znUQUYV2h#KAWmYpV1$e4QBOTqc7hiPN*1*?&tur=Evv4AKJ2L5y?`qd@=o~v%I^z{ z4W$HXy%_mBNOc2wK0+GK>wS);Ba~rBRl}pmazXdP{oD0bO^pUof21~t6@SrOxPd#; zx%>)xTOunx1o?Ro@UH((jRLj^C`k=ICB%;&S;7srI=i#A70%x4yRdkBrX#_!jbLkb zU-8)51>g9tkS}hkrk_#Vh=+r-YXnb90oj>!7f5}o>S8WB;dS2E;~ieAde!LVrQfEx zdw)j9DS~reXCY*WOEiAjI&a&PL$b7NKMSl`gRmOYwh~sK(Pg=PPN^HiF*Ht*9|bv^nanrtr%yH1^5q*FB|-K2Iy*nTIg58tz}a8|#7+kT&I zt_R;vDjW1<7nU#LyMdvFp1*6&c*S|$LgLB9vifaG8r0#rHc;_a0~AoY*k?ml^N#Ri zqh#5FzP#?$qe2TkvJQ?`BT5oCtfRY&qS@ZzVNnno!~>vC$g)|7a|A&=1IPp`RuOgG zWw)BbqH-#m-8syi_I7n-a#bEi!778L%NfrW=pQUFKyxA`^UKVrj-d4}NS-Y{Mw@T@ajpIJQ~SA+ zPzj($0%3r2H&SwKNT*|EkH z0Oo`argXjYKg|YIz4*)~uyrDft)=XZ^6oi~=YyIY#pg03C4bW&{bMMLhW|UIIZz?| zyXmj~p6~us!flB0FFsX?S~)4SiRo`zM`I$0&o?+*Pl zc=R9mo9=JP(>IYC_uT)|!5XOgTQTk5bg-KI%?I+dTh9zNX*m7|ex9nI2k1+$<#p5n z-{i&2=OBz!uNw8~qH4|}3lLwO?+rxkagz^*7@b_HCHNut4@AL1niaoxD;7tO0m=nj+`Z1wyJ7y3I_ z7-bD`);X6HIk|WaJ_ZzW*Z$!qQL0cv#36_*)R)rme|;^fYp+muJb6yRWRiLflxP}% z(HtDN047{81D==#WDl$wSBo9DV=}s6njEHvf&XA0vp*Jwv_fz$HEJQb!`bnV`n)YD z&VOe?6#Sw&Z!sf22ix5+GoG8^7tHyyR-c&m&qiTu{;yU0(f@H{@ZAI67|2%~hco$H zLkN-0CS}NKS(X>Flf6ei8t%Y3uR?v_{4k97KepchG%Gob2ol6^%SRe%acHbi+6QMP zwl@zdU+FrFey2?@d!BizJv%l{$)W1b%hY-cp1U1}FUOU~ZtC04=#i0?A2K5o9%nZD z4M86*PVaO;`Aa}n;`rhAi9L12k@1<0n`SF)og&8eeH^Sf9wDBeCGVYTtLiW2nQ*c{ zj`#8!UcA5XUX$aF{4r_n`*`uN09k7VDEs29Q*}qsQMVj3Bag32%7?`nqbEXQekSGH zapmNGPUhM>Qm!d;OHh!<_Cjg*so1LmeDt3Fp0Z%jnI%e25O+vAWEM&appM6)W1CNf zwbjGldk}Pe^K=AMXLYv@$v;gm&=(OfE&l2^k&M;j0ySTOR%|cz$hPZ>!4;nzgYHiP zLU)6^`_)aIXEhW|PPnDUuUZzaH*4i^7~U9?$c&`<+n??+=oqAJepOSG_* z3~Au7^a9W2TqEal0&b%n!Yi*ZHliIuexcT1uIk{-SRX20a}Tm}L!uOtTWPAWw8%@; z#)^kL`$Z$@!(>!7QE6pCS6|*-p6F0h8+tX-=gT$Y*Rd}JOm(l&bAHd;QYN{>d(mZ? zUy&p|V((K=G}N9(AOv`A%}ourZ14m=39;ZI=$)n!%)+c=U<2% z|C!M7-~2W*PvmHL#lal!Lt}9GL&@h7PVh|a5KC|FAH(+$J+Um&seJ2BBCXUb8LQJm z7psEjHP~m1S@yCy4^IERkJPACP?p2A7gf2o)kKn_c1C4Fx0IHpVMke5(_ZAJy~$13 zVi@h)E&I|Xrs!L?=B2(T_>K0b)gRYsa9dfP#_(QpNC09aUK2Hr23D2Y=YBUA++n??`7s(dVg*k zjK9YIC)Tff4$)L$A9l=`pUZ;kc+s<$<2&z1Wm31VvJLuPvJsH5q-oev(Kd>et|+-o zEW@ENdtH#Q8C2_Hq=l_Tby(qb>)M*vaH-xl=Gf@NBKgc8Y3W$?EDqv~P>Cd!r_Y+Z zGAGvvNYYjEO-fF)N(%-$owO>g9|mV0qrpQXvm+dkck9A-octa?HtuqcN+hL=D7UzC zfVO&HN`0eNFyRV5Ig+!PV>W)2xF8*t0m;yderC+tq)TR znyHaLm#jwg&uz(etiR6W?%?3oU=ykEm1bQ>HU5cNypCvsg#yohMbO2f@c4Vhtd@&f zi*_sXKc^i%#|$-X{j_&QKsPcyI7D9l#xI%^aIv0Tm@|asnb{ihPV7)=EK^t_Z*=tG zBaL4JugAR(yc?T+7{llA68OzAi0b*hkVZLN@*Omh9XoZ#3>$0q=6TU9E&HIaU0`=H zZOrEgk%XY5pCEZK++FAljIczBhl}{=V<%ggXp6~`->qRMr*h(NnV8wzzt)8&eZMMz zyO61~XFRqRPfI>UsZdu50FNaof|%B(&JGX|NAG>5Uds5=MCpHhGP_1sIAjVxdpx#a z%Em+TPNZ^c0@5TX;iXBRl@WP(zuHrZqbza7PEw~6_tt8qs!p^D3P%I27@R>#1iBm@ z-pENhOC3D8CnQPC!A=!5)|Ct~J&ScZUax#r)TTf6u8W&z?75sY;YN)m6qzH9&_Glp z-6cLHn1L&L>GSkPrTT@F+S%(fk|ynYPk1tSnV;&ipMK?gi$*&d6E1ZX%7th#h1wx` z0#u3VxHJI!8_&E?oNP%&ggtxC)G3$O+apn5HYe`7{3glHb-1jRmm~C4=^#Dn;#YKO zN$1YYa;p#*C03o0h%&7h-Ek=?QRp|dNF8#DVQzkydSjXU{OjmbQbKnxpu1_7QbMHQ z1BB+-odX9gk@KA&1Kw{b^S!=nJv$*sDv(R1>?fxD0I}dyFADdVu|4@|pllVJ&K4=k zZ{3J`%!vetPa;4EEl}v$X{{G+K8_2P*!`Knr|;k3^|^AARpO$HbzCQ{X6GJVBnE!S z>74T-?mjXy^~|(g7owQ~+fOX&g(0 zT$Qe*@ek*IHpE%!@ML8v=F+S#iJ2bZ3Qo~N`cp3g;u0E60}2nS!$iwGzqWL;ujH0K(=)x4 zpDIwIw8It)K8o<35=IpWupg3leC8fjKR)MPU@`Z0ovVJ6VV(8Jd{y-6JJ}U#0m)uO6Zdv_^e3OV)4Nfoj&3eWC8GW}1mzv{s)n)q@S|U>K z!|!OZFoE%s8i77S#KLeoR!p6Xe2vPSsOYUKe=B$MQTYa6W0#LW?@Z*{aAU$H+gP(y zmzr)}TbkTP^$oC#h9w|cAg*w>fTJVsD|lE-_m4}(E}~O>1U>b!)-5;I+0Ulo-QCfg zQN=??XJ5azQs7Fgt2+C%P*>38qqw;7@NLP$rz+0d8`b0{UR$s?>Oq$Z5G+(KCo~8T z+EH{q_8XXtLirP}nZnX9E7L#4jLG&5@+7O=(P8a;eDgu^PA%FELXVY6IPOkV z5TA=*ZW&&kp>7yEHulE#!3!HD28^($eY|3v^^s!Qfb{mW{QGmbl1Ly`iw`(6L_RIo zg68%?V!!VzavwZdzc6cY&q_Na>BeVQ(U$BMVOIC=Wzs{35Lcl~xrlP{cGPh|gmM@z z5uk`Oo(Ohd;&;v&mlt_5Q(12?b|!XJ?))@IH`|pBX_wX{V`anCI>Wp{&NHsh<%ktY zp$@SJMZSiGgN~ZpU$C>L-d`duVDq4XUHvIoVo+pz3c67bC)d)v;$kMxiF{Q?yQaqQ{yJkWlQY$2{5n@V5hc*+AFMd; z7SKa9bXX6Jw$BFhokwra78R<&h2Dz4v9`KgobCJ2No|38^B0ZBMx~+VV~9QQDgT{Q zVEYKN&VnIt+yp^%|6jb;*L!N8Yl!-jKQt|2`%D^CkjMF|FgbbJ`4`P;N5lex!cc!O zk<@i>4-i;ap8Bh^_P4Led*JShz#2P_hf$9%;w>>do8Ob!eQJz`2^KAm`Y4ZM{rvM+ zdFZ%&i;~R7wPh?FdoGn=l?WVvXdAkZARPLi670Z1LVOr4af9uaDLbc7o!9uI!pgnQ zJhTlJ-!1x5-TfJ6A17*e=KF~j$^@e2syo^lXxwY#y~7dKO8P|e%eLnhoL;H2&QWqs zC-PxAVvX&bgjg>2@${*W^9(o>_lOl>AU+Ve0qpd_ggQMiZ7?bhpp!I4G0D^dMyRY# znz*3t(=s6$aXx4Kb(!0(F23rU8G&2eC!t;ld;ODCzzm^{27EOU>dFBM#1KGlkJ&Uu z7COdvtcd7%!iO!2-fHTw=GXG)FgIk1X-VCXhNT#S^E?DwbrqG1$m$NR;1C|shGv)x z5NfCkPdj(rl*vv8OBZ+dzQV{+f$$M?5s%ui^RG{#PEuY2gaou&t{n*k_d1}5tcY-G zo5f^+p#yf1$p?~Zqpp-T)}5l8l|c&N^RmKWfO(=>>TSwxo3 zMcM!lEDV`6qMf_wq*I`8_t5iMxr;Y*>~5DxiTbq}HqrfyHLJPthkry-Pbg8c$V?Z7LXxKhvrC8aY{E|% zS>Py1x?>44OKJg7Z*Xn|Bx`ulD*cBd%i3*ELp-*eRddc3Bs zOr2PiEDfoRU<&N41`g5)$DnRY@}?d&T%t(dt;0cPuoFM6?2M!4%yhoY zaX5)Z^1Rat5AkO3g~U}k@9Gm+aA-1nAt4l;{Dlv{HYlJwz_=e-Ktro!q2>*E`x-1$ zl){Ku*&F>efZV7L8D@4`(b~rH>3*szS6QE91fF2U_# zgV&WV-_FU&)RP&qy3gNWAFGpbX@0p+!-M@sEdSby*N#jvYUejDA+KfCdsNWDXX>#P75D zY;>Vn;irirdbs44sJp2UW~=A%fctja*N%Y`V9I@RSuKOtDh&!!c)dznjEMdK{Io$G zu7;NBZ`mv(ro=&L8Rlsv-gzH7?y~8sd%ch|eInubMtAb*X@R2}5)oOZ%=(w^@Wn>6 zY`X!a#!r8{H2wAc|8FrS9Nb2WNwZMEBO*wS(CrY5aC-z_-H1lZ{I4Y02bJQ3wITK~ z+wY3+%^oadI*)n;Ea7woc=0YdfDHm(TI7gkU@{Oo=5v+La*I`q!CH0&UmD{Bb`oSR@XA~(2jCJJgW zQeA$=+{y_tGcsdo)+4ygXqGG5nQU^Wqwb=>qiHF>!a-h>APUs-Ufm6y5^Okhc zEQ?dl7CVtLWOIx=%YwAUyj9nOIBgH{B;o@%VdhiXG~L6eA<|j=@CW_@2%H&$D2oVb zrn3Z3`AQ=Ux6@JsVJzOZ7W>S~**dLz>4m5Dgj;26AX)-sGt~v8p z%fs2PxV)faTSoM|+cEhBO=bhp_G=Bns>tq{s&Q)XBF+d9B%!zTaDB@xo5KYG>LZQk zNm@R~XwRmtwd20-#kKfyR$*_PNgUHXR&laNMf>cE9nA^Eg+^I?LOW1c9wbTuB$5Di zc;Wc?Ped4+17cV=;wdZ3=42nVj+>vAe{u`oxg%u3AD(*s@gh{Z2C+2>8WE|-7)j2E z7R^S_<;8IHNiu6LQFp1MKo7{mHTQUMWqGTX2Oq2aeoxitNKwn~jD+#6=mxU~`Ff6w zOL=5|c7KxfReV%?HJaX4KBbvg?|B}g)k6Ve=+71|r`|&_u+llR=iXYw^9{Ce2EmNN zxuLiCZAcu{33Vk(5FASMHXaGETc(%{bdFWtEHj^XI=9dm@kB80)9VZ2sS1zVcSBAW zlG1Y2ey@hF{ww*Bi=K#mM)Wx1O_3}6w%q>DkLfV#)t#TPRcXuXq$uih=VJ}2Z8nu) zi!~)qyoQZbxT43Y3OU?AK1PZC3Aj1@Yy9s_52e9=(R7g!M&$gW&t;HSR2DgG`0uK9 z7#Dr~xViT{mLtk{ZkFS$etwsciQ_vuJJUO{7g*?oe%>GH9cYyk?i3AGZ?G}Qf?frG zJybAnX13_cU79_6VrAid>2obVzA8-`PS~aw>JiEPb%xh`v;A;`d#)e$BanT$N?%n~ zz_VpzHWuG5LBr;kA>}7=D09jTP!VOHkC<|GIu!H8kA0$OSd&JZ<6H|*B>j=BcZM(e zb^db&723GJWQCOse$f~pSk^4O5T4Cd|LS?`{x>~usn_*xD787imqS{+?MHNYuX-{T zvlyC!`~MJgQIEUpE*G9DUNNPb<)Tnp*;}}RUnGrWytY7D<(&_94}*?$pE?-*`tDeg z3D;%U+N#m_u?wekj`F=U&@cUz(^ytE_JE}L{+K@Rsm(Zzv)Z>=I3GpQ&~zOtOjFUS z=k90YP4tRTjA_w#`PpIiPs%|5m8~87G=W|Q{@N1s;AGLkU*6=%zM1%<2moGx2}9fl zw`(Q}Krs8iQR<<+X{Dr}v8{H^QN$NZCmQ{(K6sMxLt&-i6%MVG+^FROCZCWV{#&z* zDM-Dk0VrP+7{eZsShE?)yBA7Z=xk3uf7Fy15}W2G`|PVtO*q>%t@j-4o9CqFIcQD3 zcXk;DZ_d1wRJ9E**JQ2}yrgRU?))c^+%9(6;`<7dzhQ4YBRZo*uU0Y1X&os+^XDx2 z&wd|P0^aB=e~RgT3^;=npgizi>PM8 zpXCGEzg1*17TvnSW7-y0a(Rs1^}Ed&P!9i5*fH>YBWN6BhfvTKNp*gDdHBi8%=%n| z%s27cr+NiOH=eHYIZcOvBy`X}u-tnn@^_Jdrmu-NOlWyOa;)l>n5KiWdA8Z;TY=QC z4}%~1*;KBkBA-Z`}Cl{>AyjNB%^`k(^JEr}5Bq7Yz^k*6olNRL>mo zoY?&0Y>+p7^dAPh|4{(^_kPA1Rv0~fZV#+}k6$t9J%H&8thi>LuIsa@ma^YWF|>$y z^yXG%$-i8oHIHImu9j3xZ<^DcC6>A#$Ut4fIA2rDx)hJwsamU{Qs_W#am9Z&^8by` zkJ8u<(I3BM-_uq9yAXMB8FM$Xt-dzai);Ag+)DUXM_F0{Qi)&XQDNbdkje77Lo*TD|q>&Tr5)Y|ICfFeyw^q3|k= zmL`p_R(_V~TwE&FjjY6~JlpU8y#IqTor%oEsqc&yGpe(uLKFQ( zzERfeMqi`!cYnNN5mx63{AuA%7RLFR76W)=Ny$F>1GOIp+ub){j6<}_G3G#UQ7zx@ zHvqjl^xF3ro_|M@Zb?%O;7)6B(?t>x%F#9~?DIImcbh+5(3mzq|G-H-W2R~dx^Rd4UK zd9bH9PRM8QM5zQ(6Sqdqg1ZHs)qYTa&@LlzF&f{1$!K80S&U1pE4 zf8#VlfaQWR<4qPZh#lVmwTItp3G81?L5gTE{Fqz855nAj#HwnYDK9qAxO?;bw&UT* zQXJXwDw9=5=-V=UY+{zXeSwy=HIp^JP$ey$*FOJq!h%`YY^r|4eDG(5lo>0ODlI?Hd zIp==reeXN(H{Km@ygT0cLjz>*wdR^@_BGd>hbnXzv;9wsNC-{v%TS`}-!^>si(EtA zQ9hp>b5xjqZ0xm7-BQ~ez4Pu%G{@sg4(pq^R&cic&<>+BzijwMmC&CNdqi_hx}HJJ ziL8-qGN{u5MCU((@&644aN);xFWGnfsp6nN^yr3qh%E5%UJ{PA>fHL#_x68C+zYaC zN7o$_#-V-9H_G;oPbp&s*iUrX3;C*iv@_{R4-Z5UQ*0;sZbbGPt9vn}7$-P5e7ciW zcV4pWCClsJj_%bCmnge=gNsQh)jw@P^k2nC_DJj)O-g=S_mcUItor$<%1+&#vBw%i zB{yt4eVmrgX|kc)P)$^;KRs{b>L;e7G&?2pghSqKbVUEt_Di?U&Ix`xznQ6rZ*@)2 zPX#ds=9dj-d-__#atl+I0cWuM@i$$XIx@RV&v2fa1|s^XYEwU9Z2l#y+#9(iU>Ruq z@qX68A1|#cxj}>`&wZtJq%Uih0mc$Hvt}t2;(xrB@W&Xaiq+lUQO>|P@Cb+A0rvj# z$9wAeXD-dbB{m8MaZr>a?aE4Q}CwYtRsdSVo=SI`2N1JT$N7;PDY8Bqv4CCUl~Mlr~+sFS!x5$r|L2 zSidJCs^3w=rqLIqq}xV4`0H+Il=XwtBZ4E{101+KM%W!$q2|K1hv@qE-WfbLZ)uh# zTLc1ASg;Z|KMk@(N#9XC8uXq00)M>)r%&ILk$6f1x$0dS%I|!_LDGH&8=1IF0}*GC zVIPbX)o1X8M+!j;N7O*JxBNrz;D08|K2!I zB}-=%a=D#d`{eHtp*w8{9bB!9yr-P<8-Unf;U^lr3bVhsOz7W{ku#z;7*)y9F#b&S zm8}aPuL+e+awS~D(h!s8EwTRCv|8z2*N?@gL(;89JhVJ>mnnH?||M);9 zjjZa9tm+TQK+WG#*`;RiN9toENYC01k-1oPWvW3--z&~RAX0|p|4vYI1mS$4e&sJ4KgIT;&X12ie7tXcUq{n!lSW3 zIygM_nWh>dUWDcK7-aVocV{^Bs2%CYFJWtQu&-3z$GqBgYM-somL*~{`nMr}ef?i= z#H%y4KC@6*rh?KpeM9k+>3=}vNSD(IgciWk>evCh7OTczET*eXa!ld9m#teLH`0jB zguhvO!UT^Ju}U>Zxb~ak5A@>*Sju}S_Mt7<5ZnL7p17tzQ-Bo?_sVI z_Q_GMH%zXVqzaMU+`-c?Q;=hR-%%kTU4*4=h``u{o!f||-}u)uUiRbig>m_KaNYjb z4M_MI(O)p<)4t$$E&9{2$4f|d$|O5FkSf1`@IPO->krae288ts2)|>&<3!3`*soKh zo3VM0kvhl7m>>HUgx_)SugzxrL63%G&A$PG{S66=%&}cVm`h<63xbWbgn#zpKgi+# ziM@zwRE_+>u0J!v|9s_JIAU&wfe?QF^5n0S8}&0;2L6k|ebe2TBQNfVp#z`)XOR>3 zn?(5+-3r$^RVz!@u;HE2p}>^z(>DBrzpE}l6*M^GKcpeYPYgcy@P0cGJtP%~eBWLF zKRDbU@M~eUT9@9Rf`|KG?8LM`#__3#^!)J8aDT!4Kfz9$vHwX@oqaq^tlmeO`yYsC zUz5JACLbQTC%jxFHiq?K5$5I914u1?H-TI!)*M7Pv43zY+1AHE$v|Y+4GG6cL59z^ z$(j*~<7;WARssl}_7^vyxmzPpu1Yr_>GZQ&ERvHsL8y`FgAk#0jHm+0O?z7NW)dhBKu7 zc&j492f9zTC8i%;e^rwsXQ}iuqs6$ZqFTK{Xlsgo{|BV@p(SVL5$;*J-co1AOJ#>) z>_)p7%YMQ4tICSE>iCu44vYqe-fqx2CH&bCT%!6_tz=3KjvY=D4~(>_?jigyDj(Y) za{GrV@QM0bay?e9GP+@I7SRUdlVg1NFyG941Z-G())cuQp| zUkLuw+^IRVYbpRYscF$ z9`6@E^5iDo)|6s*Ewq>Z5Yd#qJ~d$TCF?~&+GdbVcLV8#Udv5KSvXTLLl58ws|O~Z zikkLJ9u13i-LQPXRohXZ=E|2H;HBZ02@1qn1YU5i)vQPIcVq($TM32>0ea@N#EyI?q|3dj<`m z&E#4-uQ$yS?po!8muOY6V$$T%M)!jcb%wCaXCC!UKHcwEsT>|{EVuX6n={#>dm06a z+cF=_@4B74IMxU(@2@Q*z)hXApC^&6>D+~b*3~+&MC2Ig*`i%ol1GUnBu*H|etbBJ zEj+bi(J0u*E@=H+G<-U2w9@h73jts#o&*T!MLrmSs|Z3QP$6jO>EjGor_#6}-YLZ< zM!PorP)5eb>!Sx`EHqCXG8NsJo1PjH?Z3VLgVzVfqn{BV0{R#YmV!o2#2QR6A_VhO zAD4mRH^Q+%@*TEzc!i_kKz(YfID87Gr6b@I-?HM=C}NUN&i>u0PZzSKoWg?6Nixek4Ztl~fx`}Y1)U*O~YG(M#=$t#X2YiI8QAOBN zVv+!NEaF(~qGHsCO>E+>v#C7EV)a#=_x-x}Va659DYKd27w1Ck$-1c5c1Qcbm~p3dXlGNCM4W5V zmGIgfS8@;TAKc@qDd8!-oCZ*9Z0i<9=XOGv*{F&L^Z0Ovt6)VOCPHTu{h%-TbdNPk zTA@`MrI&Z(e)#C+CPJCCdPCbijurk@JE+_OvefBT8df2c;X>G9ulXv)V!fjc8VR;T zRNh`&u>fa`!j|h5Rlz&fZ*$GY(Ey=)_2Fv=3edHtd^A^BKiQ?A&$P>@(L^)d7Gssa zzICfC#=X?uq2Xk?%gAe`uHmtofj+eUT0vg2PQU=yF4%f#HlWZ=!*i-c@s%y0onFv% zH$_XZNbGR^rSr;lA5zC$Ym~aC9H%eM!p2Wqy^uuBUK9Q~^C?6^+6f;CP3vnRkWH6G0Dhs3^6H7al*Wc3~OO5IK0 zmE@28nhI+n10Z^HI!kJXf%#h1Y%$kI2a%L-e|$_BOTTCjYIOV~dhp{T&F1Bnl7b&` zgJoyXgxS=oTA+n$=yO6_K0QNzZ~0cqef*kb-%vZ_Iu=#%ilD9{kFsf+`-LLdE{d~n zT;TF~guFdnM>HqX=QH|!2uP;ge)wz5(px8~jAs@qK2nlBhx)p-d3$jN#R=8TxyG(| zzP|RLr)i>jU1sm)&n`hQNOj~Ped7T!Bw#`fgX|qY7~DQMMx=*<2*VCyeT?*i&LVRP zW0M?x9SHhZS3<-bU{E{K32T+OjYwoQI@rmVhKL4mp+iA_4>M+{kg=XLr;MfFCW+f5 zFirr%I<5uk=Rovl|GfMU!v2ei#6Fec7ow~?D03Ub;c|**BKUUuK9^^~!@1jnQ{Eog zsHZo0v_Ynl^ZB@o0z8kcOT9IsD4ODD>v|5{RoO#tZkzo(7ASE?4{a?m8qC5}D{{DU z+TA;}=n9^jUR zF~|`Ox+D}hu)qx5RS==0Hq^f&#X&<(cF?B~hzvqpBTOw&8eloVipz3zE)#?sTkW{A z3Hs^+!6O(-JR%K!k_M64N)Y*!77xV!{OUR+NdtjEh?hjscKjO@WX4E>?mr&^AD>WI zgYY}*3DZg>2I%wes0d&&DExb(eq)n>NE}LGZ0Q03BIhv^dJu^HBS0i4j7eXtP@cZw&mRk&*tK1T!!PMm%_HW+g9E(&x`8P!d z4mEz_4g9_q5clQcx`_B>fIDm?#K4e#3~R#u5Q*EYSf<)8f9P zq|xvLD$s-=5CKB;eT3YBUl|ZQ|1)png5?3{{tN#Xn+=>lNBOHdU$jKKs13Ksdyi2D`L z--QtU>Z>sLSqx!81_uLaF!vfX2*F@hfKX_F7)7fpzghrczio3QT&a$s?pMHmGbT~& z!HmGKWLL#5V1P^kW$~S8_kY=_--Q^}7f3O4AOQ2B!7eakC9=~U`iA|Ek^;4|$EKEk z6PSTG&jB7_Ue*061*^M;6?XA&2Z-DK2jae?W_|-;sgc$XV~F@RQVtZNx@|nir&A$I z%rf8S*_8p~qnE4suRc=L=1f!B?1US0P95*{4NKKYwELuK?`$ukCVx;$QzU#_r@=+9 z%tZF%Uj^1G34>r)Z)+u4n&6A!Oy2*E??6=EJCWT@M(7Wi9n0rjn<)jFgApoPG;rqT zbsA~J6qIHIn`}z<8l^TMf0N;3!NwRnf@f{v)N%>n6%R(5th6hz~=vqRy zZ7Mk=%Qsf|#mu_ZWW$&-?@#1)3nE$g2zN_(c~t^DFN5D;Ojn_3M`pdCQC_#uk6c^L zzH#j;QgBb~g|y-U9VyWT!RIWviyK3d@T}IJXmwF{EF?-$ZJyxLMGo) zqJ<9Mo)Xo*L1D-iYD(x`?L%m`ON?Z}!(aCzmbQLRLic?@t24F5P=2U|pUl@t`d#`3 zICfYr1jn%$7C69uL%lO6`*Qxr51tO^b(m8R-1L9BhxLWz`nSfgRqYI+h?!5z@jkMS zVWROZ>gbcAz(MI0x{eetEAy!g7ck6mwUw0_m66d2d9T!7THht@n|MvSWR3sD(~Ti^32EWo@;R| zJ&U-6?A0XBlPM`dbp1hXx4vps=JxD6v1eMU9(^%B^Qlo#s&3a4FX`1thA+5=o&cz6 zDt?d@B&=}0xFE%Lx9MxsS4IxkB0e{UdVidwf7qsaW1n6>7+zC z%?)rXa}vv4j1R*@9d7z!bpsM2!I@w4Mq}G_-3Cq-w67F4Vt^Ibb&h$H49^rH==U;S znD2iU()<4vR{ZuVaQ2AK$QUgOo(1NiGvhsUeO3@(OQM{Y+oU##5pdb8d1qCQXELhg z+vwu%M_Z~4!%s(RgsR7F&(^$w+GOdQJScyDsr%SJ+OzLPs+|I)8dqVP_JtMrpOhnNN!3#Ki1))$p$8m_zYe)uF zX0wcIb_MBq*-puoc@Hys#TTDF#J<6%VAoS$`TW?~F&7=3FjJuA3T(x}~ouv>d-FgyaX!`c&I zG3*ro>TQ=deM3ysy9OoiLhQyB1(RugrkA@-m8D|Sqz%_6R^~IjOfu;CL26Vr zE*Q$yn3|<}BU?-?HgzodFA@TaJ)S;}Q|+)2d2}MYB4D(zZ6w5nHMgj&ymh+h|8(8XmimO_AJgdO zQ28-cHW$LM3bC?zE7FT8?DBEfbG*CmK3T7&M1pOkuIS00O#j~9d36yt&L^J`xwnDe zYNN+}DpO^zDhbpMGl37uv74y7?WIf8Qmaz=4P5e`zluQJRupk>aLYhx>=a=5GUBj< zfnY(H9;YGGlp6Fy9NSL6D&sqG4k7*V_g&LGB97fAvN~VX5-5%{&@MHP3znV`B!M0= zW~GdbY0?cABC8{uV14Zle3w}8C8S_f+%u);;lMTQyL*?PX*sAJq@~VM3 zH&PCU5z0qc!M*B|0im*!)ToEZc0kl%19By*F#a)-1%C0y@Qr$jcDck$GUhU_=`RPQ zAEhgv_KY|t=zCLo_!iHU`SULmk(#Ljzkr!^YB+ z21UjBl*LTlT6;U8m?Ob&_TG;cupRDhN#LXhuG~kqRIYBEAW?Kc1D{i{{_vducZ=-L z^+cDThFw*Lg=Yc_$mLfCtbGp;i)o%9N4wY=lz6RRGQllKo8t@B+`4A2pYR*NR=9!Y zduX9<_nn~cBiAs^%}KF})XS%XFNxCB+C=Gj<*{<@(Dq9xBJi- z0_(6`1#3a*i)*Su_xhF5;=~ANPlYxueTZqLZFGuAyX7EyVd_g$MfJhH$C|6VptYw5 zx;4(V?}e;pH}%1{9WL)IEwN30SMpX*G`(aAE&6=jku90*!na~Y@xLLTF;}Cjzk18i{)If7CPU$oENgB)h5B36OH>U98?PdGnrKt=u zCbaP8Wb?h1b42DXrELORc~94z=G-OyHkhKeA#~fR{fja@U0$_@75e&=wvmWQh7DpZ zdvKE)k7i4iMvPqiG6X6Ne)*0{YX&aP0HZ~NuH=lQVCgB)k~;pS;h17g2z03gevHq( ztl9_K$fK&ZR6JPn*ybwHSsLTVH>PibyHv<{hel2=ue=a@T#oC8zP5p&PpAbV^zJ+A zA_2KbS_L-by;;UNXc?%=nLq|J?3ivgsk*T>Uq$o7s5**;dh zs}nnS37QkjM%VX}fcIAq+}(Oi6sWZ`J+Sfuc!YpQzHmd8h0uf(FoFSh-Z7lm#7*kD z4z6i|xxp)6HINi5%senn#0kLM$K$|Z%1@62$N1+(<5IbGk$2W>S$E%2Vt(IIyD<~@ zz%#1YIa@HD1~_>82`!kP!QfeWppKa=xC9G0O7{CSCE#RbQ9{bZAU;@_3?< z3r+0*5qW03DSZZ{R}=va#In(MRAdI=@=xDUm%!Ka^OUe<%t0Voex3^R6Q*%zbv5W( z%|M_m>;-}f#PODGpiHn0%ge9^JuLb=>gSolenK`eSu)|b{lxxnZ)a@@7v;Vt<>{;9 z_a7_GK5G#6KIu-2I~e?epcbqd(l z16T5q0Lr{xdtN>3#l&OMp&9SJB2>1aneyH3DI%sFS*l`2z12#~hRN#DTSc*TRa`S$ zm{jZ+`nL}T6GR)WJR5FOlTJ*(os_-SnSD;X$kgcm$8TA0ls}xaGD~u4PZJ+YitC)V zzkahAG!m3kFKQnlRQAipw6;8ZHWXKPZbF46z=CNq4+K(`xR4Zx4l>f0wDH-xI0Y=r zVdD$8T~t=jy`posYIN6D;Y9*%jMCNc3nNmD42*@gmKgf#?sGHGrn50_t%9s7Df`4XDRoCfA^! zhy#A$ODsJN5Y=Ne9H0!|IF4Hu!y?D%3BOF0fCvQujYvW+LaWInL$JZSbXcHI7(@>c zRlOhmWSj^XiAawifc!%|;F`)D?1VF*8%LnvT7HECm+=HVz@Fkm`UWU-k3P_NmB?-# z4nW|Bh^7um=ly zFWv@W)}OHc464!`0tw{+5g^by$25ZZ8PS-Z;ZRs90?f?pVuE;rCzEitL*z4TB0 zpL0wgU&(3u)xlVkntr}gO4ev$s%^aoiyAHKkmH*#6($MQdss8P>y*RJYw*ChAxC&; zX1^F!Xv{ymbf4*GE!SP)BcxXCFO6LNnjsT>jt0HbizcA5Wxis^UAE1d(ODt!JC2QMZkAj^h!izOc?r93)u8LU}Wq0 zD8MklnoG|Ie2zh85`Gy8*OJ5yWf(cqR{+5!A#0>RABO#A0iI2T=5m)C0pp^ej=vTR zSncPrh~HWRu^pL(hzIl);9`yPmum?N`vJ&$*kvO+Fc*KqPwKB@87|nWVf%>4`ipwk$n6-ELg6}2a@@w50YxTRWgv||eR*v^C-GEpNslyTjni18mSY zU>rWO0t=M(>(N6Dmjvf!$5M68a3Cu9oJ?eFB+aps=;|0sQKtXe=V1Hn+GJCzPe zuX;y2tDFd&1MB`}Bp{T2naAqlGVb$^#-a&sLm-hVMqwb!j?2-#0X!TUbXiAV(gG*Y zwFkhyaNplUXg~%>O`FdHfr{`cR^nE+LSOm84J$0!$z1^$2T{mzFp7V|4``VV%((gR z0Acexe7}M(+>kOydV3_wsa^7Kupmj($3`;BKfH^JS9gVe0}lEP_BUyC;>f`@D#uXmz+j+`z}xQ-e&#Ae%O5TffNzjolfPW z?`KAbSd?64XP$}bN;zA6+7lds_aZ$b_9Rr`3YO@+2TxLUrjyXML~gnOMW!j=<0@rR zjq;MW<++TihR=;f)M-U=jg87@%N$RY>vty@UwC5j#5CZ^#g>E>o0dC4G8;4tTkdp7 zZ_2b6SbzIwX-fN2)=_#m2;ZwCoj5@R>n>02Dwpd`KYar)MdzU^G(XIr>~o=~=5u=^ zDzhg%(^)5ovFY&zv&?ZN*-!|=XLS#A?(P-H4VpraCeg&lrLCx%4 zxqiY{RpIeg0S9Zwc1kuo;hLa_BnwA)54L8Y=R4{GgGjQ$^(s~qwjxr^o8!}aTy(VN zu27nSxYa)#>M}@id2wA`MkL3Qt@+t;>mHvLSkMs!LeX#v)d^u+?ZP^;ox4aL5Hxd9)c2dd zx3h2AI8l+yHqD7iG~V{fnP*r~@L#$Xl%MUk9>&!GzA$6&$ZzhuKP0az-$7SNp6D$N zu*mGKmx-(%Dc0JH*Zs2RmC^yP;ln%Tjhik+EcI8NlDZx?ZXWod<+?;`-hFM^(NV2> znfOqNw^0ns%)oW`YN`AARY&tC*H`>)g*xeZP^EK`+E|Ur^`moH4|UaGHcUMJR^0hK zQIwaD)?^XWKe{p`eJSqHjtswN4kC|ilbo+$PbjO!Uvhb_Ui9hizO#I%bzk4qh!-B5 zldi>M=$l(X+7`!f82V~6}ey%Pl?bg7E+(zkP=*A zx~E~@u=Tn^kdamH+c+`T^VRdG1G9}zQD1CxC8k1y8BkvBG~1wqAsIcTwz_MgLEJj; zxCxuI(x6+A1Q_oEXoUCg3l(My4xv342KT5nfCLQ#ahy$RbP{*6tK*Et<_66Gk8Bp)9T_`3YU!(|DA(z}uR5w5KM%QFbUHk^Eq|#Cv{~LmH-oi?1qnf_dl(6K z!$MSr+w+R{W;w_PMAVDBnm@Xn=ru#;@364kz99gu5^V8i%lYz3KL2X53pX#GABpX~ zFKZ=yayom2d@!xET8B+z+h-98K3SVS?Ez``k9WEms-WZAh)(=}3LZ=TR&G;zzLvJ! zK^lDNmPPU@Wkbpe1kOc!3{|=n4}{!(ie~bicA!5Z^5n45f}-hNb9x!6`-K|wqICR8 zgMI^fiq+aU8V?*v<)bB2vy+Cki0fUts_r=p)Fe)WFvgWuhw0DLo1UcyD?Mbrc`3N# z_V}&Kj3+RGl5iG|p&A+JgN;Z^vzo1b$E*Fj{3cs z`;7cIJZM5~c<>nav3aVRVvjIO1KDyOmUCTooh0O80uVGdFxxSOg7GO)^3Tf<6m)jk z>2L!3{_f}--YZojHwWqEToYkEpjS%#ut^v!)>TF5wWmFM?St&5vlMl@D-H!+;;Ijv zHEz9^d03yksZxu%PH&SD%QW*#$&RAAIo!FbMB+yWoiD}G%Xo_86tHbJS-|TC- zD^Bkka*!KBaucBn!zB1X=i#;rYWabnRQawqSM(EI3Toe%OMc8R9DIjw1IGnNAbWr4 z2GpHfhRd5b(XjFuQsOl|)yr-mna^@WxOd!wRcj0V+l$(Wa}`ArS=N$9W+UPr`Rd$y ztZNqF&;R}^p8NCP{wsg}O1~ff#f4DT+vS^SxnC!>`*Po@!NCk|!JRu-xo|-c<%V<6 zLWmBs)rySqP2gRb^}K6(U#)BJGl!VgrPWhIla1l%J^lJsmSZ~}ofW>a_amY124l~c5Mj2h_p8q&Cbg;f%De_HC@yO+lS zri1?ui_*(Ij2uMKT^lfcD*aXmC`|i|kb_SVkEP6?dSpU3p^hXANIKN-TDUKD#4zsq z0j=OrenH7M-mBA8bUBb19PaC~-BsU}mb4Ca-C2!2GAlps9@KcZI3(df`7X9MdThmq zdhfkv+DzK2Dgc!i1Bbel)do%0@g5K^RfKi4o?d%sRp=>JLMa(Y? z4jdDXOx&Aq?yS1VNLOV-%Gdf748}Q=Krh#wX=K8KVg=vmB-2eXLz}#|$X;2M9nboX z@+g|&mH9~MR}xzO?6k}WD0>%JmKId51geB}Gnq0{A**RA{0^5&a{fbL+WLY9trLdg zqgtu40-J0_Bdco2f$4`7nXDwsEHQn}zNFU!6$4C2qd?e{H&xM?%2(~kGRbmRh*2A= zfS%SqH7cF_+2X#>>C+r9T-8tHS+3u9OksJ9EC(yL(bQV*s)%S&3q$a`UHZyOg^L5{ zwJ2EsyYqL+jkddv=+g9k6nB;hvRuj5FWeA#E3!BsVG&0=1A7n41!3QiDsh1jhr`Dd ze`QUw14gesey5byyLYFPo?SsPHPr^Q-YFZ$9dG9?DCjh3>FKF?UCI0~RDPYl{wWmd zCTa(2kK4y5z@pswZ$&2Th%g^~iLb@)WjxNGWx@9w4xfdspj#HgNjKY?uLteM&E5N4?oaYVDw3#XzqH`A`u>Y1kla8)zNO(Z`5E8m{|-XWM02 z+b)Bai>*?uZ|Wot`c>p?TCy1Jd2iB6vL*Q&*8uZ>6E_&c1K&a7zv8gFtt#eRPU6Jo zo4nF8)~2lqr{m34^<5?2b}O;_2_0VHDkD&iVR|Gg-H?Z%??T%UUlnevi@}W2N51m@ zJze-Oh^!zG7+@%{R;XW zmL7l=!3`j#A#6w-HmadjS;0bP3f?LIoT;AE8dZ{QATQM8Ig5LQrKp1i;P+FL!a|_I z1i}vUFT--dd~GVB6eov-S_u~6+2{+J?0cl@=B_;S_QoYb1%^}i2eEzk$;!CRs^a!B z{UNVdz7bPQIbE*Yf9iRtZsg$c!#0)sJYJ$>^~M5ME>^_xQkEW+$R)$l6$atmw#F)% zhYaquuxhZqFWwl!?Y760=`}H-a`eu=9`(x#n;GwI#S|UVYW2aMzn8&1xuATsoB3^U zQ>+`QB9X|`f4ilgI8&=W+J_a!OCxo_7u3S_HqsF4HgBr&(4-lhWt~#4nnE7Wc9f9` z<~WWDRF}+=jtm%f$T@eses2m}SR}~`sAoY22u%;>t`Md&R`>URZX85&!B-}XYl3eW zkIL+skhc~2Y`J$N%I^B!>Wa&rnmjA^Yi1^JVy)4A(lu_*)mUEb3>RF@QA=~lyUpm! zSxJ4bpbSpFP<4lBrAvF>G708*9u4arK(;oo9>rHVA1(C9Z?AFJ;U4Wb$t<=)qwutZ(jE1!i1~3CpD&G<;~4lo&geAZR9~5pDB4 zhumJ5!}USL;IP}xk&NbP^Jys!+}#Y*BD2=6sC$vkB{r`D>EBj8|IVN#j)Hz%M+Re^ zhj4VxC|VMHi+ndcAIt8t)(+JM$j(dF?{Y#VLH=xhk*~_+3Z57aS?F}XBU?`$8N{%= zAAz+z((QOvMW{j`pfZCqcq)}-@?N8Ho`ZE?#VfJ7nlioDV|(p}rNza=9sxZ2d`EFB z3sG5+ty+}l=Re7sv%IvuJucv7DOFeu4(X5Z9w$6`wDHC89&)h&eb*;tiA?+_Eafy2 zmSn1N^wW|r#)Kl`ll`JCmOkeiV-KEtpa1@;3uuZP!g=`K4)NI461;O@DvQcD z#1ud`9mENM@5#yG!_oc2Riy$He1SZJOV3qfR(bN8@9}_b&Z$cKPV1Wuq+`G}S!+k2 zUs3oi<4`W4f`INF$FsKX?XO|mClSq}COa$_O}Z7HY<(?~Ab#rFUQO|gScyB*!XltM zeJge=;5IK07Y*90$GTpaEj_S!n^Do*&l_(Zx4$M}dE=(Q?%?2*M-7Hq?!esJJmI2< z1MMa_9m!MuyS%gvJ8#_eIJd=e)4jgWIPDQ0rr6MQQJnsj`&~!Upz;!+^E*c4?~K;}Oxrns?K{4! zAonay?<`knbe%+Nq3TBXgoj`Sff8ju@V-m=r267imHR-+a=1z)_rjEu8TlNo+d$Qo zK)Vd99Ol)qergx9V6}p&xXp4!pAqqJZ8b{9$)c!ngRMZr<~3`e!y45?4dx>Wy@k_&^Rp~ z8Z_CTtJQ9fw7fpoVmbYE|CHb(CFZF~%b8=kxF@7MkT!r_Rl`>j^7g^yR8KS~(u5b| zeQKz9$iNflatL0BAz+`feD1ki^m(SorpGIFyZ-ZqLWFB}0|>dYfpDnOs(B;A(VPR> zLUyj>G;Ge)JUC}kQc}($9HLM6HQhQ_c28GFo~@M0(oz4g!k2GmD(%;;F5NXyWRKg5 z^?-yy8YliB(m0UMG`)58Ci00$g~-fAKdd#DE$d52*2n3Wiv9abch=JUm8;Q?RyT$v zCw;>o7&IAOWn4;feWt69Uk7|jkmqkZ8-x=eSRRtkvuR^H&PznE_Da(*Xfa*RN-*=k2M^pCMkfCd~xv#5C zocaUyk0Y5an*6sPIEy%y6LU*B=~rPR>R6cpmK)FN;#FCN2(`E87sRKW%Pm_yxBbS2 zn++~jhpj|5uHUbjTzJe)orglIorR?cQ(UXEE);h;c#*23Mp^RrR1=RC%~GgZf1aOp zoVo7m=fr*U_eM^uY_snu(WVm1F`OhT0tej=Mh}^&IB7jU)}Foa1*iE|%m3`7N^ySC z6Q{o3Ef>=F-L~b`2Xta8u@Vi7I!>q1aIgsJA;~_h&!YCW^1UU2n2G+~ok_Nrq&kxG z3ONsN3KKsZ<=t&Ktr|OwW1i?|@g9%-skhbj3||j9zlKPre`M#9}QgItpiJg z$l(RKz($H5-I~%uW%03SzYzj`DPQt5H z!f**LPtDz&ssL;QELjjv${0V2>q|bsh^P@Nz06@fQ&Tj^yh@-w0Q(k$lzm6NX%6+P z#DqcIc{*s9hZ_&rww`JGd`xWJugQ|RV1K5_D zTSmijBJ7i$!eSI8`T6u+wnKM{3LkCTCbX5G$@gMI+?I`oCYjyMY2TLI9P&Rry>MAg zRP)w0q#oHP0%~9wf`n+v}nMGMA2>T)*05R z3}bf(wM?jnm4%29<%O+mzRQyR^*A`W`)Qqrn2m_^H{;m=YuSPoH*i_zc1zkdcGm{4+uXcW>P???j@??o1(9rK zA0X@DYI0c5YBeI7_nJxw9)9~=z131?tY0iWx1oi6x?)ex5s@Tb4^LWybXBP`0?tu4 zkglP(8o`spvjH;7a*1|)l<|9AhCW`Jt~aw=s@~~`j_R2o{>Fv}L9Fs)c&xh(>;!1g zgb}XF;lfwdwPNV2Y~_@lU6gFnaG{ree8;K%2kv-0j3^q9v6FeQnxGCHCFL2?`83EH z>UMNZp-=HG@9rvJ=&h)b*-p%F=JE7$-?>L%zn**+*e0RorO=`#yTiq zd!1?bKOviXkv6lPRNts%cs;F&?F8Gsxx0nA5&fW+yWeBSm5-I?!%mzi{`m5Y%Z4hw zfv_VDcqU#?@#Vm|at;-RZC6$4IxX&+Lt8pEOjV_X1p_YZ)XI}OP;-rW-L?HQ>s*zc zaJMhmX4U*{Sm*bs`2S_UV}CBJD_u^qZ3b8IuD$>2G5y+le5{txrZB7U(WSQXUjIHy zasv5&)mmxxnv#q z)_1O<5MP{_XDRUU9z@Viw)1h z$4TVPomjH9N6?y)rNc;LB;`&)7TxZ%*bmFfSxF1n81bYV*xv2#7`RNsmjvu87OlSL_R_B%ORU_FJ+31(?wZcbdf{;A zx_of_lNT9kD@O-JPwI7XpXveXnJ^i z7FW)$LCg}p`89ljrY;Jf-rFns9l4)IJBfWeU(Ec`%Hw0`0JItyO9T*MC z?~IYm$+h$hFD-l(E}v zJb9TT>UwDA+eXwr+v-)~S0Nox$L7mGHw);A3~qGbPQ#5XJlHz&V&_NvdmweVN!OY~aBno|Tw!Gc>qs`*dvJBG!h$GLGde-Ay+u?ocpwGNy_p9?h&0tRG~Le`~lU$znkAJc|;053{F7J_RU; zO3HI&>oi4UcmcQJZmfQZ!f>OEmSIq3mxJJ?aV{3yN}0HJqxEmz?3x#9>V9Os^YL;n zMG%zX;#ESnVyv=}25c_2k_`*8YbtI*w_FFO&y5)tw8_a=l~{-zzZ0P;JSb{D!^6Xp z`=)Nglnx18*(Uao5N4iaqzTkyrdzDufeUol=_la$P4l+>yoDw6cSz_eN>h%p3Fj!c zY^!JWrI&igEY11T&aJ%rwuQd4l{BK0rGu0LQ|khpv62j1HV`Q1n$hnbal0SM6OBV* zdV+9~18ovfp5$yU~iOeI^ArR*_@tf5VmX(oiM6N*w%gvyqE zH^!P|mu19^kramasKz_9e9qPHasK$8bMABh?)yB>x$p0}|M1{}dCxW1`g%Q|&)4%} zitku-{fq6zf}?4fKkpYXJhy<)1%ydwy-S zrRJfpC|VwTGKYS7{0!fpXgU(S^gP;*mdF$tTK3L`l%%e@ zzuYWn&J{j5XkxB-5M7i}9}BgJH)gf1ZO59T*t^e_ui&d)nc@t38%-_Am|nisr7H?& zew%lZV~^{k6t7z<1(*<(t_|HlfptItQo%sonZ8aWM+scSIK}QqiME@ePtjQH!Z)k! z`dD5Iil!#%DCHDy$gwUF<=q_5f>Q@u#ZJCKG~%IC`LMccpYDu0JqjLYATd+9t}jAC z+9E-Eprg_hyU6f8oj0SMyL4aw>?rHyE>%?@rQuH|Q^SZIX7X5q51yQn>r&1*fEmK_ zmmvd@_g%Icj4{F!uRoXnDz`eDP9wTDG}|ada%$GpPj}`7_$Ud8%4LSRNY-m$2|VX| z!Ik$_W@fN*2a3P-281V9CQFTjY-tSbRb+oL+qKKj*wJ!G;Pxfh!d)iyVDY*uZpGXF zcjzy}KYuf!VaSy!F|>Cv9AvYQ1y)UWzj@G{TY zA%14&3zg+`^|Xjd^2e}EjVoBO{P+3HceAt_cFQVhXcF&@CqKbLw*PQUhj;%)z43{; z0o{m1S;T`%%-$%Mf%B7fb|;54e#`hzTog;oIa?gqdT&8#a|ZqXWrdsylfY2N5{}@n z;1jUW2}CloDj-myu86i+*+oh=QU`#eL6SQD=vUJ9D)=o7ep zqyk*FeI53MKCZHP8HgU+N}-_ZQF*gHcUnbO1p`F!>BZ`JxlkbRd(eQt-3%5rH35F@4Zf0PPs_PS(nJ+vX^elkJeNY%nrPpA@<{Myk<=`E~sWB>|*8DSroXmyS(`o48E zAn{zctXX{VO#P+WG&LXjNYPTICH^uW>nk6Y+PhizepAo(vH9JL(blZT?HuTnur_!a zGEwK(hU5?8a^lk1uScJ?_cb;pFNp5&Jf309IjU8eSpH(kZ3@5I_qz30MJ%HH{0xmV}`5&9a6+xB7qVBETs(!QCms~DKUV7*3XjFIl z^kC)iFEOM(mYj>Ns$5%OSYrtu_$wR92!yFdNmS=DA@7haVCq~OYzGd@IX1Qh6(2k; zYm~#1J(&9O^mW&)j9)mWgl$)zBEZ6>_~~L$$^A0zUv0W}S=~~T*|)A~NkQfk2j5xA z2XYnc=Q!|&gEIzY4`!1-)-RtZOh>co@FDprSm{gnv+ZCM_#I$&G_Z<;Y zrTyhvnbpVa^SNF<_S>(~zR%A$MbG|X@%aA4v1P)^l~gNGX>5c(GtM>%G|E#OLu-(= zk)93X5xeixGv~FAM%4NTqBIT^46R9i&WSpg8sm*+SYSF4)n$0-T0>`L+@zwwxR$9j z1-}!SWQZ5`@GO({e}(lU=46k1D!D!7pFN@6*Wg=w`(mUK*1266(~qo0#N)}$NGar% zyU|^2t1e)h%QbvzJOW!cYn*>qJgA4lIQlV6oQoF|kmNumZ|d{(B4(uN@icp;wC{oy zHRmOa9fm^}DigQvC>E8nDrV3rf8?ArrDuqaS7)R}(f#7;gZ*Uk@|4wx7WxR|Hf9)f z5%Mw))~Z|!0D71RsQxLr+`RWrMMLqbX2Gm#s`1tC>`^Y-Nm2esEsx#nmlAe}N%(GO zRrUa~CV%ZQ;{-YqoJ}$8euP}RWFUf=Ck^|SuS(~swK1G)5k3j4Yu@K>-#DE)(9)mv z!0|($l3xHlyCCd`AmXEakM2x3xq%9{Pad6MGcovQ0fBN%7_4T&@`#>sT@l-$8`pO9 z2!xngEoB@299X;P{M7ZTvJ!|w2!!0e_;zsObeUkfk;2TqcSE6;UiQA$&!%<@;hfV& zZS$4k#kPT+V?_RIvNa$4Gid*C9OFW&T3{YWxzf+DUfv|ZI>SlWE2SjiH8}JQ+7Q=8 z-|i!pr?V^q>Td|obQga`S5?hl;_aL7m)QXEMH%9~a5^Xx!*fNMP!iX+%K8>IQW3ii z!(nSvg=jPG)~xlkqWUKfZ@uo_^Wm^&6URk|I*-h6X$;C#2vS7cqOAV@23&*Yk^K(^S7cQ@hDz_BKM)9V+@2yxj%8|{*bb129G8H2CiqJo%e zXC%x`u|@K&m>r6tP2cefLK$WqhS0&Eu zDCnyme69U`;41@Lk+YMakP9uDyX))~6vD>V=jVJcj@g8UsWkctr#13lC%DDL{ScjeslT6u3!WPR00QcL>gs}sXtz_H*84$u^O8(c)w zC2vt{7@R3`0Hw^|Rk4n4Q+9g1&+hclhW}TwGqhLAA%&#_qJc}7sx{wz9b5HY7}*Sm zkBnK5)0N;`6&=!W(JStqzmb>mhY|PG%CQ7H?2-ZUJt!-@8-@Fa!@?Z3n>~-M5oSu8 zv-!fw!n{$5l&P*zaY8dTj%n+AEr{G^e6A*QW#f+R@`|hUIq%!2>sO@qzjFO4H+20R ze=|7n|DWwz4rQ6H>-q~g;s!aKV7mby?xSCamNw#k+0<@`!uH+9#JbWSkb&`*<*ako zP~*vG{#j9iZG_Mc`EPA;Z2kepK4uvF6q+LEr{Du{iB-%~P-G`kOy_L{`dIY|v0#@& zWheJ9#u^Unvyy**`9{K1=?nOOeDNTi7wFJ_DcS>lLY^f$2k z0!|jKH;MEorMB}-T-Z&%S%FmmTly*W)5BVF5`Z(MZ zJ_D?4pMg){5C4FkVuS7DJajwooJ>QSApU`_q2=dS6Ll%|^_O+SgD2PhQf(X3K0FYb znqT5-_-*ItL=*%nQuqH1uweM zG$hpuiVk3&uGEQ;i8c=nIduK5S%l;ER5o1Nixk1=^xljMRZn1=EsJX&G|tgrji)Q0 z`x3FGoXqe~4k{WEe)x-JHgLK+#csUN?PC1N0^Q@>7b?s4zoX}!Tr#sSXn$XT3;HTw z@-gFt0kwK(K)8j>Upk9xBD~1G&e;e`w?^K@3z5>I<0YPVJxZ9L=eL$3&YZN2q%`uI>5P;--JsHvYS{>;EeI#pl}32G9pt?I5yxjBQW^ z`dDV5Cq4;TO-lDBe(CYy;%z;oSI{eYRoXXaK=@E=%fsm})iKGb;RWKikVVa!gJwd^ zOTan&I9hMCT@Nik8Ol7dP$}YB{yFV;X;`}TZ0zpC2Ua>t3pqY?lyY<=GeK$_TZScg zsj;#RXOYuC=_kgX^^t1TQzb)x%^est8_Tt6lu3_%Br~URpI6M(ZSg##G-VN@rnzB2h-4@tDs?%KQgf|B*Y?zDZLImb&iV%#kKoH-U# z@9kkdwJEoh+{97y83AZfRxEqh9A*TCs0h0_QeIKM=3=t{{+9li3-uG$g<7v3aK32w zx$knU3gznh69LYq<6}S<0Pha~><$?qFnSAs=#Mj%5tuilecDZGiTZ3E%X>9a$6;z|C!5fh07$2d7VUsT824n*Vul2k_u=iY zWNhEqR4quL!9TtNe1>!!65vqW0BOLI^JJx-l>!-YT2}DXcJcdiwfushmyPZ!{2jha zEj4E3WYJUCP2X@Ij-EVmR|kW`5J~mAQxxxw|rSO^#v2c@6pOMoYj4 zuU%&6vhp-n_guDFuvuhOyh^w$h~+~;7m$0f1Xs2&yLSW)kmcAo+Xa+DRCo`uCw1F3 znRyTwJn514Y}rrYy(Y!;Sq>K~3!5^)Dzp7j>U!0fJtl?xM4 zh?T-F#r-y+4+nyx&31~jUOxQL;-@Ex=(_R`$NW=iOD@Fv`iIoU= zqhqqm^jqn2`APG~XbOpmXv1&=&1=m;&(P&dhd4J=mIQv?b{NNo>84?!a~la58Q2Gs zqZ>kom8s}RN=p>A^~7SRr9!jNW79*rFAOqzJ9_~0JKdpIe0>IKilAj}Ljk|1zXICAd09?By^X$}1 z=+~CFY+bawL3B$2*L^(|os(%}x=&Ppipqi(V`XX={u2$7-RGinsn1TZR|hbr3K9-h5KpwXLa3Lw(ChLjIoeCFkka z*X2`nFvE_hPYnRfyRrdm8Z*Ut`Y^n-aHd4T&rXDByJ%FObDYIQ9ZA5mKQ!Xy+?-!w z^B)JEq~kX}o$+!=`>^E9ZU!wb0{w4+F-3F2<Fk4Zu?#66ZvOq?IOEG+v<7~-+Ih--N_hA;E%}U z&E!?K+GMlmhF-rz{{F+z1=ZxWOpxAH1z7*XG3NI6qg3+6r|UOemxP&;i)=ogMbtDu zSv~PL?*1K%^r}T1A}V&aOIBZd;uH{!-6ocG5+vLZ4qV8%F zi)R#-o)4YF!uyfE$a~&M2(jkFkP4K_(|}iLc5}ZP4dq#JXU2U<%idq^eX!;_!Y4rG zI-*eX-b%d_D~7*o|xU5%+U6=8B$*L%WLJ5}QyUsYD8`}VIqm7eE9Rs0iKNk^6zeoZ-m8ia=AA@f%s5UmYJrS1| z4OhOI%GsHIRM(_n=j?&7RvMy-_L^~`t`X=SDc6sQ-ey!>w9p_Iued@cn zE94KL4dc>Vk1$ZdiU(n13PLEV^n`ul6x(FvCGdSeOObsJKw3o{1k4Pcu-}X1`I{4)0H~F^? z(kdD0b(_0QZhmwQ~6SG2X-me*KGo#W}%KphOh5j8N}F--Ji^S6NpNQ`V?Ly$9;_FPN@ z7u&35TgQ~Pk0hUW6RmPf9+r;;!2c=*xJeXrMQyi@*>tx8x0$&|>USF!5xphPbn z#S5fzc4Fh5aSIb3m*0IqXjyD4cuFR8b4FSG`6HJlnayY+ zRyE)+@*!tI^FE|B!1PHXF#kejS2$ltgNI^Hc!g(w^t)g&Vu5w}jVG5yRZ90&?e*1o z@zQK;nV!Yw{Xu&MHlNRoV?Cu9qBlntAl7L1^va{>b10clS#V<$a!I zyJ$W-5Oi8Fj5q(r=zq!h?cYq5@Lw&I`M+r^^8c)o)_e%y#CxXVv~z@`C0*DgS+Dwr zm#eqN+)2~kma4H7UP~DT`<_;&}LuOL&H;Y2{ewqXIM}{`$(@N7-o$ zc3g}eMde1@V@SJ(?3I<H8>~P;VL84=lgB0&3aN$lywlDqLmN zw<~y}_p?e`4f6JnT&bjd)}{^)_B`u}BT0cNTZ=?2M$$%o<{ zI21dJ1~sq$QfSHV4Om0aetLjyas)i{WIJcnVvnP3#7oSOB3Gi@R4Oe)eqQ(Kebrtp zUhC=?t0_hE8>nkof>S#ZGlu!gXFp~Fwm9}$6oT>v&xz)zszQs-h4zBN^jxZFbG0K+l7*1$N#{+M4kyf#M$0ugi&EwN`fDUy9` zEX>e?X4axuLl-<2Ms~RMI{0GH<=&C?PXxL5k_=^ra{-bmU&uJ9&v6=-_EtWIo2m2Z zBZpA2iCD-Sn5dFES;m%CP)X;sa{h_QQ1po)^maVMJMH8!%NA6E1d5pk-WEA@Fd$3u zYzWm}5|BeR%W3!^djZT;yE@22iS7HMj5_b?TYYYyx}NV}*X-_eepAf3;qHKG+|3Mq z!7rjQ$q~}uco8Wm=sFnzeFWtw_AD2K@IM?s6klD?r4BD1lcrmM5nzDSwA}2-!ErN% zs8T|tqOjea<)zy3--lEeL$l5Ar`hLAxj*3ayM;V(sB&JIh@#uF^08MCeBbAJS%s!x zBzdKB7O0mk>N|q&Q?P zGJ%Rdx6nd&%^X|3PR-D-xBi-1mSNpw;+OWg_pGqmD?62(H$*c(Rw>y3N=VhNYw_0> zw8eXyKjrP4LSK7(qBw9{H-Y)W+~LLgK{0#H!Q)j@e#e&*+$Z*%F8+tMsFW!Jfnkqb zh~fbRh;TPToT&_{ckA-#WRye=cjew2ORFyl{OLdD*2=ZDE2$-$Z*~2&j;m(yXUF4wtG2!Wt!qXT|e&p z^y&MTxhzxGGvJJjgzW5RkQBSh9^iR{5L~bq{ju@Q-Fw9~$0MWm+USDqTfZ~~Y{K#O zOLQMpFlG#2-M7YITx9C!FYr*67ozPY1H}(NIdP*&QRb0|YC_Tq#vDOmJxkeeNC$J6KWJ`;tU+7xtlLSh4hl~c4N>)5oL&{n*^M`pH8&zhXb4^ zLk$|)GH~{$wu@(}%;OIsC=Iq7+u3f!eAkrU-t=3DRag20R&M)FV7Vd?#umbin#t7C zToZL={Gx5&cf5Z4u-amuWTu9Mn3VP01+L?V)!O&4ULeUyDCo@aObErWqz8W=aHdL% z`^GQ-EDJX@nQgOOs}^n^wTdvv(0se(H`8XnH%4N?AiE%t=?pruk101|&y+ex?(5OT zc^{_fcH$B-dQcMd1)~qKpmDYF2;1A?Dx0b;2YyI6-KO!VEDC+Hwz#CkYQ&NO2M3)+ z#v+7k%hl-w)_aUjNUmF;q8i<+Ge4H*?k;J{NxRCVvEYd zILs7gy<_k3X3DZ&p^byUhL5Ult&j9cm=Ry^mw(`5{rbX-YRiv{>O5Jfo$O&GbV?Xj zvs&w-9T=u;b&?Vu=8FVXCzXXofDdQ`Op5th9WM;3*zSACXY;-}&pD>}ou<+f5{W01 zXB9LT64yoPOY&ZU;0bf=M7T^ zW-QFwOGE{n=clL0y}y-h{`lt21GEn%k28&2&y6Ji9Gw^3CF5&KPp>`qzj&c7A z7w{(8zZtxIE%F+s{Mbj8>k3m&rwQ)Lj_*ZU#Zzm%n>PT; z@0(ExG?h>; z)@N7qvnap60R+!_0hyM4r7eg%o7fmqbtM5gy-L7nk7F;B_$r>Wl_$bYts?!Vw#3Tm z<=12re6=UCo}9|_(3svCG@&$V@uySlZz;{i-XrZWG58m7~W)##F#51EEP3IJU7I zeFvd$joV&xakqu4hl9kwsNC3H|L5rok<;Q~z)5V+uq4?GtRo~j)B6CNQyPMV&gai~ zuXf}sXjfLaFzj$v(cgdDh@Di9nm3Bq$+PJ7%RZKlxtx&^Q#oP}@8jtlPMOyQRX3mv zg>x~V>jKjVs_cnw)0^r%s_<*yBb&55f7a~5|z7=oDuz%71(;AeDuxL{p-%HU0QVgwz^Kw55uh*PcQCG zey78S8%4lM8vw{Y!Jfi$v_=%MN1A&|Qo}I{Q5B0_+1}b!X})nD^$7+MSsL+?a&fnl zlbSV@RiwTQu3}}eWUxO$w)Qx@c(|Dk`Z$c@Ta|sjaRYO6mN9pf|E-_ui+MSBN~(U9 z)~DpM!G4vg?;8X}42tJ(%#c9Db_&OsDO5xFgxb1&^PRoNGT!=T-!iTFu$+@IV4~=j zHF&Bx_r1!U9>B^=7hg2wG;RwpUVt)f0Cyi;je%-7Q>c33^J8~dpQ*B+QOosW^XR< zqmHRu=Z`}fXI>skb||@do%`bH!!8_gsiLTk2Busg2qq@~UdmExrU8#KY{5Bn{8Ag7 zg1sCa686w5*w|>paJ^V9m$G>~v?w_si?39AxK`|I?8(P?@XD82Js@L2!G{;S@$X$b zkA-b+Evz?{uF5LRJxcfcSsq$eg0H$Q;p0J>e%E`MPe|Y%hkWu8hYM^HC_DiKLHL?2 zL))@|;LUt6~P)Jq`lw zLgpEG4*blSx!iNhDA$jzlw}>#|I+<+E`a<)w>p0uAQHQ~wXRj)ohFz>YDTo1!9fAw z8Gz=C!!d=S0s2vV>o{75@&eVBUHwDwnrg2>zNno-+vn7>wPC%&^~@a_0Z#i=YAPt# z%VU?n_ckzYv8SuBgz+GTS`Q83g-XCf4?SnGokluOg63@lLyp*M77LWz{)ZzZmz~Ep z!4mAT7wu>ADeuAM=6#g(w3ksGuf?!_)?;=zxu>=tI!RntJQ4fmYPaO!E-vhF9GkC+ z;Rp}VwLk-t*lL(@lF%BpvTlQS#b`W;=Iz&^6EN~J);PnJJKN~tk>i%Emh1W%+vWgs zXu^ET-;E5lCn728ObHYEE820|a8D)w%IdYC*DHwDZ2pv`pU8UEx$aaH+J#^_Eo*dT z*XC)XY)TUqOfW1{LY!U;88hXlUSS4_;oZ~_E7&b!n7?S~ei%{EFf{DufcyH+s``<4 zpVl|S`p)IDbIruDgbR3bE=r!Q-2>?^SoLBBL(DXqaz516B!7L`{N(v=>bD5DfLm6SYs7(quA~Byt@+aV6U6n z3AqO!l#_qq;3JxBUP*=umSD`3hH4f|)&hJZK@F#u4!k+>#odH^_?H6Q0N55}2a*$oc7kUqcE+6;`SIh%N3%<>XV;IeA9nCQ zx$B$h0o+p@huAfg2vZ77EC90-4*k_LKae8vn~5?5q4l>E{vSPcMUS$Mz0T1$K6USl zimj%xg=n@`l#C&B4oh&sXkt3?wNp%SWngwt2WF)XxZf{Sv%9(B{*~V?NWmyQ6Zdt) zXvQpd(0)=zUa!R|{cbC{` z&wTNy*cA#zJL<7ziX1V}^y{p0c7o$Wc((3bu%leS_G}cXPmv!ET2Qd+1H4p3^f8{3 zV@lDbR4a!Ufctuxdiiy>J72-R^s;VF)C=H3jU|IyKWz~yiYB;)l9=`*QQph$c{l4~ zW!61L)pK2lNdh%x*G_-x=H9(#qa;-Kw~(9Akza4s7cPkNJg#qO4i9L-U~kb1@{P&?wfNv0=6Hr zhrqPys3eXrj4?e~FH9|Xl3NY7^oVMO!I@fOc0#5dY5UZm*W~IYbFQWjO4!3#2!)8x zBZKddIg2aJbm^Ljs_$!FH_t?OU4aK@%qC}>6?-D@maxo z6q~}EGRlt)mvZtU*KkYXfoB8+Wr6KCf`w2r`&hJL`4W~a9%M?%6owDIXJqf)eWTha`K<+(u(#qflnj{ z@7KYeK|Ck-vg4u3(mJUNQ;|hTD|#l0?23r zGF=-`N`2zY3>`H7nbPwXO`dbx8myOiaybdI!_Ndz1W0^lCg_VJ21qdvy}pRzB*k^g z=V>mf1?|jp{1@YrZao*dBZ~3@MM+!GxkXZ!o7o|z1kELfQptazEKTuk(U+pn4eR(x zji`RU4=02Y1xjK$D>p%x#D;}ntJVt_?B&oWShc{=x8^`O=Kt*vY@@mLROCe*l~ zU*Gh--iNJfo4uJD?6Y{dz*psuIOy<^1nA+L4Ztb51Hp~G1iC3-n#~zh zO$5LWFCVPtr)RL)_u4B?^_h)k=GuFEIZIxZ?8fEEbmwTHZN!W)RgU5CHv+8I?IO^8 z=~*=AoH*vuDMz>R-;9H475)NFhw9w2FQ#7QIc^`kSc{m&(NEFT*S_NGzU9;SEm9U9 zB{<}%v@}X(3yho|Ia-$WU~@j$z~}hDVPcc1E93*FazAKPTnGtrs00;(2Cy`=8BWEC zMv*KNyYCD>7i=n?avw?&(=|oZ^}JEy$+J3pV;cuv_Lh~4B|k-g(&jZyXb0VeRc)pk zHEfAWJbI*I6v!|hl&j~vx4Kc%R~_@fy=_Y9!6m=UzjmH>kuKnRDmz-90V zdOREx3e_widT~N4c}nfiJw4oQ^oce%^lw@VcAl!Px#=i#sSh~3#$lmb($pe`EBk{v zc@OIe8e1ME#}?CtPh)|EuUf~0L`C_r2R6}z_~?P3uV;U>Zcl9jVwZa_O?u665ygQ< z(p=Z}GR&e{7IfkR)$5vQ#t(EVboNapoe!*PD~=not3DEVSw=Xm#o35mhb8p38_c=ZLH@VEQ?F{Ra*Ki>Ff{H+z_|9IovIOm8_Kz;_C9oj6U1Wb0MFVoNLU^=s_Ky zyz=rtvzYtu`Fn+HpZ9(oL2>saI>sQuafGfQ!Cn7bvzUh$t<+*!_v=1=&}rhoPXn(@^N4Y_~1SUH>NxHa>Lq> zXjaZ*c#`Z0#Hx6Ck5$k+{vp#giHftx-*-3zr}0htg5Fz+$@-Juu_p~&p(Mt23<23d zVv2^)!=VPS>w*F-4a%@IJkYhBotvjW7SS@B@?sxE5=oUZew+&Ne(df?e)>LtsWt4S z>m&kl!NL1CsOk&{3x+zo2U``8tODPD!=5;LHiIJQ>0XuTR(Yk&&{wQW;(BlYKFilb zWm-W0}*_g6k{vNOz?_=gMf4_%e2_LW$fQJ_ia$2qVk;hk8Gk)`X zI^o6}uOCuxxvaspHAC7YgLKZloJ9VMQU!wf-vv=wK*Rv7-237|091++Z1tr{cPR=s zLzkk=-Mtj8bt=W6p+S+HPXZZzwKq~lFm->nKt?3Q%Et#*rsrBrF(pgrgK*ZHD10|w ze6VLYv6lbXS=Uj6Xm$ViXKPg-FSYGK zd?URWMmw1bfb7`E)P!xI5)cbWS0c$TF}m<$hAokl@TBr2sVGEP7~0sZNknAs z^&RK#dSDZ_WK?D>{^33D(DBXgiVu>p)ePrhrl=>Iua6-G$8{hTkUl7)xfdKqne}{= z-+NR#)zGG3^tZ@4y~pmd5mC?M6lWZZyax7Jm$m9RG%@68;%vUEh5SSkvox1 zdrWhsMkNvn7r@i19R&hCp@3rsuypo>$a%Vv`|$^Eq!~y`mnN6#ymdT9^T(T z-OD%~Wz63Fm?6wGhp~%Zw`RMy+a`u1BBctC+G7uB94h}5dwg7Qnqs*x)Ahs+)~;2M zu*@RZd?o;l==9cuBgjbNE;@VM2`+hKTO7z6;J;<7+N~Qdmn3slnX{tc>+k+whJTqK z0_eX0JiH%lOeJhd98*GXfc~83+KrS5xwGva6+7;f8~BUf9`&_l)9K3vQ~OJ+@>REY z8J_2mn0@$m3QHISC)V%0D#lgJ5MmD%#L%h;ncqGoFOF55s2}NhH5S@UyuP@l_fhSq zs_A-$1iiQ*c6qDL0xOAxJn%$yN(ob}h8_fqgT>ndAEKpn%lereY?=t5A-nHh3aT_U z4=C(^!z1`@mQ(-0v!~{_mCpDyxQ%ZdL_WO(00Peib*6X;(CWD^0zw8yMJ?*4&x=kP zs=TL>;?V)&Z(nl}gOtTCbm%O^imVrXx^iz&Ah{*iZEh^xp^2Sw-W4J+f`RAG2l>dFXH`ZI4C=qJ`!HIsa>;XN90^PC z!v@$BrP;jtpxZy;YY`pzq&#h!T_vGX+{TB(Tk)I!!BC|jm6==qwk`dLK)7u4KwwYT zE*oX@UT&~*Sno$seThuTLU5h*6cqp%+(s;rvRy>Z1oR$l2SK)&+blfLZJaCkYtqgv z<892fpZ#GgvxVc-%Nd+X{tLJJ_kjM7?Ll@8o}-i7>RF9Iu>J!hfz}}%MeciAA;;7ggXVH^l6W8O1S4$|7X)oa*Xto%ch?LQ^gvM1Wi26dJ3qiAbyUUGg6J8?=qsLw@&>8NAFH`R%5#k+7s%dhBDMbP$G5sB$6;%d%?^1JcOyFs6|e2 zXWlKCv(eZ8m~u_rLOt`7i`vfp7wWN=c=BlYAB((7AX%W7|HJXR-r&oX*yZ1H|0SdN z|2M(lf30%TjDgEvtzgt(FA7TVd)1$*+=R!sLw82Uu4lWspTGQIhe}N5afQ|Pf_uop z8v0()PySfK5I6|Vmb-Qz9bQX;*hqmv%A4|q&!&U+ol}&ETAk5r5s^Bb#a0YZxA2_o zyKMkobukLX=KoAXQSfQ)GN7mZ4N`)DNmU#nH$C@Z+``Z0VB;H}7EAeSIYIQgLa!aI zw=sAlB0J~%yX%c*Sgt%EJcxD05^jRSG8J1JnHCOy$eyeRSKlH{eDd6*KPT+kxUJb<54khpQMig|zu-aJ+hv>jx#z$?B1 zSO)lE39{63rsKk}7=Ih_eD%!hdZVq4Z-YL^+9n>V(EIll9;X>UdF60WAf7adCqE-W z7RcG3^n2hzN!YFtph->8*1s@jC>I)VX4EO#eYtr;E4KV;L~W2yLtTw%uWu^y$)}VS z#G!EN$>bwX+}QePu%qovVAU`qf$~oYNo}KG6S9d71dY1YkRK7A15~|0?>z?h8-yn9 zuZEqSeZ;Y2*9p#heS_YJ2=GGCYq3v&jWeEH28M6~e4Pd+V>mgqcJni;*bn^d(OY@Q z>J5@{u4!p>#E$n5DprF|HdpMuXW}v3;!7{l>eX4>t#>`gV2BnYK4uLS9~MN%S>v_z*Q8Ytg$Q1#b2xX9H6J zdvFo<;_!X6IO8rcXdVHAS9fZb2jlFM=FnDsy}Je9G(&iELsF75B?c&gUU z2(2D9(KM3g`=j)?KA&5f6^kb?fSf ziD_M5g2gN{mU_f`b@&C>PFe4eVzUDapJ-tZU>k5ukwOZB3i7g@uUYx+vYkHARG)5a z-tMuRjoLcPY31sVuY_J|f1ek1CMiROqf1Y}SHXIypC1RHKLt>`0o_=p)%uIqJK+1>YGhtRfcR zF7Un_sv*T5R?}Vk%@AUDZF9o$(NObtz{GM$i?-Is@h)Zi8!59-9^IbOGXeH$mjz?3 zWh%wN;3b1;-0}~HteGB&*=v?@cz^rdo`_upK#WzAsmwz8j+0drzrjrfX2T!y23*^kpQ4|YKJmZ%eo$^nG-ha*n}`_$G~f{y zM1h5L7Av(?4l1GBfUHF|PiYJt6k6)#akqJ_YoNwfZra0ctA-%co!Q&soPwU|M>30kFT#M6%+F>*`0$4n!HB8Pl0M+uD>wHr~E=O||C2@v(0H z9_9NH{&tM(#|MM~kso`2-HV0JhlBW?qRaoWomZm|jWHu=rEI<+ z+Br{}R2?Nv@7u5qd+`b!*s#&4qj4F#)_Su~j91IY+M)dk( z{Y>XF!{ye_PXQ|W0_+?tp&tQR0doNQB3wxpCbrOzjfbLLOQSqb%j&#(dBHw>XTsqy z?-dz}^p)(*xBAvKmTSCxjY7QqtO!T@r}LC5<_;{u0)#M?@L_;u@;!r6I=z)yuh@!F z*9ol}x4#Cp3yp;TV#x4)IW>Q)@amTA{S3d`*G^TX#CAoCigEO`1MNpF9!mh(6uAIV zhgef*NTTIoaR?4+R|S?7NjI@lpq3;gs2)GFRq0#Usr*w%ryx0e-s4lt%iGdlhpv9K zr>|Pb6MVro5>Uyv+RhDgld(IaiiK-}?-(ck8jx#@)ckW}cGgNhrHR*t_|fwG!)P(h z>HnsC$Rr#%|I@q5{`dd!zD|E+YLtT6Y%N@kI8N5K->e)efJ~EmEEQSXgkMOw})OBe*E&qb9Pow z({r`{yE5;U`&{u2Qh%!MAsz7{!_#tXJ`eZwF|3PE1YJLsEHQDDWzui-lK-Ti-N3-5;IxT@a~QyL2-E7zIP+J+CQ#OD3-NaYb^U;BJoWRVH&dan zdjd8+|I(Z3=dlN^_Q#(-C{(uFrE;Yp`ABTv<_37kt-tMfje<^Z5c~SD)h?9_KUzZ= z2SakkNpN%zf_UdnfAIy0`OIU5UsNa69F&?)-K)$?(em}V6+SQwt2L|va+U{-2w^{3 za4}yJEezW(Y=~McYTNiWL&bHa(}MvAXr-8CKfZ-E}tin?5$}wYiey3;A#_3P0TH*^5F6Yko6y5&c*n9J6sNcVTTt(K% zzNM+GS&}WuGKp*@A=yomEE9@sVFqR2q7Y(4vKzYylYI->8S9V`lbL9YW_f?F?%(}6 zzx%$w|NPGPoZt8R{p0?lvdh%^YMHvRgT^l&MXz<>%I(KPWWQ8dd%a# z5rmT}Pm6%AG}1-DZ14e+Io9$O^c=baaZ&{f>nSDU8cZ+$XxULzSx9gnbhxxteCRbD zoaK^y>O3G|LfhRbDB)4cP<@YMQ3(_{d*KYw#s!RRDj%3JWC(R6C&UzO;=%p zWTaK3ww@(-)tr!IC4NZb!GKYr{);d3S&`wqe<%gX_iCsg=`+g&6C}|3?otJ}am9EQ z;xSwUDp7}FtDVnF&gs+S&kDCDbyl}hd1J}96ST5xO}zFmx7iuGFCi=pAXHu`FbD&m z!{5rq7_5V#ZUi@+VzeX?vy-#7J=)k+0{Z;3eNPH*ZBa_Iv8#YuEb-!qm+$?!R`Ltq z#Fk(ynw2>?N>TrI1cnf!5Fx1NE?ElMV>WlNB9KaJTLn!#+qvt-Hem(qFb!OLV^&|z z6AAMR;z=nZKR7RHoUz`UqIiJUF9~oQi~!imQMJrF^8^ZDUY}MaI|hxDQMc$*6Za`c zPt%`f-r&HHdcLXAipQ>{NA~U2fw^;@-ic~3!_z3I$ci}FPAblae8Zn6-Fw?}CII7I zSN&^ey!RKc6lHEDsYap%Y`%VP@9|vzOYR2jaKN-h(?zJvAi(RiM`QMKGJ?(1!U;%e z9TvL0XO4ceZQl?6mMm~_=wVK3k>ig(4+#e32b1&XIs1MUQ9{A6L7}%&fB~)Cxr7yK ze0fQbr6Czfu?Q8Ny6{_N5@6ld#=gXlvyeX7_b36 zkHmPQu;QN^m$b>wVD=T`ND;ni!|JNk6&A_qu zWUJfdGc(&jx$8%|T|;XER;s&k$XZ&b!3;uX>|u(j$73FaTg$w2M#jBI9r{Go#n%}h zw$B!%_qiOhgP{h1n@xi@qIb}gYe5qvFgGHj-A@#flSjLrxxENI&-W)O18V4E(we#vOV-JtqyUV$_~n1-uzIrypSX*isPg_ z{@a+{*7)s0yhUN>H14Km>M5AAB&* z@rvd@3<*m-Q)j9%W4k-^W^Ggcf_tF}?(JXQqWR=q^d6^%mnkofZUTmOAy5&GFIUIx zoG794)5^8ff&2s=SEc1x@*~cbG-SVnHS_U|rMteJ=o!*q^QLv3ib#dEsoq|H0(QRm zm;Wm|xBsud1O30-1Ea1X%s^ZECd%UVgKJSWKSH>=zlN)c!ETPb@cgiRe`bFn7^*ut zqXA+Q*gI%Cb08uYkc8qYl0*&mh?Uzeqpc{ey-9pqfFS68Y=DC1ECwVg+r2h-C$ZNl{+G*! zRCz^kb|2lK>w#qB2AP7f9l?jN|8z0sKR*^v^XrAP7ykQU{rAKA@0s<#^E0fp3tH}M zs*SzZ>ev_YQ5m^;)vByC*u@RW9Zix4nNzq?~{$yEw4sEs=RJ#d)oKU(+5WfO}NwbTiF9asPcxjt>Yq7&n$ z^DfMH@Jsc){n<3@v%2^B`Q9=Z_Z?Fkp8|l2w!w%a^md$dQ?7`{{Zu{iLJsBHx4@X^ z4B2@(-aDsG!B_M=})`>c&f|gELn-Okh|7mZK z4`?#+dbD=;;VXD&1yww>L^%24aU=97u<{2{;xShg`^@INmkqwE&KvC_B)13-@|RK# zq~bn2m%-ifwp=(BPtzx_x#ChHQFWu;&{&{g5|bT2xJg(leO4mBX7ez_-a|q^i%&62 z*g4uFm@)ZxYazQ78vT3r#p2v1jHz8ERDn3Eo@Ft4|#Z9x$&K; zsnP0Q<97oqt4AlBM`R)Kuts$%zY6IVxeaF)4H4dlG6s!ruXY(1lil5We`1%q?k;k8 zHk~-LG)=7U$Tj=2l_qKW-h4;&Sup*`NDJyK6F(V8=pwzQwb0q=^QXCJZ;+3=aAAJN zB64!kX-Cf`YtP(djBeqn%n*HDgiIYjZ#=Y0YW?3Db9J)ebrJ+2w6|DravIq+CYyN=b_*CEVQKEc(yXaM^tfwU3n zYjrh~7xpVJ4u~}tdta!OSUh)@r+b$3qf5tXaoV`K^KjAF{!DnQ-(Pr+d5kU*bb1r! z-a>mVl9crMuGuB*I%8`44BJrpy?|5NKC~o6W-4%R`2cc| z^j|N`IvRYnQ&^te!3x~fdsDd*5}l@R{))%TqBwh*FoV~i1kp!e4IAM)Oo+LS&h|5e zNy+p=F#{K9&+i+QkCl%)80!*EAFPlB*-_bceGJ;ilO z^2LH=R{Hg>Yn<${dmpQGMQakLr;ut$5vm%+5AZl2%I{E_R$**dax#)>$?V;NqwUCa zbx`QC!{K9xGeg>6H5Rsiy(MnJ@}cz`cw@?KIIP|&2N=loWL(i8zO}nL6KaUwem9a^ zG*`naQebeSeq2L{NtA8q*GtDWF@=gh4h|Gl8L)CO+nJ(R&?j|xX=M_$IE>dPZIQKZ zk>r=an>bV#_d89~^xBe?oV3; zxj2Hvdo!`7BQ+%*=ktCnE$PpDFd9kRCU&#mkc;>-Ac%-57h9Dwec3@4{Vr{a$hE5dW#)=pIxYs$Lpc;jbKY zge=cNNd(H%>o zpMG3^9nmnMe%OxX?RsG{Nz)_UCb>xO9wolW(9tOm;57kofxhlMvB}@?VP;cI4^`qEDYBc60kcHd(GqmA&xW?CFoh2oE<^3|<5 zSm#L*%3-!JvLn@d+YOo-=I$)X^VvUM_xNu~@{;biJDsNnKJBQ>+0b9fpnFG8(WfLGcV#*C zZE5r4F!fD+Xm-RG$Wbj*;&$&7<0r*r&o2wA+$T?a2)}ru|DM2+b|qgr;_b}06Qlp; z-V#IT`+Ng45A6}Lj{HFA7)RM6S@p!apXGjdJC-HjqZ9$VByr9<>HVX8@gwxx203XT zHEd{v0t|0h*fmS3L&bbTEu*nk;|DjC(6CsRGHraav}Ps>|{*CQ`zufgy0bqw@r zSt<8ym13w^OjGe{=<}k=7(HS9GFYY%v$ClWqVpYMfM z*tk3=OH*zkW+5y{&s{D=KdeczF{-R7x#Gf4s?sWq^Zm;nRT9gCJnZWEYq%Hps$H!* zWOTv~_2x7kG}3nvjbpG#CoWnEa-|s3z}$yQgfKzf3<$|lMWI?m?eA~Z79X-^WiY0% zH8mxx@-V-S&00@=oyeV3EZROq4gn><)dYxsr7P2t+ZDqS-Dg@(G{sUypYS=|to{66 z@{5I7-=J zV-s|xAJAH8QUO`6#G}RkkB47EXZJ=_ zh#R{Fi?qm>C6Cpx{iwJ&_&M@OKe`)3min1WF|b2+T>LF|V^s=W=tw@j){%{-ijx8! z8)cHdg_VDy+)R2A`W8Pw-pWp5ox!Y>io{TPcRT44v^V(#L^=-1VF%6uZdlK4?cSm) zRY8^3ZvO7u?|jGf-)xi zmbYVPdkX9t^|7BnhUoGr4YB0uDeQUYrHvUjAAlE zw6dswvQ=l4q`7VXv1`NS*JT0t_hE(Tq*8_zA#=1<59R` z|AsO9wMtIX!|%)w<#^7!=f5~>r0ddLbXT$ym1%bW_b2+B<}n(lIV^RV^Y4o=1wEv@ z+oUaW>bqEf$5TVaPFn)r=8{tuPrT!VU)D+ZsuVWjheZS^>j)oqBCHQZmbdOs(7uKQj@Ip>jaWd{b7U2u=G!m}?O^gO3?7VQsAqBou{-kK8k{f*`)!tfDb zj{h~vaM$J~*^zS!g5-KnpCe~^591tf+@e@DqFf&*YwbN{;TbqlulwHLit|n>4{!R> zLa8cF4+!R|^*64a1BxkP4h1j=uM(sSWL}@_XM^N*_uFpaMB`@HqYPi}hzJYZ#&jkl ze%^`LyX=!0vWfv1cLj_W#6cH!!RknHa!83qIGOn# zE1I1cgD<40k-2q5hgM-c@=8?uv?VB8%lyc880j5%T)WNa>&lI8l?$KQ6W5p_nFpwb z8=MwayHD@Vs;{gboSxR`V+gZykvm4@bq)hayozy5SP3wh$;^QcyU@hHRCtF_EH z1P}EjExJ|iod?!xU*}><*A=hHmZ*Y-VINT&yV4Lhmp86msJDK9rpg=-fECjP@4lb8 z{D2#L%8tN<>#%^+&p|K&S$4QDP(puf)hbS;os4Pqfupe5msXb|H zi!jZ{z$vHc(HtlMtb_w>vR4ZaW@DTTRfDtv8AXs+@j2Tk$LQcy{Lz zF;2)ZgKbGH%^{y ziI+?}e=gPf!B^EYjbHgH$|HJBJClAR?)x?N?GIf?0Mihi3jhaw9n`dzMK+{JZHfp+ zwaU4=h)wk+P)!hHVv{2V&JEGWQ(u2DI%gAc|NXkP^Q$kI2cW51PO2wkb(9c8sFRSc zE6851=xD5{sZ;-rODO@0=83-bFyy!MnY|0U<~1hOr6li}kB1F4e=r3>DP2oak#H0z zZ2kxL{NVJjw)%3}_oYewJ;p~e#btwqE3@f=0341Jf#7ea2CZ34stq$yPjDqSY&Mij zHfB<;g*oUQ-@5neg%P_Dp>pq@dI|cymhGA%SsvCs${Pggq!X$s%%LiTzL;I~3r(S>v^o}5W%-KN+s(6Ol6r5IwL zO?AwTsp7N{g!=y}P{E<6T9%!f40#A95MF)f@q1eBf` zPpB^OdnM;{T<#T@gn-tZidW8G58YY%Ihr3jq&rlnJUnc^lK!RA?*~^BbzhtPTBLh^+Rr;_*KMV}VwFTb`SuC-GC%w6NwNa_`CffS)>@LSNzFRE zL})bCv~=#Ar_L$ccR4rOqNv}Jtx``KbCe{Pb@-n0?ONxeI1(|Wbn>ee$UJ2wb7ww3 z>~)Y&U*j|CA9R+$b|lunaF{ED65Y{e@RQS!-#a<3c$pw+X1OmAPo>g|z~^BW2W3M( zN(o0E(~={>cA&8{B0JA8x!l2xThl~iuGf_4(X5f5oYzBd6w@bR1Pjo10Dc=?AXt4E zNr>D0;})*=^f@KaF51Ft<>lpg#$>3QGPc%;~nWC9$mhStJ(d1m;`iu$RvMY z8+wogb;CdFGNN2aS@D9JdtF-^wxGkK(-;O1Ai?jB8_2%6_hhO43@rl+^rdp@cnrbM zoE$-T8Uz7B5>oH{T}i`t*`Hr1e!|LmE?RXdRX?>>T=PNQ#)%m?$mgJPv}ai`G=%nAi2Xw;|g7hWiKHI*D^u(VZcdo_DR1E&B(eXbS`%ytlEwUmK zp+z5*K#rMBVD_K#|A9~d9zT#D3PcArnxG3je!hH5R`zf|en-w5sb~%Pgb;+KiinR)hm)=V{+Q7A z9@m=sXXtFsiXeW5Xn-W2hs69g0ugO^ee&~8gmk3+-os^gMaij($d0Bs9>dPaL@R4) z&sqUG+Oz0)RC4j7>1VH%7OpK?SaBBrU^({|uI9S#)4PK617|rps~Ik3cGRByX$>6* z#*gVzfOpYPS2voTp6_W%`n{0xr`xy1SpM<+qLQqIQG~(})BGpruyhhLl|O`TzDTsD zcO3hN!SFgPpLm^*yW(Pr{?(&pi44s5WKCi$_P&cGXFyRf9`JmEz>^C>&VB;TwynJ0 zwq-y#{D(n0l_K#E!|WR9%;(Qr@8t43&D_B^{KEj- zv~HjF3Eue*e|G!Da|5D@%GHCZo4Um!(~Y_E((I^Fz(U^W1gPY#6hZnNY93R)(dFp< zv#-4bW6ar>k|r(DkKAiGKXrAv-^8k~!tF9lw_DdV>fSl4xQ ze-7PT-HPWGa$guT%3LfJiLyHV`5s%HiGOgFnkeK2;I&m6_CjkjyHjESpKLiQ1MXiM zpPKj}n^sVe*>G^jtGe0$5nWu%15hD5RJ05!qK?}a z72|az5nnU2xzi3CjMa>*R2Q!rw*2~TaHwF~RF6U4^rpqfSwS-{B$IkzzmwS+pS(E9yhC7Z8{R=aXTp(6|esZa6fzAzTe# z+21BH?A^Li?Z_U6xD5 zsZzzKxcFWz#Exnn15xuk^M1|1BTtfqaxS~-AOQQ+@%X;ekq&rt(et~a9AIPooc&6JK=@@t$1?tpyg>GPf~gNs$ddFVwt_WJnLr( z&dAqNY4tA`oHpq`t@bM^r^q>U7Gnak*Xii^_h~o6g&L1l`(~wNshj7qXFAjYClSDB01AuwF(m;eb#p~Tbg3LT%*6kj|Vmeqy1UDZPjjRb*B(b1-Ir}hMmZPFvuj^s!Q-6bcwU&JoFgpp%ZiFWbWFVwTHx`3)%zQUK7}M%>>~Fy z$^oU3=!&LtW#WM7LjChy7<@{HXB*C0%#(|9ANvhkxS2?naP5!dWUPRjd7+4W#m@uaMe+v$$RjIc05OV0AQJeMU!lS{m8@%QLC{R9@jlM&II}pPDMe zhKPGzS1?Hkrl6UQc1X$yW@=x|WN=O;X~Mz2KUU>uH`_i%R8(7eP)z+++`Xoz>+v#Q zz3%7>S%F!8jKQjG#M0ZLN7B`WAWmnUq8L(@h)62$b4OZc<@e@VBKXW%;ZNW#*VpT- zO}+LU!hL`6(QwVW3omftYQHzAJc4xdEW!-E)t`%|(2SmNjbzNK;^s42Iet&$%+rD0 z_>3}t-nNXNtL8P-T}ICJl$Oe>9Ok4{COdx`N$$qb>ukyS zJryC>04rEle0YB2kv*C(*z8yWD*zX@k*r3T=w7-+q@z^@#)6bZ+a9uSZUh|FPrDoN z;!W@FZ&Ru743CwEXpvSd^=-=Rb80dQ1vjz`PFr0S3*WI9hUU80aKY?B5EJXfs z!2X!&T4MdW12L%`+aJ0i8ggDzoy&{A^@fQ4cv|nZ_;)ulBx~Njo!LvLc#(5f7W;sX zrRVfX@D*hR#dNnAc0L%}aU-~<=Qv5F1g*-@r@mLFsiw$ey%!BMI9OC;N(ni|h&V-RaDMPMC$lzND(zyMSmO0T zY0bAXr&3{7^sf+tH7tC&-e;%0GL(ZX_rVj})PszDc84yS(s{r4iFWQ%p>PxGOI9Jb za;I3oYdv;*&l&p|@y*eVq1D|^ zQ_B_g?okj6@x7(vH-vM+an6Y!siZTRF^za^@H5i113A|dCQWew>;*0tS~_NXz6bT^ zDx7pW9tE$&?vjDYnHL#bfm~^XF^vH3;|pmUAf(EsHqNwKQ_g~(NJ`Hq06+b#TTMXo zdLSe|nQ;ao3?{8j!bG8)(F5}7MoT(dZu*rKJ6t zUgi}U3qztVPdDAl;Eftx7bF=uih*cEOrs3pdlWQU6<5A}ZN{*LQ+fh-@+?IWOheMjF_qkKCP&zbFO4a*wa7oo@BFuu9+R}=$NAqAKbhDyQGLa|ieUe|i54c? zcQ5l&p+!OH6?Hr}V`e#LL_V!W6NPtu_=|5R&MRFqEc&`x-A9Rt5utw=Zh^Dshaw`{ zmX@K#T1FpxG9Z}bt@;LHW&3I{XIKO4dh^17iI;NK@tK8-N-%?5O*8oVB$j1(uvGWL zSnK&8$6PLxh8>D5%F-GlmFk+weiSG2s@W{?F3`h`&d01C&+>mlnCa2s(K#{guXzU^ zJ#k_B8`x@l^Q#T#c)~THt>W@Wn}%UvD35^GJ?OLY<&c1yL-fS=!W9sTEApCqd(X}k z?h_K`&u6N75m+D7a>ZktWjP;gCB6TL!DOQ2#1{o0-vkNoi-TQfp3gp?PFq@98fR>5 zxE_D`vz7i4tE)Q(*H2DNL`7&`34}f^NH18}?DvR(3J7kvm z`Hxo}Oe!vrHC8lQ?m@KSYoo^wE*nb~vo@c1F)K+MI9~cZWi1h+;J!#R{+Y^#8+PWV&-}j`@-GzE4Q8$DerlXD_%%N;)Qu-i14VO?)1yDbj2@ zeGu$5@$FRh0u>NfBdw^92w+Ezhw_0T9sOcPJO!M zb^cg?vv{bht@^RP?{1yT{PgZT$p%2Or}E#036Y1^DywP&y+hAG^;cXnvJnlE7q9=d zs(bdV`GfS6514M}7}NVimX2q6+w%42hj)j5KAjJF7O{`lv7ekQioDbuXY#&C*WYsT zc<)u6tasMd-t&hEFRAyy09nKhDJ=ad%yY3soiYwB`tE7#p*_D}`y1I?K*5tIOvJqK zrgJ0aoKB8;hw8xzKHXlRK)v+F15^968?IFrvnxa`67HD$6xVz_A8;JX{=R?&TN`>?#bY~o0}!PzAw>x zPe&L3wuA&1=)<753oNf5kuy!&Cbjr+tp1gX4j*e1hA-*eX%ZkBt#EhjiN%zr_$;|H zlR8H_`)(z@o4_SAKD=;Ojg9^E`-o(-O53N#C;QHDR9w{kcG9!0?5?ppnw*fnT}oXf zFOxb!Xd+n9n6A*WTMY@X%qB%Uo?RcoFeNvHc#={*)qU;c94gplI0x8bbst&{X$XBD z_yYooxxAsu{9-NA{sE90)Ogt*@ueE8whogc%21QmcZ&4lgxMZ&0p*-P%f?egV--S0P`2N4-RJ zZR&2b?3^LlO{WGH4fy!H?~^RA#DAI4IoieNqvyk&9t(v zpm`k0dUvNDeQTYb>h~Z^|4ah3w@PPXl%O6&3tFR6W2~(b=N|{@^Cuu?PSp;5ca33Y<-me?%r?x+8V~7mlxA z<+qpht*`avxjxoeUU>P{;EicVrY<+;tQ685RzgK6MC!V@rwEq)5wV$i$G+tG&eV_kheOug0 zo)iUrD!lwW>C8*`At>=8f0M;K06xq_kqhF&eNXpzx0!9-qkVd5u(Ok|sj(@J4=0|) zU}e6^P#$SI7eUbl4q{joJMtb7L)+5c>&Q#Y+|3R#^m23diqQVOe|Yk9)JMl|frlC_ z@x#v(A$<@&WXDZV5Y|GE>xd}gJ&5xp_b~-JUk`^mHYyoaRu>dwIb-|_Z{Ey!-BMR}{sehLp0 zn)wEy3e1zHV8=tn0fvev@N@$p!>~{Dd?>VdGvH$?{^{a%JNAUq!=q~Mi3y?_R*b4I zcH%ap8ml4iTbl3<1orE+#6J8Qvcv?#HGegs`-XZZ{pIxDx%wx;U}bjt(s?EaGcDG< z3rn=)qVi49+0TQ<;XN7}ae_W9A~xEYRr!AM(NoH>6$W<>GZFJ8Q%SbJ@PyF9NqbGB zi!PjO8MFt$-q#rxjXHt3=fh0zf(SpQC<<>r(@FAhEw^9n`EKXr&~v6awNafAR=jaF zn1PK5?p_+dOaRWlITYpDAQv3%g|*YAX|GK1J#$f|odvTb4rCG-MbD&sr)amkFv=Vz z&(3`Je`iNB;wnSlH^WkJ82sOMwjUt#+w+lH0$S7L8v||j@E0l<3$g|0J7j(YR0D?< z1~>lS{zz?9-N8=O+AdjohI&$P)vOC5=%AgMIRDv6fyh}z3cl2kPFq5$Ch!;~B0`?p zd~LbJ5pR1-35Hs(1}FC}p!fshVw@+8;w6bQYk|p?T8>hwfzr?OCqVUhYwTT3+Ji=q zb8MqpwY93RZhV)Bl3;x~)Pj zj>05}U)r!B6n&@+`m#6d`lx2dI?yd(UOKDJkEbU6Chod4JM1>}nR`Ke_`iU@|G$4~ zd|Ey{r3#~fIrUX;Om1Ws`R(x-62I{e!&S6?K^U~ENFR{0zx>NZ<=B5VbggYz^tnlx zjtO4X_B3b8LosHm^Qh~`OUi#ig!5h%skg=jhC|(9WZ6-C2u^bp>qfd)LkNrxWs;wP z5|TFC&li*RIO+)Xh*b01 zc0lH>oGvNXqm!sAv%F+cJAzy6?*vD=JL6AX^W^ZVj>o--nhfZ5+1xn~U8u~;;q&d~d2v!D`5>R_ zOK940rHF0W`R{hacx`kV0!y}aM{?e8Db@9C^owg|VfAG%-(xuj7ZlQd)x!A{kz<`6 zDn(ufAbf6gOoD@dwcl$O&jhwRYR zf>3nPpkI1F2yQyFpLF9^8pV`Y993B*(5Z1>Atz*UlSe~LSFZCO&M*rjcbGt7bfho0 zi#L)dR$y~nbZ)O>CUxX=^s?i2o&@j6f#9n{nZ`!qwThDlFJrO|@Aju3Ww5H9QV^SM zZiE(Jk=q-hvrbTKLrrMKtw&bdxYqL0JqpQtxQ*Bgt?E^Jz*zA?_B_K4A^C3)ep;xX zG!d|jX1D{sf5&LK1EL|r*GQV|uS3>g1 zc~x?lUWG_Bho$Ifk}=?Da2gz4;_KV{nC=v;-0I&v@we)bhSQlO7}-ReppA(Y4Aq|xtF_mEL*ESu@NtP+ zoM=-wrAK@TQz8nkrF5cR0Q%Z!6yh9QX)8M>5#5_vsCQ0N^iyN6xJ7)2nRte*#Q*RJQdm%L#d`Qt&2I^#}qzh+_SBX2qjf5yK`ZAd3E00oE z)-1@&k~f%=^&@Tc!4jSLX($9I`PCXMsXM8N{IJu0Yy2cfElVGF{{7r`uwTbQ&6JN`u3c!0Htj;vFmKtvK_$Y3B+keVSi>=1dxZ+09K#Xac;!<-^ z?Bh?r{OIM#pXFN^>?_hX8AUk2Wp?6^t@K5%L;2~-Z*XJv+P>;>s(p9jZ#1^S7BPR+ zv-!iZSDH2NJokRpR(0Vi;@uc5{vu)m(*)eA0*QW~|48GP+69Twi1xFdsqRW}Ct>4v znI=;go_v$2fScd^D_bzFo|98Z7ie!PLe(V%mQL4CS>B$p zFc)~6w0<}-@GW?($ynx1O}rE=X7u*DOeG?Un!)^G(ak`g?gd{=-tpp)2bBz z)iB=c3Eg&1P4C2e;9Sv=ZdWd(6pO?|36MK6eVHQ2 z*mvm&p+gMe)vAVH@ga9By7kcz-e>_#$j4VV;u{D#q*Ss35xp`TgJ)V%2oTm1FNx!@ zi<*V2TJlvMi{X7CqhDtE{g|{+{~1&_kb}dzcTal;LK~TTU?Q6@H`}FSEJHrgWGzXt zUb~g~t$WTsyZNas+k*$%@IfS;D;DU!uOK^4>~ke#TLuMQ|9Symw;HBA-aQ{&mi^`8 zTmoO>;o}#n>Ly%E%!wkH`_0XvGplhM;~V>m#5JItzfMsLRcHooGbwjP++w^$_(I;t z-$=8=MH=v*wGLP?+GIKQ>htik;+Wrf@t!LuGUKSwD+{xpcZrMkH=EfdRAinyzdfb= zGMJxSLu^_##9LD?BRkHw8WT0w9?DeCR}P5h>}8^47DvIU*3{RdeCRAvs2dlb2E( z-ju0QE-bnFK}z?n*U()MFk8oHc~Fm0R{AiqsY2v*8(i zyqBn-7*_2X>}QD+eQ+vLuqKQO-hHF0^*UzfJjDtz=5z@#g#}4^w5p2wbmC~&RNW_2 zLw(N1OpJmQ6;ia;H5x??tv)3LUs-Cq4TPGsafwmq=p4vJW5k> zY)F%T73FG?P-6qh&D2s2h zTUSLLtH+JQXFbB%Y+W>s`o)dLriTwCNA>;)6kr7E(O5sy0WAy2-F9PuJMN=KLUC-U z0`czmFdfiz>Pj_Z%ntj*G$+jK;w#_}dn4fL)e&J4Go}LtOGN{!hYOo`F zISMP?ymYk$?$(^7)S1=Ga}n%GIriV=jUF=BI*+8x zKoddKCLevYo!d-|>M||v-#%#W&0&L!-w zalPS0N%lsraD$ge~qFHRaF#RiV0LQp~ zfk~_Nm zLvD|YWKF|V(j}1{njm;^Io_D#O5cl$21z2*d+`Bw==5i5V0!xrQ6{;;@Z?$aI$52pf=Zd;84`B_zNEP8Jldag1$i$;5uETe-4wnA)Hx~3 zUkkqT$gI7n`qpRPS#(SJVHNnL2SL`Pk$)JX$H{ADW~*Ers+Za?MnXhUwKpHO2Mzb? zbS?xNDbMTWsT|V!h!Eh14hRYHFn+4FGXZKsKy|^GgS@}ZZ#6kdEBO0YFbVHPG^rF^ zn`KTnxH_x(=HiW9iQHE_uNtsW`x0~ygb5t%jp*`!VT?#usy?km@a9B43aDbdz4t?A z?;j7-MpxQ#42WdjcX^QeSgThwqKg{_qZ-i)+Ru{h0r>QUV!nk0%!n5S&|~xDh`?^s zrg_PF|G>I0@%g@h2=SvPa}Qu7h-}0RYQz@+wjR+-v}69@a|RVL9mAg zk|S2R5}^E;d(iki`ClXDtM{<~I(#O*_Jh6G9w~6)P)$@*TZX`=*#P|840bo_XI8QN=h2;dTOYZgdp}ObQLIfyglVfuuIdLBnJxbI8h6?;0yR&cBc>UiKXccD@Sr1E@MIb2!}b->-3IOvIl6;9rr?EQ&x++6qAt0L1+#p9QweA zFVt{G6kqG8M~Z+K%*nt@L#v0Pu{~d3CF-wjUb#hbLWTooe{?)Gm^g}8p*;E<1yK(q zcP#WyRvi8q0DVprJ3^g^(sANxKJ}h4l9iP%DM;>>82PMzY2Wo@e;_+Wl*gfZQ}(OH zohkF;H)_6y`VsDPBP$&|nDROgZl(!*O#Z>}?c*1Cb9yZ9HO}~f{uYohr2dIs^O8PxdE^RPvB{WM+l!(=7g8-?gxz3Ur316KeiQ(39{t0xuU0kw z5rE6Tq?eHMP}&Jk9PzAnl8H<~))ClT0w5AxjIXpO$I}mQzll+Sxk;)Ll zO$hlsFz7cU!KWIQvCE_uqIcUG49m63uJvcB`j38E--j+KC0BRfqb0Y?b|S3VYVx`B zM-SAmt4X9+Fn_mB1ep|R<&>OeZ1do zZwt5`4I&5S_fsfB4JkD64_u%>C!w)#z@`w@1;(ia8x!MOKqD^#P%r;~7tavz3jYI> zaPL3CeKIJs?NW0vnmU<(*%toC+#$JPx^fUZT>#w);ioF@C1I!%+1jMc?~%G@jllh@ z|C>vIuiUuIyNl4o(a0qZFoAASdC4yR03c@L!io8H!30|+C3_l+?@3OvOlCPIT3~Z* zb%q=DWsf{e5~2>8ob1D+<1Xo_kWIluMd~QiQnZ*?&0dHgnpatVJNn&-cl=49?T=(yEL<_D*R>w&>ueB+<}L|cwO&?z(Or^?cv4si$M zz~Zt$AUT&CpM&{vp@^&i=6DycEj;fcF?I6M`)MuHl3e4Y^co57$KLw4gX;zwtcMaj zx@YO@ieF#}*CI-;1kPwfiKlp!TCB@MV^fP^1JP3wI`+H^$-svg*xWzD_J#3pHQ=IfJOAn}stNYK8&b=;Q z8pOh9KFUC{VRai+zJxAVLm&S1G6(db&xysLSD&9f%$vE=m-wC6X5M}wcgeB-wyC#> zp;IXpM}d35UA!|g%q5fzE|$de-w8SLxtu>IyUv*3ZITMshDbZ) zK*(Pr+0F+b{@EOG2bVUH1Fr!RGdov^-1LFfKD8v}{llRBJo5%b6xneNgg=(AaqXBA z<#00*_`Cz_Nmb+FqI$ijuZCE8V!sBu%{3VMUPlO0uY*681ALK^z4QewyLJ(u{23avBCuF*IbP@85wvPbNRhlZCw(q}#b6aVwRtSRG?T~eLxkCX2S z7>+ajEC_8i|V@Z@2+pwHMR|ZKC0->tY^|{B4C!({sk`&QskqOJa zCH0IYIe4QyTfK9_z1k?zNT1tC1LR6R^eEMkVuI*hf|3$De{xnVj*JLbaq6$?(UDa_m(ZXgZPJ|8!*xbTA7Xjvcr;UPM9H?(^7g4;_?%^ z{~%+jH_B;gA(~-wrZpU=RLo92lJL2Rm+Nq4DN0BTlOq(;En|p=^noSRKMaL{Ml8~2 zI#J*4UOyj%pPH>>d3i?u*{g7k^7<=f(>HW|f4i;qyf=nY1=Y=?Z{m^9akp5RnJsUv zrNYD!-H^uOPACA%!b5c_C+Tw^4mz>B&(&_s?PrY-!aw>aus11pNhtV;Qb&5KVV*1D zHWyuCTSv>xoL&4?t<`V$NxT#rtCRU3?7e4Hli$B5iXzfMdKVNB>4G$A5s@Y$f*@5Q zy$OgYMIn(WAWftl zS-J>3d7hnJzU5Pto*vjnF>jhRSUFW|P#JS8ATL4Omypu1@(e44F9t^81qNdYJNrKi zUtQj8O8mBnEuC)IuIX1GrQ2lb= z?TP*!iHmoF`}UNcK8e@S`~fKD+Rq+X&jDm{AK9J)BbQmUO?95M zUY#O@JPT2JtIg|wb!W0i&s4oj za?cmXZwgEOV$sO9t8XlzpBnxShZO0MX{#u*0yF*o|D5?4i&g0sth^VGvSV%@3(^zc(O8{P%jHQ74)V5q_IA;naU>Gq^`iZEU*Pr?gz4SPAls#zoXJvg>*h;+tLonNI=_aa%j#$) z`#{N@j3RxdB}H4W(}W`R|i^R=h z%9T(cx3F~DO%Ze;eN>9LIm_uwCc#j$CK_R3-vpn3CMBpj)smB_8@=JUa(zBvAurCpd%pAhjfq7a1gp~ru27l zm3t&iTl|1!>AqjmEGxga;Ca{&J}8wM0QS%u@Ggb=l_V%%y8z@dSH~Lu!kzlf4;%-| zx1DA#tVEy7!$R}3!_(^mj~YAN@WoMf$*0rsOo3V%vk~aJ5KLit&<@tu{~O_Z-AAeX zv768BFFGZR46t3P@j!Lq2F?dbaV5h^I?wyN)Oc(FY#*FLj5`;HF*RU1kuE zmH9^`AAo?RM$aq{yJljXUyaw_()zxw&vzJr)(F*=STOH`I8!|7tWj?vDd<4%=a70- zH-vSCtccT2M%TGdD60k;b~nv7{)ndJ-KO&|d)E0BwEkOl2tFq5oMM@(c4xoL0p zt>2J8T8e<&&r&nWd-{rs8d6L0yvU(vtm(OFnwwvNN;Ym(bg~D_&l>3I&Jj)`^qouZ z<&873nNxVo+yJ<3BaoW1{x)u`?r%~LtwG;X(x~arzLVn8_m= z%u>EunUgj;VrXCDb3YYpk}4B*{0oCTBC3CH?MFuO^NS*mx1S71O=;zX=Lr7v_vAI^ z#zz%%sV(tvncG9)nQ21sLTxO;AeP~^I?p5?^p z>3wod#@g3ok1jX12aWWKNqyw(k*GLY2fyf`)j0dnp!#c|k}Ze24o8&%i@vC(i=~U$ z{rcL(Q%P!Zm#pYUs)!WFGLOILw+orUx#eJ$Fr&%D;m1#QDb(}|gYxAobQ4Ft~+T-5g?3>ra(zblwn!icKUcN5!*)bMjzK(R0 zr`%t{{tUgR=Qp+?*9!+v24cfC;7sh@L@(6X+DBm%)*}!=FNdt?mPolwji$I|ucq-t z>&6`AbklPRma-)8I)^8=mxM3!0C^rM-ON67+G=?jf<2E~ndQVMg^M>elYa6eAm&3W zs6jqEw0_xcL$RZ(5RiCz9CY56>}Iuw6xy5>Elo#Ak=_U0sXebz8_X0DbybMXNJreK zGXs^TjP|~XByF_PYWMNZdMLT>P%_kV^0KhjTRdl|QlC|Sn3~61Hc)69jY)7C!aA11 z%6@xO*vNk-ab;AVG0d{!jG?3j5ppFNI_)`C&oVGFbMI=h%v0g7vL4Kj+*z+{dj$;~ z>Mry`Ykd-k$++cWFbMuc=42^iUmeOB9zb@}?2M7}|G9dhi7^=U56$$Gr>sB|Ie`zJG_yx}`e%K{&pJOH4@d@EiFE#fGX4wk{|rEcQ_J z>9ZcYJ))4LXczdpDn}p!`)J|OPb;&4yg`I^46(`v5uV=Otan!%ZftM6)!j#x>Wsh8 z-pEx5JR&4m#x5BIp0!YXSMS{rleXbW;vsg!X3N_eO~z zeo_oAA2c>KD@5JUP^eQi5Wk)N1lR#_lY_t$){+@lK`Q~c3)xz;|2!#1uEFUg=Y`n& zOgl?BOG~%&Ime_^U!;oIF*PcBM#}J|ZTFQgZ9ep-)>y{`@sAx@5;KoYlC+n(3(Ky> z1nkvL`p%{KfO3O~z8s;)2t2J&v?)=j+d3YL?8ZxC9yY!z?=KCQtz90|(M^47`5-^> zzLTvcwuIr*j>t_8I7oB?(+406@2)mEi@10PFmVSG#*LJn4f_% zM0#2hj%WFZ<)cm~dZAxt9C5Pb9Z~CE%*I2!$SGaBvyn*TDC2E`xQ1%WDjFBGJtYkmFD4Vzo=PaAQ^{YEKr@r0dab?%Gv;#$F zIZp%W8i!{!Jj9svXqSAo0bXyCawqqW*sR3Id0`ax%*UEXy;IJ@quuf*S>?<8`$t;W zvDH&UZ~RM|M3XzKKn()(@q_A`BsK8>NgDfy!PptEm`&^`Y3dYcNAToe#TaP_kOeA~ zN1I!FObgid?I==oQ0^C;?9NdGmTdmF1(E-Eg_Hk-zsIco=pP2@_+;QP5BbCNP&R5( zvN00dcQ%S5ZaXN~l&2NzmVEh-z==0Wit32}DN{C1r~i-Q%h@;W2WuTj%#tx~rrMj} zL+Zaqy!?Wv%ylcYzA=3?eWVkp{v+D{wwMm>9pOIDtD zm8wc}Ca~X^B)%nC5KgGGEUJmvD;P;36n84J1()vnu4~iEUyB8Z2YFASbpCO?(y<{+aS4`0%X|1ha{?9C#?(~^4D8uUGjPK zc?yeMyuR8hGQqi|m9wv9&F>q~60vU_D|*3l)86p~p3b=-0u5E-0V9ckHl_7(3r91O zm)=lazf^;00lo@*6?5i&$kDpZCa%WGG2bv2UlotsL75Lb4je38M~6!l1wNWWfrS`#7;fyl7kPU ztI3%ZP$}uM+C$BMU!b!Iy!PwlnxqS7$J3$}LrM7$;wL_wF@KZel3FQ}-ott%rJ~}T ztT>{NZ@lEB^YA5yUd@uHNZacu|34YR5b3x{&p=0L^Itn{#8L6~TiT zaWU|VS`eNN@PUqK9H#1*>*8ySaYKtv(_ z=mqw5YrM&tOlSI5|Dlni=rQ9=PrpXClYOs$d!UX3Ih>c<(PHl+U3wmBgtuNxtSrS~i<(Xr#cRK)jWeen@;p`S zR7&w~#Lf31HA3#*tE-_&+d!YK2t!uO=;x;4t1k^jXtgkwxrNP#GnF5l{Qkr}FP6me z&cHR-T2kC|rRDSHR=00oL6}PdT_9_;KQs|i>#+T#Iy#VSMY5@$TXrB@oD_v3ARS66t+0>MEJ7-z%BnP%mi zS_i>3@$qJ-FK`5JrLYgNpE!cOn*4QzV3!QX`eKlc%-g>|P&H=vj!kIZijwRCKFLkl zMa%mci-XGZBEL_2*u8njB7QRJjYcp&=E&FpJ*E5uJMIAzlgypaYLeSa8CEvN5Em-8 z97OL22~=gKwU0|vhsr)`9=@fx!)B57ATz!Ndh{Z+94|oiT9bgA1l`+kC0+<)!+7Or z#$#pr^R;*KYZ5qLx?ieeNfCdQZZ+>p3w(pt>ExhZr?u(J62{gZWY)|D-#u<81S3-Sx6 zz#%uq9|XJ&@E+9#MO^=OKjobN!!tFZ>m4V5-M8|5Xe@wnbLSgW@{P#k2+fx`5q$St znG9PoAp*>7sQPSkNQIq;ThxMkdy1 z_#C!Ja)AJ0=GMP$z@O4(U)PBr89dVo^dG^J6sF~({r_P|an9|6 z#{S9PX@i$`8DN@foUXJ!l5KlqI5@{J=j$pSbO%v_Q2fr#3Y&)3%i)tLT#+*eI9g-= zDkRB`qpkBxw1emSzTeJ0nps{;>~~W>>-XxU)aZ$;XLK1iy#gmCd%FR}1bk3!NZdY( zZ>v=t$+@$=FJ0nKnwdEBUSflkQoOw8`AavWc9rYy^>EFy+AHe!miR`)Gaw-i%9hv0 z>kk`(+}8A5T)h0x6!@}=`@NOTFJxf2AVW~hBdTJc$0E&ec#s?iJ+h65aBb%BZslqd zGc_!IIeZ+lgnNXpKW-Z0GP^npRQ~N#A?p&Ly#ZN&N46;UZ#-dCvNHT3^^#n6*+k9^ zR9EpH@?N-JcwO{cW=mz6Z>`6S@1(KXx1la)VO(-L+%`P#^Coa(HnTN>){>b-t^S{TfsAm3yLK-?F(45 z)X#AV#2fv%DBb*_6HTy7<(qoh}yqegg|EOv$&AUnGH|tVPhC^&H^CG$cHY*2U%~cAnNON`L+13MIoT z{CBj6;9a}C4D*tWZ@z5ILoTO=lC7_icnfO&`KR0>^zhKv@eW*-xbR6ZT!HawWEpPk3yVQM6vx5#+M2YZcMbGP9z zli*8=N4L`2;eMzDx_sS4m4@mO|36G?D=t_ihgvg(G^BjX%jn5t8$M=3?QDn8Pm=W^ zRDm#3FQF8`3@D0ok$OERj|3*CKgc{gF8mpwrCO~g zLolxW+q1oC$&%cL$EgNWoeI>OQ&bxpO`LZd~)z-P4jerlLi z=7stxyeeM*M_&{+LQ!Qm?>NIRPH*s?3IX@yi?p12RcvOrF64OIN(g05!ltHBPtU%*EL0+YKfQpGiU&Xv{Wl$`oB{D4_{2Pk{75B%+1HUu-VR z)><~>3Xhw+$a3vG#}9V++n z${r$i=P>j~daKlBm$O5Ug)?r*3Oy2tQHfbGfnB1Hz#E`=C{A@jYn>!atMeu-H&jCX zm@%SS4Y+X`qj4E8Ki|YpM~2>h>@L2A>=PFtB9G9gAq3krY}k5#pEUwfw?#HKne03n zS@8SXEudi(E+1TLA<@wuYSOIVKjfQX#&jwDlg<)s@IXix5_uLi6f*al^ozKg1QVvZ zC6HPst2NMq(QM5v);tz(5kVh`L&`q?b#y>icFl+xl)ZA$`@$XsZ^7g0p^!4o72 zbIV7+P$hy^lb>eVPEbYesX!Wb_nPOQVQ^Oxzomxp1+#pO|MlsV#rCZwfY$wpJZ`81 z+P}r^oRS#S6KBlBodk`Eh0-Z=;`^NA*q@04Vo&f-7Ls-wc6rE;x?7{BsW$MCci7{0 z=Wkv4*eCLwVeutll1i~kv!ekzRR2m#IoEbGiWBGJHI7s;i87xzyzg8pD~nZ zysm;e8TT4%!GGh{P=W{mW5dM_pr zgI;(T#-7T>s*Hz?1bfW2c^x)AX_I@$Qf-}vYyfl5g>;7;#0;RMisl5DNG=h)_Y_q& zkQ6_)trX(zxXyiV!G1?*#Qdw5?6HKqDevwoXkQs~dGe~Le^*)HunE+$=b=3MjQ=pO z{lsx^zkiF3e6pt9+ZX-?T|el;t|t~b6PVogB1QaZYHdZ?OSQwsZ8$feRB2M~(}&^p z%#S;H>5D&cNZ!c6wV+*+X+A14_(Jg`WqaZ`(DE!7k?~L^kA|Z0b>Z_Z#csY$R3@afd|xeJI^jBEhWuIL z;JaVJqw9lD4!E?nmr+d+-!x$K31$ab?n5r+D!FjGdd&sJzO?l#%7(v5`-Dstx^3~= z;_HDW@?^HWGNvT|;DG>n9>l-ov!LYux6*cOjG&6QTt50H}c2L zZ9J!#CgHo4YU<*gE}dtR%3b_=_BMB(=v(C{uOwUT9M*I3v6Nf%xgs7~3*ZXL!iGd) zPu^RsbF)iayUOqUjo8PG)61=ppZnb484!nUXKR5kRvy3+15_h)fe0voX zGScJtZ7wpMVn{h@RQl$zJ_*VdC@Qn79q%ODl-?xfe@!cM)*Zj6vC>HHp+80+o5&DPW%nj;UN<<3e z2RmrK$8=K4bLHXxo(JTD^niByw z+bbzf<)Hx0ny!nQoPs}%2(uU@+u~fLha!4u^nrV+f99uPH^c7h>HHV;cNtzBfqWSy z6_y2rRyk+4N3c|h+_d9jWPm6i@5RO;SkOaWJ9*yyM6RU3mF&PjXz z8bH||SDDdnpt#^aw6ZC0O;iJ`@iXh+^rcBo$6zCHE~71d)e`uojQt_F9VR{^DVd@P z*rpKQ73}a@iNj6$(edgBcvYJzJpOBorPB_+Nara+(?dJo=(_qy#Wy3Y32c2T%$O5n z#njDgChYD0!mRmy3SRMJz*henuuc_G`p{jr#Gsw~ zEi$Q}ZnG>_aj`HLfW`rE&>cLW4Vm@_Krz~(V5t-{lu-AkD$J_B(S)e5KTtnF2<}t%I zLq&G@ddRGG^A*7eLq}R0`s)Bp4L+B?#!Wj_fEEJNuTq%#g9PQ4^)O_l*_=ryI4Xu@9V z{u^XTld1GoX_H!=bffts;7^OM$Uk;{@WiwqDKlMTz4Vy&rH9*J;4phEY08#A} z%lX;A!rE@>T^>2synP9L*J=55Em!}D>tsn>L}K-%!=nd}8Yl50Qs(mmiz5}M6`Nny-S(>E zps#cRzHn2=~`8%y{dutCQ=~$o7F>4zesK;QiruA+fSy&Vd)%edDNJBl z>N`*% zo6F+OQ1&guGrS@u#QG3JMc-_m6wBW`w<_zSyvluKTY$X%3RU0+DTD?RF84IX`N&1uTihi?(q3GH(CM2YvoupD>>^AtgMRctQafX zTeKO3$#k_mr>0TUM^$>vYjZ6O+xS3Q<}lZT-#K7}Y63`|jgW2>S(dq%`#GKS<5aZr z+E`+zdJ8do=|hogfS~Bf*TfLDr$^Rr^S7O28$PlazPkQKDIp&9I-&){!pmTAlsf0^C-X{$XH+ ztQI1fg3%uE1^19>*)>$cZ0_1XueZUJZpW*|U#|U{ZI~4F`75776xuEx#x7RU#OxJ2 z&Rd}~!!3XUo>1gpgrLPoR}*(t1qjObolHkWB=w$fb__%@}OptWP#7xeZ>V`dU< zuly~7r6kJ3#Nh?vD~F(TZ01Um&3kX3)qE$>ue?(+%D8{qru+Z^@!-oB$!%Ie*osGTd zs9x#JT4)P3A%Fz#@iJW41f-h4W(2g+seo18=XSacu@rqFFs5>T(M9Ryz;8>vLr5!K zIc2lRQo*}_A7s7Z1!vOy-^`tQ&aUH$&{T4*FfG2gc<&m+R+I(>klX88a!3={v+rJh zx8ONiha^1{fuveJR2hBw)>=XC?6MQQ4K3~~zHZ=e_kJ{p9=x7%2PPS+onA)i)GMw`%pZxfQwpDs&| zC913qqMm2+nRx~tFG>@)!Fbt)A3DkpaVVVq`V~3ul6u~%wIzwPwS3{slf19@nKEe; zre-(?-E_3#v_JXO*^aUA=P__r`WZ4eou!P}4~iHa@_Q12Yw%}@&?Xr= z>)sCXIMK<%A@KM%i-N&(AQX=$Saw5dZXvr;9jvC;$%7%d0e#&f9$oMLS1PY(hC}$1 z=uUC3v$C@p?%~B4_%qJaxTfVM?{BJR-pF9Ve%Cx(o5eJ3eO;`ac_O#bdhJ%Qc-mV- zH4ylOMzawewr44P(GarsdU@wq)n<^T-$i!IayHjwEo?o{^XplNi1S`RjetX` zEO9STazPSIf_O+^H5>ywNh{i|mybTwFfG!vOucI%ueL1a?Jpi>DL8#3-)QKz2n#)K zt6}NYyW%?}2cisN;2d$FI6I-3S!$}UwB&2wdTH7>W0E|$-eu4XPk16jd#11Vs3h`4 zq>HWj?k>~!rJI{K-8C_gpzz#bXpYLI=pQ|P>yVNaf+hS=71S%Bw5+xj8dm@Uj;QYid_Ge5=pMN(KV z6~~NJu?7{>ZCAeBvZF91cF@Ovc~UOkDheJ)hrHWqCKXSEu|_L0EB+YkhmxiCZWw{+ z{%``|MQ$;1WDc?Os6!;eA$&$UDe=$-qk**lcF@?M_!RIom z^v?fw#^XOU3kD;ayeY%kaJ>uqcMdb=FaMJv~73 z)SMX8S*Y~h;8n+Ht61RcQ}UdtwB>EtD%Y?DdwLK|@_qzxC7!qfoX0!Gf3JZ@lGLzH6l- zUmmsA`QD6t3?CUwW6V(8<>9xK&$nZ~edNx)i$BieGo3$I1@E;^{*pCW2s?D1hWLWui3+-22stVz z(Jl71^;&Z@Q&ne2rC;Mj`wghS+S0P@VyG~I?lW^t;BY!ZV1wbX6BPpHry237nShtY?I~u8Vvo*mqF12^z!)#kyeoE#@kDS<75Bq!JXQl^_cBYr! zykZ-4hv%jEK&jSxzW*u#mOwcCpaRE95=gCe64nk44fo*byI#79CNO6P%px7~|z3vMf&AC255|hyDe@JhL@XGW5(omgMbZKXfBQDQWJBTSw zr+P^+Gsb*livHy1kl|@+DES5<7jsgC0P6TY2IsF6v&j>CeqLst5#e)~0HB=k^;l@z`yYxY2w zhj}#(pGonIzC=*%Pm4V%-$*zlj}u@s7q)}x@e}7YwYY;We{KooeLYBu@e~4F(Z|lPxvzx&~MFmEE<7m?p1b4w#cnrHoS2Dza0o z>|*Kh%#beu7D!7QXg(lS&*FoKs84VKss!aUeNfaS{ykBz|HN=~lWr&Ib}y-3eRB6v zetWHOh7IDbsU5Yd*QOmvc>^a!Kl+$t{l?S3yy}G+PDba+PU))g$S};Zn8oQ-)m4Uu zhM_BbVzJjtJ(kA z%FTuNEIN#O8^v9%JkpFZsdtXHDEdi7iE1Rid=v{V|jn6DgW12MW8}FI; zt9XJIdmD&y38;oj6z4p2!K!?&LUXj@O0|Zf)KKolh_mhk10oWNh%Yc5syq?WTOG0L z7FT5!x;DnKu&}iC@U9tm2QzY0AKTzEob1`&&-K7#Y zvCQ5{_6xlo%P(&~?w2UIH$EhFXnCQJ{?Nhd{ZB}PF8H!NJGCP9NZ0DdgkrD;#s_4iVb&CwpHz`YiIy4hZ!3Cxz>2Wf!dFFc}L1Agvj zXf^oyD5?Xl`iunJdgi`nWOYdW=1HBo+6%|j+soAjjvF7<;aFB)MPaS!Y&Mi&_#L2b zl5(TlP=M_385QbVEXmuepNn0Yzt*4K{_&^yRovsg?vfzckIrxB^|ppK%yuLMh^pHH z6vsfx26R{97$14%@<}lwJZ?W3%@vtuX{s#IS?1X^@@r6L+xvlz3_fVErEy0C9kPpE zxkbx>o!G>d^^Bf~%i-(}lL}OW*SE4)kd*4BTYOtN3OtY)rErkfLh!tPGOxFXLs-j_L!3d>TF05?$l%wO zmEqjtNg|H-rG9b3m7xSX17A8@;Hv6eo68pfLL*;}w!*;$T^$z&!*J6@VyU7#ig`Pp zysSKe=3YD+QC+7^CAH2;k_t($0fG{ri9<3)Ds}(*I0!e|4ny&$_jP-!MvA%8u! z^QXq`9eWVSJ%PSJA43O(ENJ6|@Rhi+xna^cvgf3VN?Dwco%xAWtb;0*5?esX0cK zeyIA))?d@W89P4|m8tJlVtVtY{ZaEz(jxPTZW|_YUL@Y40%WU(!`IJXLu9fdV9*5X z3-BO`8$FX}T7vZaG9n)c@9GjV2L*A@22%49S-T#jAhVjgfb5MVus{_+k!;Bigzz>L z)6He}S*k>cY<(+8iuUY_3@ooUw0K|g$WAni%E<;*KbfoG_Op6s!5308@bZf+dSjMuxnY{aO*uA6A#D# z-U0a^osj>G{~q~g>fZt>#ZlP#f5{Sb?f#V|h(?kvGy-X=BZFdw^Nc-h(&?%51E?!+ zo_?Z-!#zM`0Ph-`qmY8?0Rq&Y266ZTI~%R1Evid~aT*5ZH#qRtxgp;b`1s)0Af2BKWoDNSG4L zY4M`S2BRkJLPuOmlYhAdN09!-3UfQ5>kl74d>pV8{8QQWCd&4LULARwFg*rvMLqDN z)p$esNJfk$@sU-%CFo)wum1z+-CM-+)N$7f2#p+@^vn*se;6tkwUB6-E?KMx$^~%g zAV^{(8+SXvj!jPXeOc^(4PES58jWqs(Mw2j@{%1+c#$EN{dtk&Hr#l$bTbXs`gY-R zaC&%EFLNG0i8E!LSh@GjZ5OpH%XkQ*RkvFcM1RxSMAmoN0LNS3c+KiOZoeT1sT#Yt z;L~ZvyTf-vo8#snN7~sJtIhpm9Znx>Vprf@Vq@KM0Pd)7Nnr1BfbkMs{IPs~e;yF> zt|_zeW+Lm)4=1P7Ulh*0s^ywP{a8M@Ho4F5xX&W}%H~OVx-mmfu?X|AaLwfP%f$Od zT+?sKFjCFWQE#0|=Yxp(obBRYR~L?iC|NYYzFb>e^+|9herwA4!wr~oLvqQ6tkD-6 zX!V-hWSP{|LNmg8uN$P1{8WO~PLwBhqov_w`f(%PlaQDbTQ10-F+8V@#BT$RJ>z2r z)aVLuE72vRS5;GsnZg5&-eYU}*NP~ZS^%+kgNilm7EU#@1-mHyRgR3DlNHV7d&jwLq1Po{oO zd`ESijMfXpRU1{8W?;A0SgP^P20yC!58Glrf4vs*Xc5^@n_r;c(EF>6KG%R-p`B?& z&CTCN#B@GX+`c{B?8~9c^CM=v%lVfW zNoUl~p1l-3^!TxG;~EZL+gc11QwuUp|D@L6$aA^xbD7Jg!_CbT{TsdxJV7`$q4A=siJDHqw`*rq-*ps`gr{xwx;D;h1S$}OiWSV2N*#$n zplan<B0-5EYwP5hoUK51{KN3cjivudWlVc9Mybt2wE}^;<;d1Ve@ZttQsBKs2|=i;iEtd8b?bc;@;r zXQsM*pc=0iixYNfrE#fj{&*HixIQKsd4tZDzb47+kL0edsy~e8ez&0c%P;qCUTm{R zqq$eq zrtLjl#^7Is_rgsNTEmg$Z~DuP#J5r;E++{emtYytk*=xC(p}zFZjJs+$BmODSdK9u zlaMtlcveD3sTL^1XGH$){R1svwME{kZ%PO^lNPH>hi1W<+CiP|*ekLeq3`Ekqa?|0 z-HrH8)yONB$wAjXSyu48S8o$8ZP2QXDZKN{?&zqf+F@4KsWg(Woyz?Vb>OGkHthE< zhcR#I%lT9LwxD!<303)f8?y@6Li2z~&p>Y-*cONgk;2H`M(pcB^ZNeMjqpnE2~dcC$5yL8JWCa?R?J`oijLk2PQ9LjRxkFD@pl1Ht=w zs&8M-vGQ6tVR=2p(9;@)c}G4CtCCplPEYI&sCrGkZcJ417LKxK2xqLh<3+!(WPFQG zxIYHsXzb)w5E+TkO6kYw^DBf@sA>m}~re>K<0P##FFsL+SI00sGQpyT3>| zi$BCR0EvgRLAIfIz^0IOZb*tA`)Ejakt8|wO$QwCXYPyha1Qeon}t||U>Uxxk@ zKL-9T{q;Yg_bsmfPj3k^OFc{W2lX|HrxK3~fNn4IM{zcritIw?;^uJQA>9LfI$@;; zq{C+tDg18!1#RTQTjO76UUyjDzW+5IqJfW!NMJtEG;-qIT4h`4r$5-j21{nCU`B?6 zuCkjdOYA`A=mpbXY}$X~m*hXw{}w0x8>=vh;lKX>zaE2sX8vo;{-g8b|84{DzaHEF z=ym@y|Ng(1R{PKVod0h=+duz~9j*8oc5W-;k7_rB|0gi94|(8U0=42OzdI;%w@s08 z4sk@_$MiS21pMnWazFjk!vuO}2MhOaPvYU5YmK2+o9@j-hFuXW=?gesbmHr)+{ifj{p2BNWWTQpMMjz-SFx_b? zUs&}|-Iv;R{zAu>KAcWs$rl*fXLC&7M}3W;vjW28u@)dE>Ps<%eaSh=0Q5(q4L^=+ zrm=sZ)hz23j)!`zzCv%FO_B~vt$pK-5E|pAKmpGOCIy>@eB|y$#aa!3QAZ0i!O zVCB|(-o;H-Hj`svJ3Z{lrYWB&kCkFshZ8g#vIjd8{-{v_92$`U8wCu_gl$HO4iL9* zp`US%gx?}36DXIaGANEwqCwa2_ZKdN$SOTsK*n^R&rHchp<#;jDJTPA%y0trnqCzD z{13NdC$O#(J{xzNexc@hmpbj06Gbnx{{p&6n)Db+!d+kLTkU^YH+l70(=;O=qKmD` zD#vQHsbY7X98dgVjtzhD5M&?z8);Qu^jy_4zB3hB zdTM8tdz)YWGAvo}qh`nSuvwGp6(ISmoHK!z2Ey@c84e-@MLNqaMLimHU7tkgTY`)7 z5~Hy%|6$+@s@lKhaHXB%iu9>%7~lQcNRet9%NaZuZ|ZZ~$@%JgVX00#AxN8KDp0Ig znG+&K5QGxwa?$SO23&`nG10rcb6kaKZSmEWsr7@I@viR{8pd~r-(-t?P5Rbp0GpBI zqAP;Aq6Jf;Pa~UVc-KXJ=Ft1QrH+xDC_H{xB7O1Jrt?Uc?(@g3T_-%Jo}NVH zDzGDq(S#5{L|cBuJzuxB(hKS${e6cghp)}vZ<9E)+bcAxR9Mj$>G~ z=F=c*Yn|jltA%@`7xcQd4RvTa)UeXX*=x1w9<4!!K@$sl2z%c+#4yioR-4l*tE?gd zbT|m0gSe@@P!bq_M6walY)uINpv;w=Ye{&r`3F|`);K2AlXnbO+n%d#P?dhg=_bG9 zl#X%Eq8R)0C!oiSQ?{nFcKpM@d~Su_4`pe1R5-@69Dp5*50=huGU&@A*{(QOE!?eE zZcpUez^dDP)Z1>z7pR{wTF*0 z=fB`V;IuYUHj?IPdGMf>c{$RT@!V!KCA+>t2# za=feWZXmTUT9#rSsZDAEPxY2+L@NOz57zN8Rq}qR#-Eg&Wnsczrc%zZ#8AICf6+A= zOGQUZsZ=Lzi@MM|%^_6~5Q7U+0XGqWjaDb)DQwaFU13q0KQ$M6_&IPJzPH&fA$XK_MzAN)-ql2^|3e=@JBz(3F~ph7Bp+#c$4;IsbF#&fGS4?uYZi8D~4& zz4Pw1-u0H}eV&Ebo9SsCdwPT@aFC%u3WW^*t+~MtQzYnZaicXtLvB#n87n@Xo(h#V zFSCCRM{uKSb}6@qUjgVeGNW~k>v7&UJbsv zA7&94{{T?8bISLWF>&#jdu3K*aaO@gk{Nj z{;7AKis^iidYSY>TMK<9>*nQY4FGQP+&-KL|~YLn0~La(o&|gV7wlA<(N^c9Q`p5Px6!w7=*1=W{4?L z0DB;v`F2%$f~6cPUikE$o{@jxeOb+(Qf7={>XoR|Gagb1l?5X|wZOec7xA|Mf_fbB zJ)MRe!rX-&Wc|o-kIqz%&uIkOxHFBk#<#;Zikq*Kzo>~h{cb<{qvF8!!}(XAQyQ@E z@f0LH2`&sqs~~B7XL2HZALC*;2_xtU`;NRj`e&O*>uf~6O+}R1Gi@<)>NhYd++pMoWKy686)wCwS%5PP*g`OaC_B z6n4ly8qAJ~b+Ow1%J7?j6Ej37RyD|cds!X-*w|RoK%KzW;H!B*710s!qBAYwd5DN6 zO?A+v0(WTsj8Jl>4Y!CmtDn^l=Rk|2$FUUemD?)k!FPOt34C#aAh< z1^uau`(|*4<8~VPmQ;$myMJ#E>Vpy*c&Ap-HX=6hqi1VGv)U0;d>v@fp=LQR(J9+d z@5{rb@2kUCL?XyooIOC7*U-x z`rdTXsRyaz^Ty_s%KEK0G#`b70|po=utq(WfJT-Y>KcS0W2ZQ}(IO6)$;yxPENMBy z<+S4G-o~d!zUjnDW!y=>pK~!=Dsc0D=urk6GZ*fOU_+1pW2-HH2KPolaw2Wmqv&s* z#R^-gKlP7i`qWhRok<9O(H_TM+9cV@s zU1yJbur#YGTut$-Q;#<7)RsJQgr^kjK0VluZT#f_%8HYI1K7k6?^OQ1@y25VW+6+~ z1ASz)?CmCbWZhkLR^6xo>-NM%v+d%ZcpvG*yat$kfCKv%!;fOc;*KNwC15JBts5}Z zS;(BcW;=Vic__Hgw0?M|-P|CCtX+1D_oo!rr^C=nhU>JOZlHZRLr8?k(FyxL$KsZs zOD5oH%aHa*h7)934#m)IsyAixCa&X0d^0kKr6PQ#k9>}LG4dtd)9D~jvt5krsr-6Y z46;gOZHx|MO%i<83+YM#CWrD+Zz&C9k8Yt`RKlk{eSRnm6(1MMW&^K#v3$XujAV%a zD)#^%#Ig?)Im1phdRz{7%Nz^(AVa-aRTm!Vu93*^2+xz*r!uWx@p%5e$kB5>lyc}q zGr)lFcYxBt(Q>8=s-nxRzz*vlfp*sA7e-JV#e+Ud(Gh+c#fqH;T9OF8pLJT?;=^5} zp{u?J!*zmk#qmo!mWeA3Vqh30OB#Y?nn9-%S}f>o_j!JG7*!wDNcVZhjcg z`qIpW5`o<5x~S9N&<9asBL@;lemsPCnb~(M`_4t}9~j*{6_Q$!SGF3l!uW$H&dtVf zQX5&^LqvGgZ=~jnX5~+m%y>VM%v#at`INByX{~j}>C66AbO%Z;LkMVfb0*Ir6a5+c zDEO{f?wQc{-l?vKDyPZE%1rNBzpcNcIG_4*!NSDk?gh3q`5CwuY$^nB=f?k;hFLQ| zHu)7oXQq&u9zom6TV%5FUR-?Rk?kAH*Zs%wv3$_EIo82%UQu%A)K*Q9xr%XtxIAFJPl_iv}+=<{g$gn$OUM0;?m5iz~)m ze4jD;j=VsTLXOpsrFi|Y#Bc?3{&`s*ixg5uoemGVx2SE7sH@Ig4zdzY*7mt#7NsWk zY2D){Ho?yS^`adWf93kE z%To8l%6G?*P9-zMvgyL;?=Xt-d2TX3p>r5aR>T#~EbD~CMNhXrHF;sCU|GR^Zoyh6 z`ihr3|LN2hbympd%7y6fV0;{z2n46`kY@MX;OY<+(M@B)E6m(4jp97-)MS3Qw)?SS zdn0Yq(nNY&Z_M+8J=?JMhCxQ#($8APVFB7OH6#f`Ow&SEYk`vY-iQkxIpdW(S+S^# z!F8>!Ue1K$hD%;cT=~-Jk2oc*R-UeYrawe~!PLAWG{GtdpWQ=wGaprGzTJ6v&AE1S zE6izn&Vq+-=d~n#`do_!>spTA+DEEYH>NTx5<`5sMPLB2IVW$jO|Z}1RZeNUgoN!B z?gU(-NT@=JpYaWe{w&=Wh640(VD>@?25dty@`EH*Q84CXznQMtPC`s}fkS9sPxq9@ ziK2Y75Y$MQv0^aK@mrOzq?Pzp_r4d4J%#Rt23gz=!0EgPLRVbK=$X^dFH&r9LRb>6 zPjJKEPq65I@0N=Y8{FD7^dbOJ_zk4GzTVLB zmDSA3?KH)F8$)c}>4Th$ar_9?3#2%QB77B|atZS&PmcK(P6Q|eoYks1Jru}BX0@;m zc_RcNuRdF_fUX^xvA3IP%DWsBS!1Aq3wSYBgsT6RoKtz8QnCCdAwez?XvyvqZ zy(GD>G08Aa$jTPq^zF~={>fkC=>(IHvtFu$f}fIp|2W`7p0;}4ir${v;b(#~nD`F+ z$sH&^bd&rD-sH?^lG$DBCv#>w&gUs{%`4S7Zt316hQgP9cjnG%FfIZ&0X%pGX0@M= zKsiDgRJ*yt7y-u#mXITau%(nvIoP)Z(McMsGakk=Nixg@oMMe@@U}{#05%=t2a7_2 zzQbKUNPYv*2?I8Dd5Y<62oD&AshXulwoMaSmN~v}oIhM$>>1t>3~eWLe8Ick-TVAV ziir0Oa+&;3O#ai=9=bQH2VdhB7eeNn4D8;~O1d)=UrO+t*i%?Do7)_8sK&be&YIFA zOCN#q0Qk3oF`##qJPMxC={RVC{t&ha|tXPtB{_pcoS$Tw+ z1cqoovD2U zsO7ixc^W>CGQtWD*}@MilBI=v?>417+r8*h>C8UHs}IdR!#o1C1lX2xf+;POnY4x> z*1{@fMz?IiXP%AKYEX_>N85^F`CcmO-&!s1c^dq6$bt82_sOe`dTC>H3s1T|Lz-C+ z*3g3TB%GED$p#eHwxj`Hc`0!gz0Mix#n zf+rw_^jyIk?=k}OkwL}8tD!9Jtyf4-q32ds6~f!2xJW0G=lX)w9`udFyyt$|YpwcF zlgaB7_KEN2M#(Hxo0c;Z_)aFFr;*p35#?uj`H1zv;fK6q`}9#0oy_5owSwc|(CON>jklOq4ZO>mIjxAw z7qdilD9es;bs+?Q?j?L#q}@F1BqjaGXTW%>KSCGC`QEJr9>N{J7yV7tC*fBnO9v2e zlaH1x_)AT_FZ7Yh`Fge`llHAd^@HWJ4(Vb3Xtz7PKTVIxEpTIJr-c~1*OQ6x6IQl74l*W6)-lhB4 z+*4D@a)U;|WYT~1Ip(PFM`(Sp7(tKpp0X$-V9aSWP5BrE?;LL^5%qoNOHEg7E1g%l zRCIOwj&K@TGgU%7`l0sTrJXxN$aWAq!EBgic#lCyc^7y*`4}Lw>Ps2w31q%U#3R#$ zY5Rzr;hDt|^R#7>k8`HFXJ--qd28zUjl@bRIe(@ds|8cZzO<@EKldHQHmc`eg1VA7 z{Ck~d=4XF*B35 zatF_k5j{87WDrK0oUi>nDn2!3sbG1!vzFkBv(LSyHez!P)9hs8%4k!RzYM0|wCKZp2#h)7evmC`h0Ta;4KPK(VC zbGd)5;B_L&2A_{SvQkRI0&4Lb+KJO6YcdUK>Ut`1+!_0yK`qi&+dsjNgTsk0XpUeb zL{Y$zpY^m8FaE2K(%b*nmCFA9wSeVVfAtKO{_+t7FnpGL+QVMZ^Ry}G*R zxsK6MwuF?%``4|sLFvF73I*wTn1*tkD1vb~WUlicTN@_UR=F+7UoZGWn7$H~7hd(} zxsR?1|G+}*4YF#Vfaawyw@DQ^mKG?O;`o5*6P69~o8wr`rw2eAWbE1@@8U~4e;y2b znZ4FiDEobM@$(gvA@MV*QrD{F#XIsP#T%jr|Hv|%fNk?MO1D>Rt~OiP~jc6v+>PR zP!ww#ek1<;RnX+NfH&XHCD{P3YrkcI-KB(vqSVATJ9q z!cy+T*0QP90ImXJ6k%$262lwNso$m6K2@kr0;j!yP(xiqlNxRamnyhvdlWrLk5MWr zb3M}NTBMcxoXs5+M+4Qq0iO+bTj@D*6c~9@MME}cHp1@Xrnc~<2 zU+KNpFI?_l7<|YO2g^zarSYv<5^5)xx8uRK9rz5jQ|thSynclAO~a-vt8+(iA~n`> z=fs25&-HPM@yE|)%F1vn{k3J3fV+WahF&Sd2`v5?XqyyBz1(FR3h~YPKgbf!YSV5q zVP=;MG-?DU&z7(Z-Nb#BSh#@K^-K!m4ipVWH=i_vTrt>dVkp$vxOpsZ($4u>&5fqc zeTOe)az486pO-;ZX7Km1xMWGdOPvO2_(mZ9r9mp^{t;^U0~|;fKh@XU)lPdHx%BQWGd$;l>Dmneludx2&x-^>2OhYGKR#a(CU} zjePa@dkZ37egup12#_g5Rt94uf!sPMRpGj0^<1f%gmum)tNhS%i&q|2@A93MgpHmf z4&O7X4069SZdDVr=L8$OcU-FIHB@mX!9Sk9$vXIvgaLN023&ODvyoXp@chcv67U;38Qn2W zfM_c4kO;tCKS?!}9*SRLK`rba z?%|Cl9XhpTH(C(KQz`Qv%RQ)ehSw%a>TrHqX!&L!cH6u`rxLnpeiIN&88)7rpL zMmfL92X#jtO84Wnzn&%PUxd#N^(UNjiS2s#B;0~rmzp0Evi*GG=hM|z`W2KjxW5uu zn+F6oSWvmf!8#(`HXNCGYvq5#5dxm(dLoV|MtpFMlZvH(g`bC|cd8kj`RZ`X#CdYj z?ZRreq&=$x@60!)MZ~PFQBL*I5}A3vzUvG%OmLtlPgc#E_vy?bM!lLC{{^0N=U0{q zEJ-+V)^D`JhuK9Xq04mct6;6NhawbjuQJ zmf5stKW9P3zr|>GpiUx!pxY9H)_}I?{-`TJLi&1Ht#V4URQ;$FW&UOf@ww+~o)g<4 zu8zjeN%)Wj!DF&=AmK*D%Xh)u4XD3S?34i1E$S)kobr)F$G>@r(w;}@DqiN~ z!V@g>&eU(ltkMDY@bee`FNgI12PpvG|HuU+9|@JdSw^3N9LUH+KP$E;a$+*VeD;S& zt!G->+;)xKCmoq~L1Lx(?IU>VAfyR8oKNWinAY=p&eW$C%ugu3e{2V%@$vWmSmSC~ z!y|wB&aWL`?NxCNyl?K2p%2{%1l^&9Sx9-7A%>?e(H=A3onV&0iMcZZj~yDR`NlJf zL;SSC>%Xgo*b;sw&40VnHjgQ{@L_puo}F>dV39nE7DMVvFtRW(OAVbYToa*tVp^r) zZa|)t#a@G}f?6qBZ4V8e5EQ|Xx*J2cK%@1UU*&ZWChSYeh?HCJx(mbKAF`;j5Y?;6~&f?ep$l zNjjQh&gA!aV-lTACU3DgRe-DBo5ecRiepCQQ+Uw|-Q>9<&1h!vd#WdG>m`QJJVrzO zb*J46x1?P9qX&*g9fS=t1q07kFa|NLfOr6lte#!<0q5c;s1^{Z3_F4<*b~35JlToA zE7NKSdPK$&mVG_d)tftPysDqeH|LlD@3P^K6u~GA9Jt&BJjHkelCxTUNeAPHK+MYn z_H_2U=vJrS{?39>+X=dzSpz*{dD4sWVr$BwPaNLV5FK)DsC@7hQ3W=2NHCB+d9#KY zg&}6(DgJnnpS0yEKtpsz2;F{h;&0(&v(YB`a7+QRCf`%Esy*x&{dVTmT5f?uxk3O4 z>?BhiM$t)_RvtJp4p#FN@W&!8fK{?UU%$;7>S)dnF7zCpz8H@2FY;=gt81?Jc1t@r zYM@Xo?$udqk|uR!tyZ6O{lCud&l)DVuk5_)99=yOs8@#b*rd`E8kaWJljAqfGg;8Jsm@F%$EvaON zx%=m)Nn;|*2Tvpp0y&>J@No){^MZC%7j|-aof{e7e6TiLKHRiNSj@-ndK%*F6s`lk z>|tNk(h)nbWz|Le1^H2vX~JMNWy6Ud9K|U{s~L1xPt9g=idKxWWafr-bNz^gbf)xg z%k`Dj5dQu!b$_+hBa)VGGM}!KSW)Wcw}g_HtXNzi9@zhZS_a0k_p1PNBEQ=Ku4LQy zF+oo%YU}E&D|7ymNBP%(ljiHv4hqdb$aE9ymgP>5L?SS)2k@?b$;gVQK-#huKq~+Q zME*61YPHB~O8?lba+cfIEVuG(9US`;~|+_eKMukN^{ z@NG7NaB063vtAznSM~-Cejn+y)u@ebR)9>&>=TJ4L3aI@#y$LAd$`vAHtWZY*mZ4) zuTnEn2QHM{P;p}7hc0DAbyLI&(H$5{fNU3#Ph)m71nfFsyeKWeyzeM7($>Mhgh;TS zFe@RG#bBDcTaSRmRMXc?Qd`>ZFD6z@5Y0pxVpZlnfm6cWtMM>Bpq6pkE-lyLuMyFaopE(*m&lAsj_2K=zJgmctad?Eb8$g2CtW^!} zeN>+VT4@Z21Mq_wk;Rc({kE@t&8!k$ZHT#(Z)b0#Ag{8B8g zDPV{qW+TI#ZjEV$-7C>^CGm-9j5CG9N;67JmF71I1B--ZJAdi>y&|i>r+OBn*(B=k z&;D50-OvjnEY6Q~P#$Ov*H=JKaneDn@M`mk<>vhq*XQr&Au+_bP^ktdzp~Q7%--HG zAD^}7qy?oePn)y7A=wruT3d%rjDd*~{ zV!276yMAtX@LtuHZr7%o0hjOjG6B7q3NE?DeGsT@>;g27;E8Yab->?qrZBB$c;f2@ z!>5!UT>p)gzSkb_CnDBSw)zz5>xAcP7KLPg8yDd27 zHXW2JKo(ML{J2wOi{Dc;D>KV7<;O2VUaFta>)AuAsom=Q2K(LT6-?YMZ?L(s2y*$^ zGbHCl%=alfWOS+bAFGyPCDyV^^( zH#p|jC7{@e@x6WPddmNntAWg;|jC7U>>8a<4YR5nL9fOC8C79F*pE?-XMhlKX7X1qE;M>!-#{X z<8{5Jueg(jIkR^B+P(Pjr+b5Ck8H487u77VUda`HZR_5i25k5sLL|;(lQ9*%@8~dw zcb?C#mZgHEz62$4YeybYVsB?A+Ulls=hYGRhV^XQ^|?NzqNRp&t@}#h>Tao88!Dx0 zu@1VH09AUtkD^91EOgjUU^euzQLd7rhLlriqkf;Rn2lT9od2TH8P0v%*IR_{9>UnF z-`stJxlm;qsIoqO-MkOop94Of6e%y#Ppe(gewx5+n7uh{D^$^Z*EhemAha(pn-Xuj*1JC8=uU)kDG^BLAx(N-A5T@9U6vesc_c8RIL`}L`Uuliu)P|Dfx)k zj1=V$)}u1dv|QT>QEBgB+oxctTldDg)e{r+OF&b|SbvMYo3g6IiEptuB!j=a3zR{x z0?FTy$BPt)%w&nrOYMWgH6PzEcMV(1Y@WpB$eY$L{3iNWo3M#_9+&)bOAmlT{_`Rf zWr`Io0$AcB{?10EPS)x%Iy@AC7(<;b4dG2Kvzhfw`}{m4I?wV`eO*Isnvkc&JQ86aV{JH~ItxuC?lYnooPJ)$6^yK`^3$|Kf29-Z^;+*?|$qI4g^I z!6eY)D*`jkxo@Dywj!3953HU^+u0@Q?5zI?OMy*yp05&iu}_O6=&*W!=D$$D2x2KF zeh}EtYk)OcFqw7SkB;_i&}uJkJalQxx|XsSJN~Y$y!>_MajA+h#kJBo{w&Ea!(ZBY z^xeEz<3Hb~hy17b{eKz59=6mu&3&7SXiFA=+%G2#d>##YIb|2R#XhU>t>NTC>^_AH zJ(9eCe+J#)*aS8D13%G#j~lUSN8b~=1gg4 zXKAL}U$Qiex$QAsdncey65Cfk`IL2t#Vo)(;UT4^C+PDi0SYvRac6>!cw=;}I%ZVS z(nnQng}W|uz%I&SpBt#;Xf&fXq!r&@6hS+_e zWcoc!s|rIAoF*_(E81r!v#K*TuVPDhIVqJF+!$rrce>W#ej?R$AAWscWK-qx5Aea2 zKr07Ex%H3j5tyni2Z91X*fHp(nTftMBzvm{aroC(=G|es?Py_Ty?3Z$W?-P;O~Fh# zf=B+XioFlk_u|XX$Sv+ptS}DKfeI~{ZvdP}CjZ!ykX1%&LL@%O2<1awv+EGixt_~> z;~GMXEysnMT8j--*X0P#s$YK*CYb2*s6YV55CDu;;C{v)uo5{L?#u+VA_S+)lHC*3 z%SER=ZMA68i6AY^vKle_{FeDvEKV`KKWm|{_szqap?=H_!JA*zpjH5`(xzkZ46!iS z>fv3=W)WAE6|*2n8AwyJ&SP-b=&UhXc_pS;w`O~)(OJ*xCF(LW6GNOw^0QiVI2inn z$fq+xjTDEjr1zdxp*#yMG_NVuZ zNxKO0?9iWC(9@j~sqJjcb@9@uw;DKlhnYF%x6_xOkb_v`I;8rT3C{(Vd_OeejTC# zo@fQivqE*twxDMROB19_H5!_38SyAxx|-Ilcd_DRqg(=Q_mhPKE=mm0v^Wg{>8M?l zjk$V~T1!r0G;VPih%`v3dXzo!pqTlVtO-u+k&2VeLF8FxETz;)U9o_V83=9zJJvFz z;dc;(5#lUEB%PMT#DO(vPS+xpKl@|%(u~`h9`S0{JI6Fv#{gN?sr#clBkFSY-HX5f z_8eoy=%4r%va|F^qhH`^a(_e9$oidVRgs`H!`^)9N2-Wh7s}e=%@SG1|8qh^Ct(ah z5c(3Mu)-K8toYCSkQ&*rmddGT{@&9{C-)V_>w)DsAs{x7JnS9oyeS=ZDRR+XSX#+ratFENg6&`^*NCE2tF6Nf+eryL{{9v*5l z7e#UvYo6DH+~~YNNOM*&--FA(;``>)(!!t_-4)$BOSu7uhoQa`LGQ9J!*4&A-7*zk)tyz59*bk}kp4OULa$vOea^c<&s1 z`^*XUrJTWU&f+wq-wh36afOkUq0`B0Fv?Urwu+UYpX=}W9ujF6Icy@BHhm#QScRbX zsasg+eTs;o=NBi@N1j-aMBl|gX9gJpn=Ecqs`A`yi}KTlRvmmGjgVm6%F0>fGMm)jqX%>Y1 zIy7h0M}5oiRl(;iyX?RgoNbVXc^R2qR4j-Ovi_l*eQF{x>&pIYctiYY*>CQ@m%#nI zc07VIhQz?%bD%!D9Lje>Q4-3cj*l;!T_T^-(j?sLJ7Rit#$vnh_x#r?RciJzs|V9- z+sTv!+Tqc=z=zVj@L$#p$Bv+XJLXQ^*f z>2++oc!_D*bUwY~Emln^d2XhgbxxH1M#xwHFg0-ISRbzpWJ()!_PzZ}RjajA8bcn8nx-z1^@#vvpWH1Lv z-r&O;)(768Q{E)jo7zpzT>5FcebX?t>AM2;186GUy$wtc&%_YgS)97mbXqd70}Z9b z%-5Gh>C{kbTO?p@@b5c+SrsA9fdW4aTt7-*@-}%da_duF=5%cd&%TsxF%xkRB?3o3 zL`Pv-72tlD@ zAYH4u(PnTogY9>M(x2cTZFo<(Gxj?e4&Otbg=Dol`J&*In=lcNE#zT+d5@jubRKZv zIeGfMwO!V{W!o)YUO3f~#b0bZutsmm=PLP;P7gESy{3SXo~0P^O=1A(gMNx}0Ez_b z;V*S78i$S95z5RcMMBDC1rcGL;>%?2>^GXrw5~A5l-1Wt0h!mqFnKoPeWVS7AMFCL z;vd!|W}h;~1w(jDyGP+mU^x4u8qq4I{sp$nN2X+2o|}l-^$4809cJXhwZ?IX6IG1~ zxqY*LQ~~7)p1H})`iuQu3q%zC06j*sl_)Kr*neDJzHwf`IW3@HI!ckK^*nLmB+s5B zMf1EFGzY=kiT`g+vHVvGsQx=y7@n8ChQ0ON_4`+k|Z(1O={_i^I#K}Z}Ybl$J-BUkq*{JiIzHKVDVGw6#$%3>Icy>mM80`&PW-@zw{fpe*x0Tm>-5=xE^cw;BJ~ zzW4!#-tl*Ww&$Z(0hr*wULX3;Z~Di!x&7aMROwYYTsZq=*8hAxpCv2;obS@+KeiKi zfQ+zxe$#`-U8Vd#T{dDUD0BQ{8(d;t#;{IR%e@_16>&rUmy5~&)hu2&4f4M${V!wv zFJt|$mG%GSHY~Lboo%bFjJ?z3&>dN)iurlLqUd9ovpeQU3{55j;dTscUGcBWk^5Z& z4k?ChJe9#-85e6;(>3usdCVsvzzP6Nlgy5FcIi) zZnhF?lNqi-H9GoF-Syu@1oVFWG-Y=p4Mw7_gyz0OLOiz%(&`Zi6Nyg^T`=Ep>&UFMtN#xRRs$ylh9q zQX9x4muY#2KSh{Zrz);H3)jRR%Q4(Md8uW;b2LxrFJvyBa@#Kk&l`k%hOL}#PI~QG zlRCpN9U<0P`o^xiAZ-@nNF>~~yVV3pGM zIeyho(fg#tdFuA7&^LE8xt9fS3IcP0HKBiuGU9doH1t?cNBHR*DL3ul?Sh=yUd5D| zt9|=Rw0U~>F4$BGe!#6Zeit08hTF)Mx6S_F6XJmfk~6i*HgE3yB&vSW-hceb?W?>p zvi^Up$RY7~FYMGN8L>7?HK(0t=9Ds2e4I$e>MuW3mp(Wh?@6(r>{KYa#c%7mnB%nI zvj0j_CGW-KJ@iAYF+2{Lh*o8oFjIjtb%LS{DwwjaYx?D{grE+=OV}z_0_puKYM)`i z-g*7u_%DIi6RwgMttxh28lXVVzyAxf04>EDhgZ-dsg30NYMq&P!5G0==Y*c8KA+n= zUR>%=&M*v{pJ=s3G6chEiJyV#u0nX)U>02=bkJ~|)#{UAPP@oCs<>94fHwKP;2RR$ z`sP-~57er7?*tt6vwW$0&T6e2WO3@u%qC6}p0G8Ce5xlE4xnRN!rul-5!z)UmLsIk zMrnMi3lr|SIQG;>K{FSQJPOdoi?PN)!DVpLuN%XwFU1-i&*N#Jh;$r!(B8h4;~1aC z9bSO=5aaSSjY&(gNXQ*Xy!o8H_cPo{>8JfqCTet-%WoL1@(VOwX!#}GB8kBc=l|L{ zsON8|O?G`vlDyMwy(c4v`v8&ZSdJWotxeDIJ#z#V)~IHN%2S%}utIS1F)tST%j+W( zw?j6)yO89u<`64Fgt3%LwsOV>2R zs3I|QX+n>0Chxbimwt23r?>dYfc4@2rw4U(PGK)Bjd!mm32k#yjez816~+Tb@a{xb zxv#>6Bj5g-<+VJ>FeUr(OQ#!7Y)J*!61s&MYd5C)PHbmH%IyuyO2`6N5mR%CPS&b%LAho6uHmqcx2;IC z41pCI2&kf7peLJ!DDK_LNAOP=aN_=^JPG4%)m$SR<5Q{9p@JOG4vy@VVSg-=^gXwP zjftgV6N2Dup>Rv;Y}~?c^=bZraC*H;ZVK>1hTWAP)eAX9dbdPFtaNN!saElN=ZP2| zFMTy?@VqTLqhQK3EG$^f=NWgXU{jjPYSHVo2zImt1fv1`LR39^bY@>Y^CK7?P*iJe z;D~Y$k86UH`h8?0%x~jr-MEHz81cU|PN*oHLENu<(7`Oid*b7KP8(=CJ-aD?Zhu$%iR%W)iV%~^u%R7S`Wf^LylQJL_ruCK za?jGJ-x~@gy0D?XB6EDk;8nw$qyn~AFJ`0m-487ff<9BbNr)B)J_J>^bK31ZfA{Ut zJHtM%84?{?S-MRh9wk`=*2*-tqx10qkk083vJCl)%#0>!wEA*`1nX%a=`$CCr?tS& zWdHcMnb*lpcq$*Hn8irpiZcWLqSE@>_?Gz@ss zy2y;-h-Rn|{hl_IG_ZHo0@dz3A9D);MoWGYeS)7on)7Db>x?_3IG?y%`EJ8^&4+1l z_YdX0*vTyx7x$cxGts?E!kiXM8P&mbz#@-KYsChwsZ6cbCh+hh_SdXV^~$uwwG0qn za}4lu^urF~K@<Xn!S3t{{s~ zN#K~=b?0}xsQH29X)^?1bsX|j__OB5ni-zEk<#fcI9%eV_qeF5Y0Zfm&k z9eV8HE(NUqOsKBqw?A~<*~3MUEq7l?SV^@+CM(eKxk9B;UD8{%7LN3Bd^lRfhGF~( z3NNrhA)6{k^$-#bW$*k84To%d&e8k%;a`iw_g%e#GjAs5P8llYgY|bkotlhbVyh+G z&NVp~{n{uZ{CWrbnmM59;<=CgcMDI+S!*qh_`eTXp`fz4aTFCmNoELbkrLrm4^o{= zLi?;J3WNuF4fze$Q(+S#F{arxRM$CY%YdTG;&)s~h#~Y|<_Ek_`D86EkAi98n`(yh zc{jj1dSt3Wt39?!OFX4SG%@rffh5D(?rULxDxf9i-npwmL1y|<|ILg2r7NR{@%(5d z*2FB1qZJ{{%%TgBJ}*WecqW0TGjs&OtGTKl3pVeKLWvN%Ffo>rq=|{n(tC zgb$t4YIX=d!kjdx#nJ9lK<_fSuI2c};@b|x_wpjt;cu?W|BjiXbwl0so9K~tqWdbG zc7(XW@nf#bQKRY6v~Ad9mVF7EtBvbQR*(JW)&Z9`D4%Wyu{+2; zpxK+*$!Vd}0aAn6Z7E0nH_vU~j=A_dgmD;e#(b~PEaGj;Dlhq2PFa=+x3PP9aP6JW zORuXZ6;B!;7?h)36E31zn$>=)uQvJvH^AtF>AHI`=#0o(24&NZS%`7Xph(m{4Jw|B zEoqVpb*&rob*`P4{w;r|V3_Z(GwMVYHsgNxRtRaPk&F=xck@Qb7u2HmF@q4^50xX$j&UYR< z>W&c$Q^gIP$1lwn$*asqww%JtlmSQuerko*1a;3P^P$_~NXdsnWl7_OO40h&_yL%csd7>SX3dBgrzbK8-V+V%qMMtE)>NA4{TT=5{>uVIQTvE%v8&aFr_&+C{HZc zS9VM(DBRm4vOdSDMn$UQeMD?erCQXnu8}RHH4vP#vrjP2)Abk%RQFXnX>_~^Lv7a8 zpwy%TFZUBfVmyIYx2~0Bn+B(F5c)R7(**ei-fp}e9ok7UK%cf}4TGvb=KCxe8F`lp zx-29?s#PQ^yyW6*{f{JE4tJ95&Bnnr@vBn&m+nvMa{E|n?>~e8h#p2(8?9ZQLlP2M zov=^Sd8r(+K|UH97y;;M$HCt=o}Uo7Rzc6>re>0^_k`}n96ieWCH$25Po~X-+qX7a z_MK{KBzAWVM$h7u+gA|r*aLq<|Mu@?EiT&}159CYYC%4@h;*z*SZ z8{u6{=^RJ$*HPDDq9)D%3uF7{X_;(im=diufZONBrQ{Agq;MdKe(eVf)WO)+^@K`( zW)cj#lY;6{xRlfHZL3E}br zXK$L##S7G`x&mwLp+;sh^)hKALvggha$|MP6j9IC9_-hYb&Nga zapdO5w%jk+6d2`dDIl}9nw$N;*w+rPv|^leJVqIe-Y9nxVM*C6VlA3-%>7SxRpgi4 z@2Wf_Je9WH4tsGb{@Jq)Z?yF0D&}^Gm1T`)rQ^((PwiY;X+|e3gbYojDQx0tUhk7j zezH!CIE~}D0i($JfuDu7sjhL|*NRU|Chmwb%+qN_%r{^(hYyk+?0|wy&nvgW1oXHF zWzln{+&^c+Z)9fvW)|44hPbMhAIWk%xZJM576@+GAC?S4h>6w9`Ju&~fu87-lpK%r zeo}k(8cJO$HifK`tXkz#mIV2^@T<>iwi zWaoHs>TwL$kEhZLKE~KItrVVNJ0*QTo?A`{0))26RODfnE~eF$!A=RWVwN?zI}w$6 ze(FdE%=QzfI`d5bRz}C3STx7=f8yBV=Fq|%yJzIMLch%VPKc=|!YBR6g;BT&RZ~R# zj0$C5>+te}zHpgD+Z1!j$SL0*vceacn<<7ywnDPO6W{E+SJs3;;PIGN#jMxBLUQm} z(4N_tjPGs_xadP#F|*J(6fASZ^UDk}S1-?GR^^nzGr3W=W0Chhc>O_spQF8Hesp0L zq3q~~S(BO#=O^O+m*p;#Z(ilM5uhMEA+wLxq~;Q_WIJY6(0_q1GOX7|I^cp{|8X# z|EKr!`fep3_4vL)OJ?r<%-^)W#WFf1!kWSS;req`c4TMy_N92al3p-x`DphK{NMjX zFUSDHLs2DBXgyZ1G-e1fg8vgFv;(7qntab|lqEPrbOD44j92_Rb7uQb{HnY!+8OlD z;qV0^G$;7it4rIa$fO+PDk$f}lHiofvr+o;-_$u7R(y||Od`s?2#UCJ!3n~(hncVc zcluebLzczrQ>LY$v9KxAZVLPI;Qx-mirtPCo7Y#hE}m%*(ntF<3Ib62o{`xXYnYjsQ4? zU!T2|i2N_hJ_TUEW-$cjXn!fl)r*t^fTcwmYA5vv zH!FmiT8%0K-9f&|?|pg=CAGJ3DcefH+tYU3dRO6&32q zV;uPy?@9N+h}dik^XqzhS<_5X-SVXtfnvi^Y#` zJXq{ly36-V?m6Ra-U(j{>c2NVkIa=$Kc;W()ku}1J);2B!4?Rg6^VS4=Y9WzE(%I@ z;w?2fmT`$zISJs0sTr>$(cel&7M@FWbSy}^m}SHJ;^Wo8k1=MziwsERFfZAT;{sGY{@ItB z^owCir7psUnvB!*1qJUG)$3ZM)K1_j2pA8#>)}iV)t!u4^`rFJg52n7iMnah@u|*+ zq`yBhcRB(-q*;?{8}8mNNZo6f$zA)PjV`}6zSGuiUgLfK>e2NRz8}v#HUGQ+h|WSyN`?e`#1e*4EJ*8_nK?vG{a%vep&;*8qu$`kQy zC_hSk*UkMbxo^P|r%oTuxSjTUnrtZ@_{dsd;#t?{`1k?yrdRQ%rm#WTRgh7$--g8K z(&)aOkkvW*^pUduBeca6Cp05Yv|DpD)-XB z#z6Kl8Lk`XwE%7*p6g>Wk^44luR_30?cZQ#>uWKLcc)<4RljLCDBr{)KaVj~%1a0@ zZI&M@_U+@6`{bOR66KJM-V{{e0vW60#(w^*>0japk3Xn5CZAV#zRF_bPrsH$?8T2$ z-2w5fQN58ZPTz=2sh7&#rfWAvPC(A|J(zZV$drbLTB%dCtI)ABI}fdcPB`4Ca$!A2 z#P)kZ$I%lFn^*T%>a1~*`c~Or{BP{Nc|6o_-#4s~o$OmoW#3D-kWsQF5y@@{*=Zrv zm=Pj-6pEOVEn;NNI>{EwzK$@KvQB2uG&M`l(Rp6K>pp+y^}L?vxt{xey{_}V?%yAz zSABhtIhN1yS>B)bTM+UH$_GbM_CsS{A!Lvpx>fofgiX?2Oj8{N8oS4fRE%mWff-=E zGf!}4l`gHc|42EG7()qd$><`Zsir|Um)5tA`i=HmJjU%xW1~^Z`oa6Y z-YWa?n2{ny$XjZRrt82@jv6RakGG3u->rJ9s$6wO-_0vhDKnEf>~IXLJF8TNTijZW zf+wKfVPJXos#vdEDbUI36Fm|c!jooA|A-;j55+@8zf~&k zUK*}L?jNGn1Rgo@F~CE!d&S0O%k7-uV5WHrN?FsDoIx>-;GwhMp?Fe-2rOF#jkiJd zw_P+~B%-$qapaC;Aik3RzJJ&d<7>8}yj5(5)fQsDAvlA4&R*(S>i5${cGfaRFJeGQ ziI2&(_vPESeHC_g*}276BH=@;SSa?+TO{EEnylc{D~`dsOJi;kgl&hzB7&rw zVs4F9&Pd;9jXfTse(M5DzwMW|-g1m2m5F*Qz>@rmhL)WOtytjTfspT**EBSYAM6O{ z%K98v?KGemb51||%Skh)thuxanuxi84Sx5M)~JY=4^z10HyNmZWkUlj`UYxnY73xr zn6=0X6h6cUYs3JiKDDHD5uFI*vROW_dvyAvlK+*H63+tsJWUb;%ln_NgkFZ~W)_K6 zfTvCpOVdCWmMmxb4kHNnCOoAxV2B~`Ivoo@}h%J zGyas$v8pcE&|Al)en~KJNqJV|Q^l=*0)Kgp^!m#e3+vCf>W5ZaI&T4sYrKs$;9;Ay z)CbW~csB=5L2@3!@cU%zQ9q$1RQkBh#&`;qfr1E%q%e+ zrb+1S0`-5gS*z4ivL71T*WKr*TKUxBc+{rU;%O7biih>097qREoBtd8Y-vA9yB&L% zj07jPl+e57Q%Q9tPJt+GdSNlU9vC?u2WQ!IK)7M@^Q%SFm6NX0**ZaAA7?2@8)kow z!OvB<_b&ss2h5>XCBlt?UTF1d>hq==^2=htGd+$QQ9Y6gX>g89op_O-)4z|D-|{%v zMThx9!1pimg)y4(f~@x${w5a9d?bcZr%AruUEFB-OK(Y(qW9(R%?lp{f;`E4?Vs8ElzjV#qW|MK39f@O)_TP!91fYRDnsUIHRoOfEZ z$xdDdt9|d3VW5xXOvR&1j)k zp?LuwWiL7fhwqK+#zji+s+ZqLic9T;v~ni{9Vk%;_t^J`$2{Xj3J;#Wd(mY0nft!= zq>B|^S(wGgpR88Vq`S0I1X%PATZ7|pDOx_@Lci2Xluchm3;EHH6HVR(mDV(lZS4^T zKF1emAhk{_-FwRzVO?C)jeMR|>{+t@)5T=r_j&F6?LDxax^Up6=B<*OmeSex`5IPt zF_dHU@dftZpw9vg(1xRonC7~(m>hr>@RyVYNoA7xw#qk4U4T{NqKDPY&+K z1WV7b`jEbJ8YxIV~b$_I`kx6&Hv zPbzkhq+~LO&vMAL8tDSj{xY}XE+vnFhn)iiEkoq8y3zqw|*6| zd3Cnt{HiBLtb=UqT6mUK7Jtm_0-u4hACzzb18i9;U5|RFzNMb*Lws>=Oe&#Ei(nou z7++d+ruql}t8=4ep_e`C9r6e24x($ns}ER=X^b5it1N6hq4h(*j;*Qf0*l;7R2-v=o-q|S6)+P*O;u)Wg&LcI)Q0bb>U@@EYN?MxULpD|Xylur$I zE_-&Zef1-(#tLsqm?ia)C5qu8WKXr9nrp}Ao%9$rQ@7kB8+Mi<>U^v^`-Ushv+g0_ zU}t)KX(gP(yj{TZSl2W)0~Y#)`?+ovRRezf=SdB|9l4k74$03n$}!$iTvb*g1wV*> zTfrGSe))QCN>iq|{beXCG%!8cNejKS5Dg*NE!>@KG&n(y#Pxl<6Xl1O%J50H9KSSE z-`}BFk$xm@c1PeVD;q~PXbrGMOh7oGxR-Q!asWl1-n&HU8a7^vf_5QfCg^>q^4qgY zrsKIqOVhOYv>)Hf!qvptboW3Db=>3UWn!2v8H=#LVBqnv+X|i2J8zO~*_bxxntUvA z|Nh>WI*hsXc1n8w4gASpUD2y!i{W^2TnJfa8*fj{TWrL83x0`RMBb-MHxW_st;Q0s zrWzWL?mg6O5#cju%E+&XDT-kWJYa%cQ+>JMM-nSKFl7^?cR!KEt6buS?zJ0=M>MK( zM;=X(057EboT&q?{1*ejFeHN9qID3zS?g#k8?w-uY|&{u-@9V|vX`+}`uouneTGD? zS9i2NeYNKx0EW+-Ady1gAX(DVRocZ6W2t2kUX^pC=g;Pn*}evSyW=Qx^fbr2H2vvP zN8V1nwHAENkX|ZiCvcQOfRjOo!Omh%`@(n@|Ha0%MNf~TYyvu$|0e%1Sx0&~r&)r! ztB-yG-9rihmZ%}n;DYC^@U<6{SnaK7bJ{f=QyuI>yd&+J`VO`RscU@DKB#?< z+u*WI1aex6^c3|w^7RP-w_M0u#}E~S7FCPT@Tz8Q%&|Y`*7pDn`o&bYQ<)ZTs-}M& zJtH_CPLJIHOY)?OSHKK+!2RUm(7x@Y3zgepw8Niwp^0;*AWn|)Bd3#~ ztOFtEj|*A97-*UMele8lY?8Q>hOm*LuU(%1k;?eRuwvB;loyac#ismRU|rB3{M%6c z@6^UM@`Hk&Vu6e2W1*KPLh&5unkK(RX3vif$`wSy?wKt!KS{4=5| z-&>pos1k82H2nCazLoc4BsHk2^Ll{ao$#=u6{c6BkI3qLKdCN3(>94^#*_uY%*X2S zikn80N1T%_rVH4XLYj&1*=tZ&*bZQF0hbIcMjsF~yoJ>Ok5kg`a;{`zG2C_pzx7J_ zh{c_Nh>Qz-I>XF%lF{sMq~{((#v}wTTJJL!_%Hcl(Z2JGK`K5OFb6|_GT)YooZF_R ziK+c2FKT4V+|Ex^#2AU*<;ziRXH%LfUwDK!`OPSiLyz6B>quAbS zh|P0}yXt8;>*%#TD_-)Qu3^iYivyIXGkTFu^Y#r{-j}kDH;KGFtiIxHC8gOxl_f6_ zYM1llUzfw>CzdBw_P8!DpBNsx>+TQZ%s4~{mAmfRWoTtuj4Jqu3%s*%a2ahSfVy47 z-C{@TxvA}{Z$o@+t9O$7sE?eTBc5Mol`=E>C{^Kg?kFStuQ-t6r>Y&{Vj@Am@m4hPt zcGFUct8@+yBF7>lea3)m6wd3hsF$oYxwpXLQrXpSCDMko?MqK( zOl!jZ((@rL-#)uZ|DKe05J7<3vgo?qdaGT57#eP~F2tEB+-xELp*G9gCi(R~&!+h6 zJptLEvBnp|jTrx32DsM_B|EXy)e&V-1wo&_`pTtZ6_)1iwhUxRFuDgb>WFqD&VhTx ztkskl@(y$&L1%jH$h<`}n+VlAJbX^;+1R*L!N=KOr{7-9f4I8iZhyk;WxUs!^3#V$ zObLfQmzFs{F6TZIvd5uG@6C(Rc|e)u-Cm%H9?WdQPEXN5sx#OA!9;;d*x z#-O^=Zh8l*ZVEp|Ifa;ZW2O}#gBTa1kcQ+EHP4Z6Fz#;67AH5yXSWh3UHF>3E?ml# za4@jTZqvi5;q|nsnk;xrpXE)J6IR0p!d#Nu`0UF8Tq~1!pPL#VZ_1y{&+z!6?dkje zkQkz`2&h~)aC?4JZQWcN>P97?7!4#hV1n&R8-6dyNV7|KTKahBuJASYG&xK>{`^}C zGJuF)Ouz%!4zoY4Sf5u7??-SpCER*~bDY1E&;Rar?hvh_+~Mukf%Qj%=HBdQgW7oW z@}D1bUIjcV^}nUKPS9BsiQ&sE-SU-{+f$AftXJyO2ORLvG1a~kH5Xh$4mXPK37(5> zM_1k(M4C5iy)GdqnAT10`5AZT?;ZbiT5**}pj_0!hlS~2WThhgb{H}pL$E`~!fI~a zqc}UQx3ed|#mc6a&Oz@v2766#XWD2wUVVx`4b3*HKJ#3FHtBBDJ_8CMArPT82nRyI z8S%l1$!Q^-u4}|Z7pfai6w035X0ku=$yLBx#v{pFT8VRK=7^d4y51TnJe?}Uh-hqM z?Eyh^X1fNNu>;e{M}~Dva+4pHcRzF9bQ3dwmUDh07&&gL*#EksTk3j#^gYhccU@C& zE~D``DNgWnw8nbWbO`B8GmwWw!&Qn-zueGmOr-XCw`49J&AL*1@5CF$^DTWQYrQvc z2Tna~UtTrcf{z&maeQ*oNz>`LdR2#=MLP5LK1ZxF#M#4ha*Oz?j zIn09ROx_m&wQvvrZ; z3=biqySGIUy?_a4KEtB~Zb@)$b-Ld&@*VVnZREOi3kDzA<;EH5#^d%oi`!*Ak~1&9 z_8Uj&V~o@RRX=1PlB@*7yHcQ=s6&W@j%3B2-gnyeuitC3sv3;F8t{AKBpxyR=&VEk zGv$6a*fOKuZ5S5<3w$`+Z7huB-9oEr-@iuZBw+68pYl7^)$CTH8jKFRI(9|nj&jgV4xjAI0++57isdvrIHxuePjSZm+*W6lv*BJg1f2BJh&XTE_YqdZZwb`S5pnm zbJjV3luHHDe-CzDX6A`REU}6GX*d)sVzaMiQiGAptkrPLj*c&ewEK(UnSJ3!1JsmFAsF6O>wl9;-S^MZMN|P` zHf*VZ1htNkBn&@L*~BEN-FyKz8E-$liS51@M;a}6ozXx3Ci~>Azz#D><391>3RPSL zDTFNLLw$%&#gOb{r?J;g(6T2)HQLkzR>r?H zBvyGc+t@@m$4+ahGdp#K{8W#6vI9D)acIs67I5YevAAvKd8+={N<-!jlzT&4-SyLS zUX$BZbLmGppSlOK0@Z`Dc4mj+t`r+&KMe4!Qx1>;BExcYYD+~h6V>{89)xqZMthfi zr~(#rD-7u@sOO~v)Yr(Ztj`Nf?P69$6zLhQm3|QWp<#zyGGsjJnU%;PKJPa*qQtjP zhxx9)q@EaUr96i;=H~FxFGLU9ryQ89aoVEC!CgJ!vJ3iRli859so*(4^g(l5`aHRc zQ`-W0d}jVuS~kWfm4jqO?j!Ve4-lO@CR3kJuQlj84{*Ple08NUlWqLb@i#N=r!yuV zMo2-q;mW!%!5Z(U`s!}5qA`m~Ie6AL?*>SCy$BI$cdgrp>^I_I%otoPhNsSA=pKrv zU4{(l!k(@95D4(R@~Ts=jY%fb2h5f;=bqf$*>*fWcEnKdJ7mk4qUZ;CjoQK&!Fk5M z2~Ad278UGTj~74{s{BJ(cNkfckr42&N#v0bE{i0ni&lcAQi#MHEWjn|(#J&>hNtW_^~ z)eVkhI4%TmE(+eaF{3|Ud-dEt=gQMyq!OU#Xw?Mi&nO}w7!b$tFwlFD+?w0>o4W^# zx#qq+4?mMe^IIol@2e>qDLIKS}bDz@Ff|9Ncs!_;34!=}*saon!ra@R)l|*)@?+os%oJVfF181ZL{}NsY!egeb7GN+Ip) za0{{^F`-lFXM4+%ReX)hkOyJ=64B5b(_EjaFnYcv+DXvB%;wnh#^|!s#%mxPLDoS# z?+nc}n$S{1ikipTrOEqMY%Bvz+Uj%J+*9jCoo~6Gc^Xxy={@-E%4v}fXrti4#=BPo zvW)qTu{%SKGd`26RIszu4C*kM9H-L2-YD5bNw#WJI3t#wX-{R^V!Iv~1)sMQq4>^e3oc|hh7@4K;vAMElr_>Jj9sGAqEf{m-s z2}Ai2iu<9s63A;DbWbG$*jh_9S2P|hr2-%A0dh-e2}zT!Lr>=_+PT^`^r45Sz5PgN z!9nsjz_TRF0yx4S8q;1LZ5?S9Ci>XH1jxv)#2}T=wI5}7uLsFJ$TSmmJFke>+rJWB zRrJ$m>*A8D1qp1vn7iaD(hovfR(L7(xJTNOY=^?p1lx4ajLR4KqS4=YcO2_ z3mkLJ|1EMD2BU|8ng!De6IgQYX>PhC5lMdpv;UeZpxfA<*OF@L`NNWEnn;%;gYIb- zitZ;r-IB)V)Kg6paguuD`ns-~tQv``UpS4P$#_adNK=cc^+y_mpiuH2vO~w2ltktN zo1g2&#_gg_B99?`j%c?)%1F%rYTfN`NwnK7>O6PLq*ab-hlO;-(A_Tn^oC+(8p@-@ znd6ieD}K@CL594sC{Wd{a7Yf_o@!EkLf$64pxY)8P3e8}s9y{(Dg$<7>FnY-G+37d z$N*4Jq<@4k<3Bh-@qxMOulgQj=f-IVNUV!&NN}`uEU%!XsjbM%c0U3LRwQlO3%Due zk|SaY+Ax%)5p>gmP@g)8T8xtFB%aTWif&e}`E=rZ+0Y4-ZthFpSfGjwFDK0BNNlFh zw_uV0Z?F)9Ay{JW&a;fx&q3^4hnYVn+|zikksQ{PefU9!=(Qn{!_ubrWaip83d|^l zx&2kgaJ&y9zX@yr3O9@YUtHP$PiT{Wau)1+NSX8G%R8(l&c1;>Bvwyq`E%ltQ^}X? zEX3cSx0c%CK6_ht5-Y;a{#Q8&f4ujf9fk9VndJbH=c`-SEu;(P-LcHKRruHb8Spav zud5BIz{=#l!=)myZ?%Se(_us|vK8P1T2tT5p1WRG(83e)yoA=+1K9&b`@T3J9KLr> z$;$U&{@NG+4bO-ftY-?axi{wxpmz)zW=ff?O!aQjVTT0Iglj3K?y@7!>`y&qJ`A5ah|5Y3c|J{GjpZIE} z+@|VKoPO$?xhw6)uf)UKN2foXNfY27zx%fGwgUQ?ega*%_Z{jdC~2>-VaRh3Lc0M7 zVTbJj4XGty+1#8et5mb9rA+TLih z{Q!_hkjb{c7*4~eceQ^ph`;#r+}{2s;D++~j^5$0z$^mybqDh2$qPmP`Ej&cVe;4( z{Tu!CUsL}dcI97F|JSbn%i#QFaQ?U{{<8IdU9*38Vg9;i|A+hbmxuG)b^Xh^WLW-h z>|Czu@^qsU=H(^|hxbky4K<@~b?p?n;F;5vqeGc?0!GRT3oldT{b`{4?4`)R7w*RX zb#rn5suAixQd0eGjOe?_0MO@eO68}J4ow{;pz)?eQ*%OltwzAHvE!PgXRo1i;q`TC zkt5V`%IV)IgPMh{%4IcXssZhpzDK}frO%SY>@@k-6pyH`_a3P|uZqsBVr|Joxu|vj@68Rr)wzXs$ zI_^I%Z+=-MP?G04nvHyf;zu7?s7vnxq!AKisU-^RU_{7*rl|1BRZ-i);}3Ys$_oF1!x5bg;0UuYk~3hB@y1~i7V211RN$Jgs75V!Al%qb zrW&cp>*Af_M@3$Q*+?$Q{OC`AdS`jViQzx&7j!lr8Wb{f10bG@n`ucw`X2tj7zAEb zGE?ag-jA!rO^H6-uz{V|pPL&O26@g;6(ITm5aZ&|%n8yIIT=XO`2b-bwlG)EMem>M zKlg4rSq9$!y3->mmu2Uj{vpQhQ)x_-RDgnyyTNMVn+0yTF&Vk6BDvaSz%!++d51%N z;8b_&hw$Sj@1Nhln)-OZK+Ne?7R{ZUtrMW)%1g__;9eLoPtv6j{kbbYC!Z|h&Ffmm zZu)6HN<4P)XtjBkL(% z;mk2@^{qL!7B_EPct5JsA^qT0*HZ?8%z!Zs9xM;8^nSZ=gwL`iI&qH@!Wr@I-IUG9 z_O_3+*;hRJisPc!ZqlLFKykl)(HqP{jMHoAe5Bttc8qk3mePKxn|PCyPc+8F!B1*# zIo*J{^eW#EF0l_yKUkES?eJ+;WV!NA!QHBL1BKzhR?BDSRRcA}BfeIaSQ}=FKGHqL z#28Z~Y2wT1_U~m7rA72CV4c^b$kIE+Fn2?t*pM#1wHydNK4XBL}Y1$!`xIVu%{}+eF;B9`XHcZA60=LuaLfK?{3ijo-jc(UwbxwU)W@IQQ7h z<1sw9H{}@$lm7T3s9~*0g=7{WSa3XNSq(3eg-2-gy@JUN&1?k*cM%p7&V3BK@2d*_ z*2w!uL+`)(pMPpNR_Zf=QZK+_VEd>+#3#>%8Q zkjyB?8?f1#0*q_$(6oKmWf+e>XCQX0tDWuOkhF07?f!@NMa&y?9*cOV9cAy;zmvm$ z2E7Q|lA_##y8~`wKdR?4Y8ucHF%x!SlL*NT-A~xw1bvHV9paTA(h@n$gH@%IL_G@R zUp#o0@7);Y?RBL$Wc}}i_(wtREeU)87a-1ng>^$_8anNP>bVU099J`jRGgYTQ2EKE zrlvGs`h-rdpOZ$Wp83p?c=hhCw3fQ?A8e3^Ky9uE2?RzV1ZT)BWU-gZpV#4Jnrv{G zjP7!JVG+K>My)(yJ{DN7pud@D`h+b5YhIY9$sHN^$ek7a2|@+~pbE~Cs%a%iKPVVU zHDH_5?#zWHw9CTP>hmsUZl1R~D%)6Fpchb-f3~LUL%N35q2b0H>EH`GoC}Iz==@`J z?HNkw@k6PgRUB1S{_Q=Yx1F~ZRJ^GYJQVi`9_x(7#z{aNuf^9Vie1ZrPG7855IGz@ z!*a^kJsb&I6Dt-7j!?1!CKVn@gwfe-7kQZdP^_(NWm+hA`my=OpDV;3>Eo+|cIFbI z($o4`iC)HjiGAodNG~W^EvIuk8OQ=bfMU}Wo^A07TyrTsX7m?>H@U^IR>v_+LG);Y zP(eVgbhxeAQM)I}#)rdX-X$QHXGu|iW47+1#eRd{htyh8g*g`OQ*4*Q6T(iaYTUuI zf5AC!I9*TDOw)lk;g{V7cRN0|BtTN?%|tO#m?||^JbuGOb+PeaQTb>mH^N-{NOF*!zu1|k*)=5L5{j%d z)CH-{$4~_xcK%{ugNv5KWyWPK@qA3=G)Q!$9a zH6!b|AA&hv0xO zQ1Om^q_I8og3d<7rB5%vgRxPKUm`~44^&qZf=c7R)_oVUH9*xcXP-Wl9Ntzr^;n4a z2>dSOq(!}?uXn%1Yz`!5f zn1d02J*Y(Pqh(UBFGI*$3v_m$G6a}Ri7H7s>K?7?BzY^r)P&#(&9^uObLv@o=k!#nYkd1R=q;xDD8Vz@j2ZMQcGnmd{Mvv3!fB8XaT>d<>?E>98PR#p_C;$S{D10peO=o+J%NOsWYZI-~!tz=^T6Ed6 z*3HRX7z>L!?K*S5dOO!I_=-c$O;65q>hVf@IaFRHQa%AJA`o7jq8KB(7U~8QU-<%+ zsE^COM+ti+(;POFjk-@W>QcU@9Br~F!?N+Z!e$SnN};#~FOb-8MJ#NYO7%3pfPNR7 zP+YJ!0DV(2s>luxr0)VYZ%GZn8g1$9T#GdAP6#i8vuMic-sIDQ^C}@eC!FV%M-pW$ z31$L`9ybq_{a^^<{KX&(2Q`J5a&WMNK(}2{V}TB>5Q%%z&O$KBAtZNTQ?Q6EpQVq0 zJSKM~o0!NM={NzOv^P<#Wb$?O+;z>$ZS5oplCKMcRuMHMo=8ZF%$?DyTWVyAbnen&bkYp&>jV?RnVGRxO?jebWxNDjxWnF)v{PY zRwQI8Ra*V@o17gPduBU&sQY`1p!o$(WCti?0Gm?}^k(&z!ryWUlfp(dXo#sLE_#I| zy2NEx<9m3bhw9VU9sV1g1DO|Z9z8Jqi{ZF!-7?Dv1~<15596Yqpd1N+3D8BTY!it6 z>+>2D-CeqFud7@(Z8wh1o-NC%Ae9}CxN_seeRlX_c*^rLNKlHyeUTZ$&rp~-!-$n2 zKhPh4&nEK=#HgKC41}eS1ZUJVAdK%=Mq#H&?S${K@Z-zoRP*Op^38L#i6JVL%4+%} zVTU#aKg7Lf@2z{XAgWJ-M9~+J1nX@q3n>sRnw%fBajG+GbSOQfj)!%_CaP+txMufT zNu97roLOnGY*Z|Apx5Pc2=!0~1q#3TdtDzDs1vr|Nyx6cPJj^Z{3Q0>;0cwk$;q2M z`aQ|J$J4a^SQ#?QuCXm)vemH5bPfd;{1b3?)yI&YA@K{Gt!B-r=sh&&I$et>Ks?(l zk=2l)QrW6*q~5=y-)Mi7`Czk2zr=lIVU5u@5MU}@+D}CEl9DGdxNIoI;K&a~H)4Kw zNqreDi_Z2#PDmG#C%zzm)$5SbMg0I(I`#cgRkFHwP@|9q3lT9gE!kAMW8juO`-xx za6rze;e@vUNRGK+5@_&(9%cEdX#LYmK*@Sx>0>G;qkPTw1G>b2c-GmuF?YmV+*|U% zQK2v{&Y2XwvzORZm=GJt4g)YIWCI6%{x*urH$`W^yO_#iO3ssg8WzG8fDlrn&>!wTuOOv?cLvok`ck@qWG!%gbZ|U4JKj%o5O4K`5PDnf#KX>jD%I5yDhi zf-8WQ7dXCZRag0{su_@~ezNt2zHbi4b$J_Bb*|!SZwLKX_G>8o6qw)zpcVw1=2?XH zqVHjN)WnyjdXG&48Q!#7&Y7C_hR~!fmmdAZFAi5Tq*AR-#x(AmOlbb#q5uMfp4+Bd zp1;djZRHIIPa6b2E%knVt|FC>mIr&8F2Yz3Am!?7{Y9(IF*O{_PRx+Z0o( z5E?W<5{Yu8o3uw5T)CtcIZQla8#iAFygbnet8=@K%0~*mPVP({*RwqAaIdEOR%fKi z!|RexT2nz`ylawjV6y6f;qwIc4kCz60?13iJIF22fX;ObpZzNWG|&LWnOCR&5<5DB6CY#wl#;&0nmg> zoQN*ex5R$d`-D(+%)QiudUTFd;=7!Ha zyyI|w?`0l#7uWUM!Icb+?Lq1)TG;u~rBJ5DDv!jghpMs50qS_qZ`xGxyQM=Fh#3^Y z`jqPQU74Zepe36*##5x$14!LKvmXO=E=gzu)s2nFHFS5O6GbTG@4B5x-2dqZY8Exi zXbJQp+Yby$(M-0U(zg96{B+jOU`1|Kj{x1%1qo=d8kVDjE<%{hA?$Vqe=H$G%o2=~ z=wEikVQO&SS+S)rUqVy*d-Y>(CjGFWbNSO=wH~3I2%v)<7;_OGI{XdA4Y!|cwS4JM znw+||@c=!evDQV}5xRc#T(a-i1MjMe4o>U{Q~8WYX5>UlFtVc!h#+sjg~Vxn+3mJR z9IpkcWUynVq>5B^B@VQdvRyc!3i4~c$K7wv;Kfts92LM=P^_3Do=#}2@^(sRDQQ&Vi807J_HD|6G}DAfS`Y(|v=-ya}4 zNt2f|sis8D_J^7!m>?3S_He_v!2}y`pNP6~m`}BijyDr7c$r8aqjP-3IjfPSicka# zJyd*w5=0WY$cKz)G@%bknn?0{u0C*ZVKaU`w?adGpqLJPN8WK6q6!+3&H^5igW%Qm zklNL^sFYhhz1XtP+j&T8!cLj<6voODq~io96zuR>RVH*R7SsgHTfNGE|?LWXw48iHYCH)40B| zLtJm0*Z1N2WTl_fnp0ra=z!Mxp_6y~Dy5l%X0ta73~azGgCpM20TICjCXheN;h#3k zB*FCvMFm=82eN`EACK-W%VTyq`eX@tmG7#1)Mo}GsQnG5U36M z)1S1VD50+=zm-vh@#E^f$rMfC0M*r_{>^Fy&l7;yZVRP1M^_YOW6I2j=lBt;K)?d?*tP zE^c0oR${=|UFPiDh~(W;_G*|oYF1VGHK;P=?EOvg_fLbF4t5+avr?aUlW7vLtX`l9<_IsN~VPRtfV1ZU`d6b}Yhm)n~j zh+uP^ks`|(4;SG4a!%XKzV}XpwjD~kdP1M^1LcBX{+Wz(O4dGKwW)kQbPhp^1$_)~ z3TPvGFb&+Pi<__{7)xLX`k@VBIcIG9MRm^B?g5y(vFyp*qmtUz8)W&E((o^)TSzdC z03sh7jl?}e)+QvjdQjx4CkeOEcW`;LiJGZ5d?)v5Tov((>FJ+zF5D`^_@YS5Ov(|0)2 z_sLbo!P#)!aRxCOII6LF0?5urIDC4AdT#sx@~d$1nBQTj84?GAtVkAM8-Qp%_AW9IC5GrX zFrYmNMYBiPzjKqd5+t`r6d?tCiT*VXeIYWLMI_535TB=P3*VD>L-)|>7~Bxg53Tf6 zBuUrk7ei5nEwW?0zjrBTEBr2sT!}WNn~W(q&sW2Y#-YG@AS(mXISZT*Rd5F2+%PqA zv7tPN`l{B1K0aFyUvtl>*Q@Y({PFq?pOmPAxr-$k`_8<-7hV<4LhnEl?17jcPXZG6 z5@5hO)G)Z0l6(YOeACh$z2r7w>tX%@vjV-y5GlP$KhNBG-{+YcI*vBhf;-{NJ803v zA!L1}cU~!Y!li`uUkrQ!ugxjGATFCl)rQa#!NAXM)@?N~?v%behU|cuQV-b@Wz`aP zBRJ{9Mn+bRr>67hyt~DKHGletn;XlMFSML3N)2=}1fN8Y2B}0eS+Nn{26QsyJ;B(N zF}N%@$zj#j>x*+b{s?6t9}q6i!8$L0)$YF1-Fl&j%ccqn>gS{`F*~Mz9>T{`EP?VI z8(07q4e|?8Kdp36!W#i0FTW!-V8iJ7fwJm6TLXIbq1+>{3hcToq?6EXMh80Hv#hEC z`sb||6b-}&B*Ae=1Ob451Y5}dh}dpaZErW_;+?{PT;{H~SpRgcs$C@`X|Eq4hx%4R z`r?jg z@Z{Li<)$U^PNrH{BT{&SSZJcOrF4N;o+;KbQ*&4J;(e#Rbh3u|IjRcnd7=o|<=A~c zjAUdp-#~7V`RVKdl#3DKbdHHC@Oe%keLs&1zjc!yDH(6&3J zvN!uAtZ%v2uU;u{)4_%% z<|K7EQH~V%Dk+cKhUE~S*$)_zBmND@m&Z?^dO_mqRAX+VL<8O@mlYW}q ziO*41z<_WfuB93A;EX5|@f=^>)KW~3bKaY##i&GmP0ac3Q_uLCvWdj00cha`4B{;Fjuv>h>{1Bwr_wR3I zHWfJbj`iODV41X$l!xOqOLJoeLFS3d%5N=Lx1L(#!<|ym{hhYW)oExsLzaZ~UV= zs~~E$|CKrqlbDnI{0iiD{$hCF+m~y9UnWIU4-dhW+&F8KM!smYytPEk z9&_^pJ-cT4XZ!!XT=~Dp&piLwiLJJK zVZ5M-KkNe)aLP$eAjKCe&CXzspB9~Mx+7!&qT<7J;q4&Q?<;uO_5Agsi?1%GC`L1o z1b$9E*&F__0*K^U2#wUA;GehYKtzydG33X$Uktqq)PProX_4U^Qo!$ugS=bV-2{pz z7&5r{SfTeYtH9nski@E*q?4aNDW_S{4I*M9ly9Q|cq{tvdh z>9L_@(8ywqh8t<=;*GFtmOCh?+Eo#2_1Fi zjd`4^>8A15T=%+De1MbR@}F;e_Nkm(B%#a&*M|4cZNBzaUwjA?Blj+uv@9hzI(Xj+ zICky9^B&U*&zI1wC@JD)`-Hc;u|3z!QjrHTPSd#Hwr+!tCaS70oaTFT9@}O5v#lve zKg~TSk9OI4_n+}A&<-27({hfz87s|Xkr+urjh2lV*^k8Bn*O=H7~Ulcw`W{;dJPG> z8ymM4km6FK-fpau!l~xP-;3iwnE&V2}dXEB05m4OR)2vA7iX&-)=e?K( z>_4OMAV^})q{RZ3r0w)v=+?jzn&09TdFXO`?1jvMvvF?89Lb47@}pl32M(QgY_j}# zG|kaaN=25$_)YzPqd?+61RgH`NR%^b|0g!WOUR)e#t|?@UBlqppWunRsASlVRXxvd zRfiXR?JDTHb`@lw!8jD3Lr*zJZfM&=ldY!qOj95KV&J`ve0?_~1*AuqZDTbgj`kZW9LJc5$l2BW& zcBN(pQzA)$v;Bnj&&;oPkM!y%%?f5M5r#=w>pHyw_2UCPk3A{|GEZe3Q`tn1QNv^5B`g zDO&NM=^OM40MdXwBhwZw2Nds+oXD$47W&Z7gK6~LrG~WTl9t<-qwH zDP@?UAYH!S96C)y>YxbMd8o0Nt1ya^lhux9v36zj&vZJ6FvSZovQTYxk1k9fS>Pyc z$a$+pS8N>95BE`d{N~BtW{HX8(TCe-?XG0&r+hK@Tuy;_fh0vG{Zl%HhyIo>Lo3o} z3=7g~xslyqAm+!AEs731uwR=!A}mc!9UkhPGBpfo4Br|krcW=_uypavwAeUMhxpg| zjl6cEro&iYYZjGa_2N>8s?jSvpsqBuv`tn_=)%m1(?RNFIoaZQ2tiOS3}nEs#VMyQ zt|LBr9J>3%^Rx7^bI7*P&XX)mHvY6YFcCFf3u0eEP7cDf0jUcmx(hUH>|577&`X zqVs;Ij^lM@Gr%?AJKAalSoCi9c5bspj9J_TgipmoB%=^zE&Az{PY3OUKN!l;H~L>q z+vZaG-`uv_Z6;Qhr<}~3C>fUu5<<6SlMD>sFL)C1M6h znZc%EOFVIlv^Ko{sU^sMa`~F0dF5J?jfE(T<%`}G*j$y%^MqCFS%_l58jvr9;robr zICM99A7TJjtA#2VT82RcBDU*qb=TL~46JnR|7`bU`lR%0tnfaGr=Gh3q#W84jJp-< z(6b2sWe8qy86|4|YEGec?A15c`Y&l`w2CS@ulL*DqGjZ_psusC&ZWNrK<;@laQ>p; zw&ZkN`pf{SXyO}`lU)C5d<{!IeEN{qPPVnGs*LSE(@f=q1CKl(SRa?uHeyjE-%d*o zx5cQOly3=Ab>0lAd#GXeS~1n%zx?v``Y(C*b2mdV7I`QZx&n}IG~h*y7)St33oCN? z1>$O4>$zK&RT056F@A9mo_~H|%3l85qs=Q_xykE_-CgP@TKgV!)|M`Lov@>znaW7< z-?q+ZWnXmetFP6zT2a~ldcy@N$yCtO?5e#a+_OD>0_e;H5PTNZdZx&Ii%3y_tx4_M zD&23}GW*#|l2l5+kFmN`fs~es{cfPwBn1R-iLVSY6P7?V^ZyZf* zGp{`jiIfYdCuMJzw|}5DkomodITy&5FC6K^;n~7;b+ZM^g(zjgx=79x`vK1T*QBuA zmu_&~p1VR0?<$ZWB9cRvtk@Q2{d2>x+c6uXb_Hyg_|((lDmY`*+5WHg&NQg0D+<6^ zrBqOH11hLN3?za|Su9J1C;}EJtA#126``0e#P;+B(2`h+3g5?pb?JyTuYYWYY zwXSUL#fZa|RMPgkSza65r7454`&j;vhC*VM52KRc4oJHK$i!uyGj%AMrb^^hrfaGd z2dHaBVbe-2lB@K`me{nK{oiGjs>MZ>tDX02T(KnlJiKZprv0|kA+9@cJ=yQ&(=X=L z|NG_p9{HJ0awX{VUSst}ri3Q`Bl)?ul23Hb@lcHU9{U7ic^~3WOWidZjl^(vP+C(K^)8aqHXq?suc4S zmhgoCkWtlr;Xz4D5<&f*Ku5Td=+iuOA)%t{kwjT3wT-5)vV}z)`Tl-fsXZetRVR!` zJ4ak-I(*-MaIPM7?da>TRdu_fMKQS>c7suel-*Yc4=At}z=N&jmQl{K`~#R)BhR{^ z))<`dC7RVgu`@I$KMSwvd8qQXL;nwVP^lQumM*E5#u=}7inI^}30K!%d%_4QuBa45 z`$hCjaOfU2^e^~>`xHH)HmvS71&J-Fc7*(x+H4FQ_Kcx zZ|lm4o+j(WZW>sCK^0%dcVqj_uha94WFC1fRUdGhs9S$M%@&Quxm>u$dH3?h!`g;V zG|@BWDTNbrcmp0#U|@|oVhT~EfFkv22ml+E$F{%7-N859-_wcF;#wdK&ScvNXPVV# zdgCijS0^mx%OjaumHw_7H;axwnP=4%OQ1H}SWSmDH&Qz~>7sbd3E)5S4Huorf;3b6 zD5@Rmk73O?odiuD13^X1zIz=^IYWu|E3pTExW9(LdI~HSmwEd4_+rVM_ips*#0q|- zB4R?6_0m?;ehIZdg_do(YjkQdFw_W6Mw7A(hxnbJVjDEBxI_7@4EnN3A zDqn1iD81vLk)~I13|e7!GeqF9Teoi%2DO0V9!0}vb{Uv9=_~+V9ss-m`%+K>XJFC| z0Pv2#k=aO`V#FVg8XHIEI!e}20pMjzi~%s-FG6fy0K81maWDhhByPa|6JO z!U%wZe}YY=oc59sSknzaxh9&uJ1GU^1;A?zV6&LWqe|8jtB(WLJP%kiOoV)zbfjFm z6scCEwj$j@Qor)Q^R^=|SUBE}E$c3+wzlx)2M9&(f@WLVT!c$AZrfNKBYY0-+JE+y z*I2F}yC_GYtiF5i1B#x=WOkO`W1r`lny}v4{n}zF%SE{vKkbT0t}>0X*54D$l8v(w z%SIl+q|YTL&O!0LPjUq}Tqv2P*ysfmJb15S5ML7DgHP5!aY?;Zj^UdQOTe4>1!R>6 zjpNh2v9|Wv*V9K2y8OKNe#YO$zGUrL%GIprmlV#JE=PoM*5`|n!Q4Ds;il1QWSS$) z!*oT=+B*qgrZd;zIi8)%ZuNa$f8DFWFa5>9 zGvfn=CE%yWZB5T&oJ;(}b@q=~<;Uz&`b?I2+q0LcOD~4Jj}kf`%*yLDk1;qnQsIo5 z1$ky333+2Dc2k|`ckhwSlCIz#w{{`HXsX}HX6az5Hl()kCp!hCYCQ)ohHp~+Ut+-j K@LCn$aDD?z$|4y6 diff --git a/docs/reference/ml/images/ml-anomaly-alert-severity.png b/docs/reference/ml/images/ml-anomaly-alert-severity.png new file mode 100644 index 0000000000000000000000000000000000000000..9f15d464d2b674e6a3c9ca0c2d9953df0b89fc30 GIT binary patch literal 241369 zcmeFZWmFt(uq{l`;K4n(JA~j82<|e2Td?5n9zqBZBsjqdK7+eEgu#L*Fu1!r40m$Q zx%bVz-+TUlKh9b`YxNp>x}UDDuG+P07m*rj^4J*U7zhXm*oq2owGa@HK?n$_`p;3} z-z2mq)FL1}bFh_>(NL6;q0w-2wzhS!LO@{o>iJb&8Bvq4KQ8P=N_0M532`mqrrX7%KQ=26q1Tw*Z0KNge*R`j$v)_&hH@M*PK5`yw zZFAqora=)HX7lZpYF_*G+e2yW_yB_Bd?ND!*ZsMz-;9zqU&hK^>Er%6`b$k}nKkfz zdw+D?@ZE_oNTO6NYxLpp*svpCb4K4#MZVDi?SybBonR0@K;~twFkv(6uY;4?73l~= zr*R+KUVz|)aaY0w*K#*-gcvj4v+?JA8CCTih)m^CX*()tlkOEDt?<|AG zSF%uP#!!~!9*wWkU&cviOZu8gx}$~89M1H(kPm8t$;4RAOjieN?@ji z$+Pd3T^ccaAAi9RA9#(=rhnq7!p$#arKf1Es*1n{-+zwqEW#E68NT-n{vn5d5D*Y^ z5&!o$$e>)L|Gkf@|L2dT{;b&u2vP`&Z{O(nJUhrj$t0Q~8N^LMrCr2M!2jT(kV;2G z%N7+{uox4pK`4tN>mVg-`?iI#u(efNOY7&;qD-!~eJ=LJz8M38mDV-|2x2y?zF{x?Uqp(k;rEQr!X#X5b?Z>eHq zkV5>|Z75R;Lk&q~i!&ru{ojw!d>fJi(DdNfBf_V7_Ai^l5poGw>4-CA?!P|)q>QD7 z42*#ukMJ)WgEyiMknx(hYoGbw&QBA{LMLSfAY8-(hX1=&5=5V|ffD>NS1uF(?fmgl zgm}3%Y*9|AAnAX<#i1;OWWZ26%E{foo0BH$xrRQGtY8>`_TN{yp%wxu+gL*MdHh!+ zGrW+6+bAkOqr2>1Ef6o|#tgqjH|J+)JQQ6+KN)I>P?_`ZM$Z2Xx6%K&!T-3y|G2^bL<8jiKSYCg zwy3?H{|cxKJfYDf+>k9!!_ARheyt`KT2>93V8pr4{r^u&U=aM1Y#K$!_8I?#ax}zR z>+!Eg;@K(Z+vEQ4po{tG0n?p1P`6>uY@Vb4H&{zCpcmYzD>7gz{Ak0alHgb$eA=qT z!1ZV|<+Fn9@V}&I;6?<^fs^~AQ-7Iw92y7|I{fBq*iKMdS>f))FFpz#SJ7%7uAZxQ z-{vYcZhYeqVq0mv@cL{h1XlFoXn-X2yL{YGTJ^d^G`m())hpY{A6Jw}|5r+);U!R* zOaWL7#-;{Q2W*_fEJwcq<{MkbZN%ghOpuvW(pZ**uEP#xbA;Vr53z&pI<7+x>*=d} zYm0PFL?#t8nTymij`U77zYjvt9`BUCy|SfTKY45U9Bfc;u;l+FvoJE8+L{GnGvvc! z_$${b93Usu)!0p!R0l0mvoMx`&oGJTze-KN(Cc_t=xFTJylKs zSks!Y{}ouOTAemj0WU=0qB|_Yp|-Q!$8{*4w*62THQ}#J%^;4b2-L5)JtzKM@84+Z z?Ew1_?so9Ybtqdr!}OFa5?O)99No-st-X9+1o-ZDGCV=l+iCDz@$_wpic(V3D4y~x zhki+kTR|_uDjkm#6xp{k=h0&-;^hI$;e_g_D zA^*gthZq0<0VAifG>U3SMzvm9loD6`8rdDgdM1(x?Re=J6Tv;4iaz~jLqV6yu-4uay2T1WN9@| zQVqZgl~Xsb6%$dELC3#qbz!i3=k*p05-x^vgZdc@NA)k80^#;cUt0x$e z+v%Dtt4Uj0^#{ZLsLjI6uw}}H5tY!A-bt(Bn3;mB{$`{kTh7I5ZS>ey9$9pqn zK2!S?v1ZNH3!I`V$b)-2hWX)UG0^d-M15F%a3L>W#+R*0u3}wq3erw z&pw=Qb1RSuY=Ypg`haO^!m6q4@x$#opZk`>?`rqr35n~&1@0%V6xl)k|nQIk$};PI|^`&5qMmp&R2lJGna0K^n2j1Y&3FftV_}wte$Gzk~;v_!(Q5Zy0rc=t{ z4XmI%1}!-Ls?sFpGK!AizM?(U(gS7te%XNM8WR+BTM>aZz)ZFWdQ?E)v z;%QBJoV6}%kb0jZtwZS_EFRjYuyQfxs5EgAQg^dzqnNl9$YI$o!BQ4a>}$zR!Y|P{ zpm;FII-NTnIGy^V;$&PhRWYN1iqigZV82o+r{h>OJ4ezvg^*S2*J;~r-@@^OddK4t zntQxWaL>)efSEh=27R*C+f+CY)m{y8nI{Z2aBa@aU^{f-_38BxpTn=XPaotWL>+mb zA6;n3`);hN4TMavzU(Ph%i@aj-nFLKtk*S7c)8dL`9fB^(VuGK*4k5z4WBfaeL*z(B?@44D>skNqv0zqZ}|L(Mfq$ua$Xgqp91ZLF>C3Zc`|G zUX;LRrDW`oS-$Xyf>affD;_Akks-9}+10?g9}`+8(XjB^*|pN_t}KgC7c$W7evlmD zb$Zw{mLP{KPYIRL6{UeGq)y;mD$Jw&5OxL(G}Rj)F;iQc$}PN{6x;b7NJY z6_wQ(y+$;^dCE_dfe8`N*V+_eUanV@m|W$YZY`nMOw0qzJ#r|ObV z?=BSmXWFkBQIdL&S0kts(5Rc1x6fCvIOJL?FFmR5#43WPcdF0&Nn{@EWE3L&7eF?G zGWrGQ)lM?)+!`;{GKGdTI99_Vx!*k9?0m-sEk~mn-OID&K>$rtuSGYi&0r2Pyi4ud zQw^>-N6S_pEdc5&y|{9Sx9vDg!9{;Lk^&x2l4_hs(o}1MauHoWl9paN5g;9dB#~G~ z9vOTPHSHPBZ~7!*A5q))e7lg7`yi?t^TJ|}kur*cIh}Ha?StAa4W((QzxZV)NE+>w zN2sIn@G(}|i#}GLr|M|FJk{g)G|4oIt)O2D10&fF``dFcp){V}mPZgcdVK$Gb58Tj za_%J}BhlW&34PqXj!vG5Bv zLHBjf{;`|@YzUewVRvr3ja_zp?z_IUq+bt|dQ9!~loq>aRrfk%755Xj(7U=zBz~l? zSck=CB_$te1}FR)=sy0Em=AgV>@O%pLxvZ^NR*_a@!A%Xoh2_o(M?k)R3tchPK=X)O{bOZBvlGfVzgb~UpAup$L5vz?H?|^F-7D1 zfM05SABzb3u7Ny7Y|gmG&~a3{Fio=A5bzy?{>KdnA`Ai%1c6j zTWUAKR84W7{j2u(tZECT)^85Goz5dnIPaDSP912-S1$wToz z0$GqNjq0~od<|YttDv)@Q5f#Q;hEGgdFtRb4~PJzY2E5NM2#nFy|g$7(@*357y+?j z8XsB71CNJKu40nbu5l+v8JO%z#I26Qy-JB3wmuHgVmWEi8`kSI|E}eOU5|?9f%U|7 zy*M||J35kHOF6#UcOZODIj*bI@gYF9(Om_s)^=ZUPpDH|mo`TDC08}iF`O+Y=&D)0 z-?7B1D^4qw*}v(E43_kaot?!Z3wivocD$W5Wt&(@>Lw+MI21s`!4w8i+1r=$gJMDx zr5N!8%R4YeOB$<(>j)FxA`TKt7luf>~J~@3IDMV z%`zKXRQH!)TXo_y)A4XH{4I>Gh8iN}6?K+lMWk1aMgHm+IpsEKQ22bkBYhfdmx=VC zC(?^$&H8cUwM&?cGqcTAi8_lm!UXUGKm?^1QJZa|C_r%tpYe+xM&d$SdW!updIV!Qc3r8%hR~0o-;+N*FsKYPyXZl1f$zkp}*bhAfj8GLZvioYJ z{ee`IpiypFRp6%4VyYDH2Y-Ciwb{8lLo^Pa)vdS}w=46j{N?6hcNM?$b1pWu#{`h3 z6ocm}_re>nb9|f-F{imvab2T(-2W!JXkVNZRw>0sv8|}^fu5PPcno=(#m?+rV94tc zRjTd-W2d;P*jCQR#n&ULrCa$QEIjw;Wy>L{%2j?7Id%{%j!#g_#XD@br^mzg{I2Wg z59>{e+wvlc*G??#jpkYPc9(X$We>?h*hM@uIAPyya&|XU$`6IHJWPUaQ7rlez%Do4 zXw~JQf9o%Vqkwkd38cu<) zPu$5)hNRYi9d(u)HkJ{!z?s~I{>D$cGgw(g3+9aMqJ?Bf^%;DXwuu9?`418>S>e~$ zjPJL-7XgDB10k32#6^BVs9nd{c>m5b6W0nY$=11JW+CNSlNnxUglNAIAY$E9$VyJ+ zVUvAD?1xE{Su7#Pi+wmpcmEBIqpL{G-OC^@`34p1=I4HPH~NM9c>BhCUPMrD8K8aP zMfQT=lAk<@eQSB~&ALPqhrX8aT<0fQvLq?BaZX}KA5G2|Aw=lCLwyZyT%l<&L1WIk z6(%3z3<1Y?;EU&0OhpCaPd&r2iZ1H$m6Gg&2K=FC{FH&^0;a_lb=}=uN|buCI5#&k z>o0f3s@~ZS`yacvHQoe12b@5g>V+>Bkt8Wr^viKK&C8aD%SK1j?;{*fO-p73T0tl% zTE%LP{!6-MkAB{zSH?YhuaLBdMyIS-X{b49(=e9RA5(rQ8d|mA4IvztuFh`Hb5ZsF zLtE1DzW9j&Qgmr?GGKh+IR}CXX~oJ*I=aZxcI~{w#neJ zSO-0v`g4P@+FUF|=pM#}hGMB)13O1fc$2~Kx@5@ExnoPS${a-k>t-N^inV;9&YO}l z=B2n?wr@)@bC!12$m$KgWDxYd1wxcdK$dfv&usa^&uYp(P0u?oO`wfSm>OU-+y~JpZC6+aXL);|kBUUodL;C8{{7}aunzR;>R$cFnaQQ1? zU2Meq$$lf)!|mAE(f({Hqp%yYjVkgcCuK_OkoPjy={NigZPpj|ETCSar(v>_I#sOK z@-c3}9%-_3Xsx<001avM>hLJBCma(Uh+Ip+0tG`KsB&BS-D`W^`opaHPK4O=-h>Lc zuIUY4VMw~2;Ca~>sbnjs3)sinEIXlrxUc*><8ou{Y@tngyj2lle$294XGH8+R>^AX zUgaV<4X^?JV%K@<%7BUHp+?hS+Y3w)wNF^Pg);XI39J@!z6X&NB0^)tm_o>9UIeqCp(X8$5%f3@CK2E8e52oR4PL4Pj?fU%1N(~PB z?NLKn?5SAPSM45tH=2gwL>126J+jX#gY<#={J~&CuR`VI3$}5;+=ap=gYk9JZRprS zp*iV4ML7c#q8A4HG&KaYQB>07$((K*vd%1on|Z?cyUeG|xRt45S(6EEw+otS{scSr zXDz#EmWv@LO)oXiJN%x%+ew=SQuT{w{l z^z$bKh#;cwRI(_u3;JraPUXJk$DC}xgG6u9R0itprFHhKGbGvD&vB$8-FrcbuqgB z)XpMnN|e(13J-1TbXh1eE$=SUO^TtJ*6YoI+lPW}@Rjp|i?V&^f3LX?r~FJNZvmS% zydUmV&D=<vh~7Df z?v5#S|MK;PXigMcqnJqV-u#L87=kVNJbuxgT*MA3c6%VyHn#6A(=d)|^Qh^wcTEw)MC zm`6U{f)0MeQ|PcneWg7XOC`FM0^O%9LJ-la|7QCf00~AoSKS|meGC`J=>@%PyR{U$H*oLhK9JF`^In#i!97eWS87I*-48`Vv%EfPuYf^6unu#L zmTDh$5;dk)TBeS$kL>DytSpu4JG4-xx#i*dD^4*Z0&Vu1ylQdT^-LH^6!9T!rX9XK zL^vImV67vs>o=_6`VRo$?OlGRMBtZmXZx&!*f`MU>LNqxnZSlJ6fBEK_OXkOlT8ZwZ|?Du1gJWE5q}XB4xJW!M~#kY2VoZZ6US?msGL zu|i%>iC{+|1Gj&1n_ZFc8#`ANd(a5GL@pCbjS zi}oLhcqf?D5Q8gJb#pm4Kh4G|~{h|Z(`k_If^ACfiS6c-$OX7CD4 z1|20>%1NtZElaD&pT< z2Rd2oJ*+3XQla$<_iao*hFI=KC{adM*Yza)F~w+Cb)|_}eKUSV4|X#422l)toKt_e zoKqZR=|H_!_-w11*0Njqv3EOFTR)A9OwCn$Bivx@D}6Q4Fu0ioOEXq-+_B^`QpZp5 zlW;OVgt|M4pV@VI`5O~FPzpE)$DZD6*8;`K9yaID%Sxn;1M7u@D+W1nIP@NqlUNBJ z_rp6=AwK`SaWD9uiOLcsX#uH$dSowL6T=wp8i#hLY~&6kR@pDs`j2AE=z&eF>#vB5 zH;S|eDzes{>@(rr3H>m!!BTBzE+o;L+%m2Dl!vo@mT`f%Q#eJ4B16*k`NWp%ayFfc zdbDGE{==-2#V2*PQzBUj@L!5#^XEM8!{{}&$8?9mj-R%JbtK=x`9sM=alqbmMSZhN zcjx`eH$}uOm1M~6e3f*z{`iVx(bo^OO%llu)A`o+Y> z6E=CA|9PPBLNJpM@a>HoKu`8+6gU;LLOh%)uN9Q@>bDD&YKSjjGixzobDDQ^nGw#2 zSNx`wI1p21*R6n7*nU6UK!Z0UQc;jN!XT-w#!9JNRu07~Sw}nHIWp&d<#yEKRyUt- zDWesBxwGhKf8v?`zA4TrT@gDKW%@(U(7T!FouK0^XV4L}nNodSV!G7P*>NlFhe!TL z167{PHEXNSZr=nP@jYCH-L0<+E-#@y`MiXkmf0Wh&6sUsf9=;1cLols*~cHARxJ8E z^y2O8zZ7QrGDvU|aeXPcd<&opc+8{jp(E7WJ&ojAd{iQN&tU4v1^4cjuC;`}S@;(c zh#iE07KZXFB{gkCg8CYaAnh&COfnrSL2wR7AZ%{Iv%Xy+C9W+{m81v5@??0EscOSD zp^t}W+H<+lxpN;9r$6I%=D5(1(rWlBbs<@u@0>7?h+TKrfvzPS5fNvSSv4EKnDV2! zUFlRlg2q8p`D+qpnTQaMsIWwb`8HWD(+)aYV~(qp5{VZ>$-};TbL`xmW?(%sg`lrr z_P63duHRA5M(4tMwF2Pet?~KqTANBjZj%w-F*rePwL59muwA{EDLEzVNDx8G6ER%( zPH!Vi%vX!gX6!%@zsRda|EPJ9eo$row$FPR+mjP-D)hL$v9P+#rbwkamM8B1gnjc{ zXFW zldX5S*18KS^xohvu3I)Z?hE5t3%^C2AMdc8 z6xSb(DvZW}X8W^_FK!4hBcyD1x32AfolqM`>$h1(dcYIOLMMzG+eMXPH__MVND%T; z)aipgQ9#ys#^CRqWcz_rA2;}qV+-5QuPDyW)q4vL1IzotCDI=h?!0SLtjbrG+V8<{ z#F+kWgCQV>vXyxd)%UPzufEF$Ab!==O8P0^kFH?V^D(lb@LqItwety|YAc8klWM$Mr3Pfs_sjwvoDnA?ltm_#+=k{)UIxlKWV-ty-S=}a0c&q%_j z0`1IpEHyngAw>F3?-ko-?kZY5DTnwHPR~btRiwkx1n28jq(9b2X}mWVtOk z_Ls}Hyv%vJZ(F}jfUphOGKzrJ1Px|Lv<0#(dq2gT%`O~c?4fNU>_tBs&NRHA=+OcJ zRn@f`x4o9AF)bHcEL) z@!JjFY!WkJ?U&e@o10PUd2LRLWVq-b2d_f=#co#U`Rq2HS65VB-jbM#;26bMG?43A ztYqu;*>PG`T*EsVqi|sK7;w|Nhsy7XdCRxAdd{q7o9&8u#8Zaoa65AR#-4R)lZ5Q=A z2vN=d`mR4pWcJeEYE&~%TG$F@P72<$CZ>NGupRI#IOO3bv9(ATAGc{CcjAan#N$1M z&0fB-Sa7dFy&Wqx?#(o>NKqrOQd5_cMq_~#p@;g{fv`K(`n^bK!*&5!+GlkBa++{d z7poAsHPVO?V8dzLV)P07X^7YUhQ|I^Zs;NW9Xl0CviF)SKECHu8?nJ1%MYUF|SoC(W2=xA|IjzN>Y(#UK-~1AouQxb!&*y>BmD-)|0| z1f}~q2N$vC^WfE!|f{YyR_S)84qGn5lg>m4!b&pua3Ct#=+gx z-*&03v>W!TnqWX&|9cT*EBcu#mYaM(Myz&Ujz3J+cYk!QA8I<9CTwWpf2gW!YTR_B zaZktc{ij?eVVMS&1smVI-D*$x23QMRu2-#9#Jc~kk@eWPMU41#Va=NN^M0v^jb|@} zOsRaqsZ~|qK?3hoq#EHLfbkhQbk>47B9oI4xcY;6BkC)6T}lLfWmn)RF0^;pPv7gHiaVL0t( zb|c~r@rO*?evk}C>6kmRpMos!ycuK%fP=3GIc`xR(A$DfG<(6zGOi@SuWY;u~8 z9)Ju00*BLvmV(7=p+?HT z`3g~RwzH6d$}o%o8U4-lV*h4|^Vu7x!6s|4-+Sh&F5jzklRtq^hWh6&txd2(9v^z0 zbe#Cz=V_O!GmKZ+>U~jD`|M4c8(6kh zLM7Q1Qs1?YyleL9=NgdDmh*3rBu~$3H=OmW0wSH8x1FwE7w%6rI2uv&-5Iegq}a7A z5X4E6yAV@fA8%jhPGkxP6g@sn{mh;Gb{@KRGiMrfrn3DiXX^f>bv>>J;Z(QWpghsH zL4&C$E=3Zeu1=Rh#Zop9N3mJv@bMkkzmeHd>z*1p$67Mrss1cHX^dC1M1_*yOX%5f zb!Eq$^d6i@ru`J$t?;zD@&*xevX9QCfclH{b70O*bY0uoS@hY@Fe(|Yty*=-*8x(W zGQU!j<%(m!6#E&M#;f^hnC%yS7#Ko+)>XI(%|km%}!u zkHd8de>FGqK@5FAS=ZE8vjd3KXNHWC!J5e2f&&g45|o%S`}Q-Q8x5tP%5U}7CT3=6 ztu~uwKUS3OuCOU`!$U=--0wU__p#$*&LmKAzR`K#JHY-eOR)85CV5>yb`e4Bjc-%+ zWVV3wId3)!RzY6CCF8IzI3f7yQBbWeQ`o&E96bm}DKBEAOc(TyB}kb);=t)cO9yOz znC}Mj32klbp~|dpBwagzEv>h^&!|H3zG~%u_1<}7x7hEFKwOPdWsnk1)=C_EXMWUt z5z^5V)ZY3~fu5w(`zeruTdilRNR=Kg_puP857PZfh&MiSt&~ndJ@us0PyVOP-~JBA z4Z2(SyDAbWo%Po)X@xqW<0ZI5qCdslIYi~sj15$X3M2a zvWgVqlEJ>^0j0PjV{G|jmk&n_jnFk{&{rn7RE6(sF30jijZlkeIr(;+$MLLe3b!#! z4%QAg8NWy)iMt8BAV_R>wuP3XXc{S`BTZZdF(5v1K2b9V)Sd`!0t@_slHU|&fp3d!BKuX z^*@YDyj0Q)g*@QPJ8q@D9=nfHD<@y+-EL0sg!3Cxxr`HnVa;sK?wd)Oc6%?1tWoxQ z**})``A-g~R{nq!2gsn`kL8pnCLIC4^#Z{f-5&<(2mQl2P}IJO$z~tyId~%ON4TiW zRTJ*}_`vl-VKYJf9G+#<3F7jvSq%z4uUZWAYJO(c*n&NxZR|{ZciY(EE&QTtu3|Hs zCE93L+>Ese6iSLLR?A5VL&L$yj(`hD6udT}Up&!^whfE=YL_q8Qy{}~x3I>HEki2>2(ga2-5{%Ezs4b&Z z0LS~a!szT}jCK8yEOrh@uFScMamlA2c`^|(HfKGzm~M#%LiFm{&;Z_)LD^uG8z*KdMv zPmA$BonKSTlsR^!fR87{>xkA8n5|3%{_=_c)DB8%F~LRiS{CaP?@6&u9i+ke05|Fuu^8*k-Rm!vhIm zyy0J2=Ks`wq5E(J|2u7(P#(NAe(&sMVeH3Miq4d;*P=52R|x*E5s6R+Y4}6{gB!gs z8M?X|Bq@zG#7ThF^vH`$(yKtH5;((2KK}JD3FLn%O{HoWE8xF9nL7C3LUP>^ zbLFRhX#2knf-mq4KK(J>7?=Ft%w818!tbNNQ_@%9UrrDAfcTO>a|0#L)a?Jw^oL?J zTrjSp@rSYtnOISSL9X3Aagt7Gtz>Ez>2w;;8=UdwL$P2f{+iU(D%z zR!M`R%hZlDiH-~2Lw$<;U;GyR)d}DM$836r*MJRKR{gjmhjeBrz@BM$ZhPi^={M8M zwAU8dU+x{t@ET3B_9pX;l{+VIDQpM8w>3_WOp1j;(b>DVO`Sue+$zX76x2-dcq2^T zmN@fEx$xd*zpwGS=ghtze0OU#?L1IFT>?~m8C2!iHFwz`pI775OH6N^Dle*3C<;Nz zlz+wSbGluBc3lXPBQji0*CkIG#T%WmTKx0CXmFl+tEy%0Oi)#M{I>hu;+R^T0Gb7- zvL&HEP)+jsbaNDV)keQL(9FzzTzHR-4VeOLJXdxpAXr!!dN7HB9tWxogh(Av~JI z>Ub$kuF%bLoGqQ-CTbaj>BA)5qY1w0F)=etWV1S#)rh6DKsAF2=kcaYIr9UVnR3@* zqfbAiKKm62==}LR_^f!*=~vxo;-Fiysg^cy)Yw&_!YH!Zr$TQBI9~4-@yBiPF++DK zSy2^_<4}~_w8k~9DuA8yX|euD^s8bEBaiy*@3&wxMEJjkKXkUDv5I8Ex} z+fE$j6bSKB!~$n(m69>|tOh8X18$=TwTa(HDr%xgVeikSI7h(VRpnbb>U~qmhit5L z)1M9HrrA_`4i`-pXfD15Jl&KyX!&yMH~FNiq`w}Nx*A|tSA+%lt|3tme|`~)dTHk? zft*X-7$tewV6sTgXFrCxp4IAfxHrq;^B+=WhWIYzgfBIFKPJ{cNY)-e>vhL6#hJJ|8pLQMg^JY@^~uFZ@Ae#Sh?s6Sx=JvgO$6@=9yN51G?4bRSwi+J?2!GN15(F zV&?O2QsHM-mMeqD5I7;mF1Sc&w7giMifKE0!?nrBP+wkX{4?1?8lNUK1YO10{q1Y2 zFQOT=&M6rMw#`Nfs{;(*8=j}Bx~aum9^LiG4vSsmZ7sHX9rMS+g{ujsO5&DHGJkZ< ztIY9Q`X89&WRsa(Em}PMQvWk}hrX4jF>a}&og>sfWrZJxuAaM`h7GwKU|fi@R7f@W zmNIVZ9~-KeAqDSRu14Q{*oUyG3l?ia4F zST4NwR{nI+*cT~iv-EBgLDV`t8E9vR3}!yq=<4pR@g4U1-Q=5*`E>umQddG!2@2G) z16FddY86x|uc2e2O_c%IFdpz7bb`>}H3Az+(z*A|6Kk-)ZMSZt2J$sR(9Z^k1#`Va z<0cnPVdU}LA`zk#0|C&Fa=T@=S^t-Ik7^H|T3e6RZ?C9?-ruP8C<7`oe-{E{*tAN_ zBmpgd@*bXZ>ecDsPzy6{&$N$bh!*D>LHUDG z94-4Ho;7l}j3RyqAHQ-qXJlm&IBrM0nSplMR_=qxsz+0) zdmy!E>*OqpL6p?uS?0T0h}(q$%L7)%n1?4L)mn`@B*k4hY)hirj$Yq&-!GKF&c3bM zycidq$I4c8DKV`qax^U(_MU%7R1%qJLt1PP?3^lzGlrUF%eDrLWj`9ij2e{6rRs-w zbbmGyv!$8xP%+AUY6`3tjsxjdLiPkGV*1g&%805ejBe${L5)h0YqBxhqjMkVw%V$k zJG`AbHD^8Y>SXmqE44dtt3S+Tx1%v68(^UX;s^96aqEhV9S@XF*;qn9vdR%bkH*i4 z>~*L~)+G|mJL?x0^YBHN!U7`hMi;qG zn7!$jWY4yf<*@f}bV$)AbtBG9@D)rgHbHSRzKuV9cMhj_-IL?`=;cg7y>hYHJE!Wv zLO0pLYoR{eLay$%?H!T3T7m6grAJu4I3b{OQ5KUVezMRh`dm!^9IME*_KG5}V;lM2 z&|yvxdERNXll!{fZuJIB9*KFsJ$iiI-fp!leLoKJfpc*4rzr`e<2Q-|ouw~Yr7qU2 zraXzzB&I@p&oPrWwnSa(1VF3$&}t&dw9mHWQ1UHPsRm0T)R48Zynp0&cy8mnqTznuXD`_gT_z7bA@N zHILl-ZHFHZM7>O+Hk6#F>peHy_O81F1Kzch+8Zx9Y0If)b-p`6NL(-GR*XsbUEnoz zpvC8XaNN~EzjqyUkFMXaUXa;(5=S_mXmZpn6GL@wRY$z)iM3SwnlkY=BW@^^fMg@# zOfRQq z3o^*cp5(uuoW6;0{dhP*VL35R@3!0WJ=i_T(79a`wxhjFa3POuN!uYoObcbt+yxA>k(Xommmq>&AQ;U|?2d zMXcvM3V`t3WcpsL;$)LhQ=v)j1albKQLYubuWyytc-)c)&Zp7Gh1u_Ix__-fsErbK zYHtyE)B-x{LSD5a`y&ZNP~%vPx&c*Z(s}(mn4gDbk}FkPCZ7@%lS{nMeel2O_L`j< zIL+@Lwo22gNk@S-tQ5Evv8^{3w+eNLMMSe!*o@o^ZLyJZrzodX@XbqAI>p@?;Z3ri zUWQ#Nl?q)9YPuaApPLlt#Y-(RvxduTN6ahMmwEeSH7=cN>}2y~ z0Hn^|Zv>?VPH!$8%acmXwy~AsoH*nj>S{;jjM=U1#F^!mFNhHeIYG)4wXOXE06HM8 zp_~J8SDvn?fFHPWPb=s;q8bJv!+ZievXX1cxgI-H}^7B)W^^Xjl12p zSXS#ha!*pi7a|;6zUL+r&#wvCQ})7XJVc=10rszd9G7u=H61(2=;w{=H+nguA4+ql z{nWs&JGNhhPNg807B{z>yFM4SA|fO_E+axJA1BB>_>N;Sy%*0dxx5H`*7`#~Ns4GUb-F#mByNw3%(q1~ZQv(SeI2%BgSOe}hKt-I|h zgf@GbJyTvHBYDysav^+{d)K95$l7y=jDhfgqo1lLm_!k^`cZMJ(D^tON$Xeoan^!! zQiXbnBOX%k8&ep~`*UmFAi*R$0^r1Zv@Kk!2wduO-{5$XY{SKDQSqO%#Wi+`TF`y3 z!hOVnn=mOnDAgmL9`^dW{OP-JtCf}G z_5`!Mh>1P=X?yNVz9oyh;IqPQK@<6pNKe9cR&^$eFL?>R8n|>w2B41)>56{7xTROxJ?~Xb+Q0)0{ZEn}l##KccZ;?6Vd!nZD8 zYi;8y%KhMgkxZ1?I*%gry(wp>hYz>c!-p#E?Wb6vJ{t; z3RI%KdZcdMoYGwCBmoIhrGDl2R%`?^7G^+t)iQmYff$v)?G%LHA29hgx}y|sMDM#h z-?j9<+*BLLm}Ci`!%J^7%PCF^uJDVq$by_*FZuC#{qnWmvD42>icW@X#B1HYOZE{F zDY_Pp1nSIy5(eE>)m+K4>(HU`gj{eG9_+`3WV%CJUpClr!rfHCbM_X)42?S=`MG{cgL!P^(mZGrV`nk7zg)QEVS&c2n5^P;Mr}q{3LB&Ku#+BHseTLKjYQ+KBX9 zGF^C$^MFT_QEY8HVX3|%xJk*K>e;2ym;U}6KU{CuK;|Y$_8XYQTlNocSZjH1+!8eQ z(4x6&^D#Ep`n7|N<Sd`8KbKdaK3@X=VJ(Lp?`FRTH zttWA4PIG;8BsHm#yzz0NlqG`9rig1y#ymQfIr>q3PIbT{ABB)W>5W>qA}XPel4NTF z3Q9RJ;_=#wb42e)a4Nf@7Mg#JBe*{G7-+^8` zz?Uw`SqaV_8OI9}G`l4IlG}L`@I!;XG-$xn^g~x1AozZ$s}ft5sAs~aI|_iAombgO zVXn8Z%i|Ds2-5syZC(+9Q|r%-UR23DM|7XpW~g^QIoL?V?qC$1PI8A6$f*`PD^sf6 zwB&vvpw~G2;F-bm1Y7ey^PF)`lYF}y&@)qsa-U65vHUJj+>b(~b;kCv0sfX(sGX8| zspopm97$q^&n127VP?_X?XV7%p4 z4Af8NKf^*f&qC_n&r4MqBxTb-%?)Yuy+6Sn(A8{vua@~r+o0azT?V_!PhKAXhp#xU z9CEo>ewY2(axEQ?sD41CY4Zj$5o92TDNrsZ%QCZ{?_icl3k+dZVeC>4OOMeX0yO70 zHkWV#P1Oaherp4<)9N^kI}8E?VKYK~xzthID#KIwYDJ`(tJ&8+XXfK`4o1TprL{A4 z*`$m!b?N5?zEgMEq3snBQ^dQHj}M09;)9sC^pF}3o3USSs&CmB9>SF=lNq~bakZ~H zD2RJsT=rd>vwbGkI|{fM(jCBL;kd7{9$m9~mb-2aI-c6+zCL65Y8E+h9rk8GcmpLg zq1L_FOO7I(>`_Dj8wDx&d6C}Quq|rg`w6~x8$NF`BBZ{~f&6V`%uCS(7LpN3AZS0J zlEffea7;{~>C;TPvFd=aE@Ag_rm+at=elXFsfH0pUHIJLb{T8(-8aoSt~jq(MQ3}v z+V)9^ESpUrXa`V-hzqWqy8y=y_anID3 z+!W^FfV3&Q5Ps{?R9R&p=_yZqP|+NL$v6SW^2NDCK%ZW(NY_Cy(iD5g?J5bqILv>e zyJ6tsEmg=lb>Lm{#FA^y#A}{k=ylm5lKaGg+fwk3c6n%SW{5y;1^qm85o0eK^8KU4 zowOgQbi&WCOhA&Q?Fg)6@^bIjz`2UW&#USdR6Xaswo<$L$_9w7lMqAZINctSEJYFp zxA@%X_nQItkEMC%mBBig=QUZLCPw%{bsei}TMed%ay1wDPjMXS~8yOZ3 z;znaZVdh9Y@&9m-xjFBy{{0~hhN{hq#%j+YMxmhu22kg+fo$F?R|R_J9y4}d3AAMU zffmejkBLRv7aRT;dv6&P*S0K-CP)ZD1HlOpg1ZEFvT+U4H16*15G*9PHSWP_++7kN zG#1<;xVtpYTl?O7&Ue3LzxV%*asG6V)w9=}HKn{})i@%YsH;)Ri>nhb2_89fbsFs9 z48uuV_O7)Tt9P5nLGeXsiX5SheOtOc0RU7yeb-vpdn>aa5>arPEm8E;r48R#T%ZnAF zs%|=&AoQMp&0|@gV7(Nziu?zsnLOiSDnU{zgYRwv&yU)=&?Yaj2#XF76n#-XSg z%dG7_p-Nib@QAn}*q8Kpt&vGsJs1uba(z*B-bAJSsg^@((lep@1tN-cw3yt+mWCCd zBK-uZmD*r_!N^7&)6O{E*RW)INY0YBQvUr+f7-RAh*6JZPA)?tGTi9-_tXOZsA@FyA)%ReW3{8YPD2DuaVJ7n% zmlHemxHP+&3N5Zx@rE(9c;p0k`G0!QimsUOm>mlgyL`q)5n>VkEPc6!)l9fDsu1GO z%c8*olF1#}V=h=o##4XZCs*%DJP@!4!_J^tkpF;k^-d3% z7~`GF!%^K^BdAN?Fo>`x!mLZ~{}T{Z92OdGwYbJVm?T<;RnoErzpfkuIW&Gc)FrD<=C2@{QYdp~;LM0n2B;a%-?8|Z06-rz7%HM@HLzy+=_BWB(!yLaC+|3 zOf-XKu8%v|5>h9;!jDJ5ZZm465NN`~30S8ED7yLz8l zd^Vv?*HX~xo8(fg0lxkOGfT*NYaC%D-#0q{Y4Q{n)5j=NXQa8#oj=pu;pKN-k_49` z6&Rq&SemX+KCRF`L4n17TKo-kEWUFPu2&w7&*Vw&nhW7v=dVfJ-69mDZL#uo?oO)&BIpyj!pylE z-v~~4hYM1^QB#u$v|1fG`}Exp)p?8cQU@X3D-C%VTnE;FaBZA-+i^w1L@(NB5{$t| ztTeOAY{BPEsmE>AF=Bg3Zj|}Lf{f}teLh6vK&wxStaG+3Q7(zan@NdnsI$NO>9nnS z?zSL9O1iE#p2Uhtn3z(^7`024-aO3xyPsw~gH|i07H0N-VD4rySu||ELS&DNj$3My zF{_I~oRcf7&yVdQ2sx}?yoB@8}UP^;9tI z*nic@b|G8~iuvop*K!frL<8R*KOA+e+>BX1cL;z_+6W76C$T_A*=^NSC8qmGYX&TdV5>a><5aK`$GYic`7PL|Pw{kXz!vO~ ztM9*_bD2rEayT96If6QEU$vD>S&560PU)ce)bzSV<^*X&X;Gz$6Wv8pGhuSUbpxTA?Q7YtM~7@Mg%p>b`@^?tU~je zo>A0#dHaw^JI#EW!0?Ts$EgTQK=x@qvR~sbcbH-|U3dZQE?$)YHNeH@<=>;bsH!b` zS@S!d+2a|F%9Cv%87+6_w&uD`3OH}3ulyj|GFUP6Gkj0zGiy+ZIZ)!di`K!d(aWbF zNML2F9wJsXdGkZSH$s!T8}l{4m4L@cJ`=*KTqOr_dYgFm_n)PBsSp)#o9Sh1NUvH! z9jpNl+Die(P?XyUtJ*B)m$!G`fR($i+0^L4dqQxxSiigVu9$`0u2RjB6Api)5_#2C z%S+c9kwmZ&;W>YqkxBpN2b|n=G`B2na05wIq(-F>s`A|sdyfkI1xGlCq6HF?l*w-t zg*8AfAOm4=I}SJRQyiQkne!TD$pqqM!9x}eTJ(EL8}4#w1-;jdl}JCQc*7yCcue-S z+et2@(`CB4D7SS&9>S5x+W1AK9_uec3yeWAdeu zRga$>T=I)J=Po1;=Y>IwIjn3sKwqR*1iDX%i3+dz%&!hr?ya&i_}InH^wROQ8NF&r zFtoM2m9!MptDS>iWigQ*l>kJ_f}UC^?j5zy+p7VlD_1(*l143G)?RU_QpZRHGG!%# zQEWG|Eo9?iNPhhACz8@hc=i)F6-pE>RywluSCPD-!i1^;2j5#NqI}7# z!ow>aE++XO9WJ@-MjFRkLdvBY8@eW_7moO*th;X`F2zFd9@FQL#=4xQ82-=cznJUx zB*jSY_7v(`uNyVl2(t7(^_u zggT))0x5Go?w!vhURDKW@|DM*od?3iEk_<|BqwSJjLb8W$8T!%-)%8F=HmSzHWn*T zT89zB*K{`1#<#~|S--eOgx$D{CvR#O@(VEe%fR3Y7fd2SyP7BlZOqL0bZ&gJe6(d^ zshk4bkh7&-!S}Px^d&JBu8>Y;n9DBtP!tQLJB+!|0GHbk|7vLTO7@l+;v57>Hg zQDg}_XBrmg;(3-%_i8z-LBUg1T5k9_{MGfRKl{8$> z%nioK3m>+Nalh;sf`Z#7)Wfg%oZ7EH*_I@%cz$OOnP9Nsl!HJ<10@DbI!=pMqs0eO zLl@bZ=FflaD_snxLew{l1JsYu2X}H)J_uh-&MH)!I)j2UZv{ZtJN6w8oq2T5jMF}t z15VcLw;5xGZrZiv3ygHxgSP#{#NSwa8^66{^zr3UYN%yIInbTHuD!tykf4JaktJx4 zq!f06N06PNdx5np9{90X1yLLZ z(Z#a$sN9MNi{rJg9*5RrNWsAiJ&Jt4IM4Q8ru*^SD;#|xi1w{x8NY)D&rKtpwG{8Y zJhWwVWq%wFURxaXQZSc4D(I@@UM_Vwx6a{svC%LCqe<@@?W9ki#@divr0=OYvYgDH&-LSya2z><%C_VDV0j3Yf^x zuV1a&+`porG`%O7KO8HBKZsRtnm*)u^(PDyv*PqAZyK^c5h@$jdImKqFTz8tbIrYI zoo7!VPL>VJ-;O0iJg3}6-~)WVJu;&~NrUBaS|(PU4s zi;(7%3@%u&hBzAXYb`VMmk`P{C{DN-owZ$Mqw${CCGVAgTlKasf8UrXu|UAX-Y~$p zYh4GJ#g#%;FH>p=iaM@8U8D?e1*hhQhfp1xyA79(gc_4rY5(>5IPKIgOT*+YxVU3uca&;MAx; zN*TLfdcl>N1qt4@cG1Q0BH4kHIzQ}`Gu9wBnVq%0(CEOIzWz*7zPu_rfI;Q=&f!H~ z`%09zm_k1q$l$fBacTmB#3W+}6k-{E2{^@zt>z3Hjq^+#R&zt#KapfC;~C^9N}f!@ zzKb5IXP*pRU9@q9hEyv_sMDRtDa`@F=7Cs*t-;8Y(uh5g6&3S8##>K`(ruTgo163{ zcGS!7!brQMug1y?4S_I{*#VU3%v04O13%*ujAsd8F}jviP+^Ccz;y#^KI}^XJv5}U zwHY71vQk`klrjYUQQ8Z(Fui=gpE8!a_Q|;POiB;PyW@P`A97~qZ?NgYOJGTKB{8_G zN{8}75PgiTjwQPK5(X_WeuK`4jdANKudH7D6|@sr{Q|+7fv)sCK`MAsT@@J-@9NuF zAnVdp&(m{0n@9egg@)~7tr?>LWdH9;aosFf2Lu>fdOr1~%P>CME)zT4WBPdPTurg2 zJvj%ZS`+0FUGeY~ePFM_bWI8*oScn4xzJAtNTs8zSR=xql5VL_b+D~$P`WWS=<3r8 z_s7>(+7oabSR+b8zJ2H=9Hz%5;j%ZSYc(1TroB_XyF0p9+6Zc|@V+5 z>)Ip$_Wq0^r%&nXP@`DE;>G?&mYhrLWlWxpn(W2- zpp!G|odg`@Foqu$;czedl5B%wXDOB@IR#_JTpw1vVYeDmVdoErp5EmZHt~0!j7N)B z7oxP+dq<395_qd73gk0=($<--(EMj>`W~=TGV((AnsE`w4+PviSsEIh z5^ncZm1g*Vej4M%iKCV?Q>wpB$PY+M&TupDCruk>R^S~L%W>aswx^a3VBJe z(i2q+yVpV|iqStwi3B_`n~)so5q)Z`*WO;N#Z4TfT_|#9<$Ff(urnRf$uBmtC6ax2 z0@k4BG)D+6MT4B8s~&b!H%jteVuILa==Gp6u|D&ci)70b4cefQaaDv9b+6uwjmTK; zSIZ=du8I}~iWeNAX()o~M^KB>&y8Lesr%e~6%R@YWzNY+wuG{h&(mKD(DxW%@YqW3 zPZzz|kP9t-)?PCj#_%0l1mp6T;V%5tQLCIcbulUESXQ&ibbiHIq9?JKv=@QU-vJ)& z&fI34wuy}-gNS5t0Zs=cc+C|-)J={biI)ogmd@^+-u!D`%Q+h|-ksDiun zULfP{pNe{?^ZSjl@{T%DQu_<5yp5Ca&jNrI=fjl@)t~rwaFP)_KWSR zDBiX!Nh(Tw7yK`O1&q5$fmIA0Lp7c8zxTiNefg@(Yff;EJ@eio#sNO5ctpK&XuTS= z*b`o*diYIKFK^j7Mg-&$+*uLlxk7s8YH$)AElgx|zITt}v%`n?#cwFSDNA7GhWWCF zcg>ysCw#}^*ZR?g1cP2%Sd*aU<2&yp;MELyJzwyR;tZ8>((9Nq8VOkaeU%?inmX(? zUFE5a8Q&5(dRKlYw;3jGJ=V@l}j590JSa%Jh8(Gwbsc$P$`2hpbu zvetO5=zcfX4IF@p-Z+HfU~rLmWZAf8Innoaf!W$;L-RW7RPt4;!EQkIBtlt)6M88! zhzXlFJaWQ-{Y~=j-kG^}qq3jUyOyUYV9aZ?Mq0&2_hU+}>!|rin-5c4`j}j#X4(Tp z>`sAFo+Hy$%hFw*M_L`Vkw#A$ESYlHqV&fhBQS*2nHh~ ziY_+T&eTp;E>-q+o%|%1>c9Fjp4Y)(-)XMzolEg3zz8`b^)DrLO7ji*g~X)Nezz&b z=XBn(zQl zr0;bfeQt`JZY?j8_=`?y^pj*%ip2sTi;ozL%C}$-l~(;&zTpUp=YBvIceVgdi0A*AvnB_-*Ch%(0;hYsRnLiTa#dcRGf~l@4NR~Ih?}fa8 z`wf~#TE%h7C^ zzE?AUTZ`#+s~ZuWgaNT&m*WQ_aL<>-Az-hqan%kedNK+%*6Wl2UMR{Wh(FQG`)-f;T;LD5N zO5Bg)i#(h&ejWKym}x3++Rc?*Ms%bsOi-6?BQF=4Pru$`*a5@2@YzsibR$z0_O7IO zb}Dh(!>!!qB72~1dpj(28e;Xe$!U`f8orYC!YL`$j?Z$}#+cleNcaX;v<26{fOxm< z9#vWuKTxbyQoiSFU`1sg8sd0OzhZEIK^HC0sf#IfFU#gEP2n{zjT7K?q{J%sQsgIWW%wP&9-wn?s@+-Ti&C>Me|4A* zFK695+~MCc^kup2B}%<>Am+Q_8972+61(kq{G<)h9l;=OkvZO?se>R-Y-Jexy@zcM z(Am2(&>msxTT`Vp>}S85^Ux_-&eXM2A4kwQ5~QDZ&$@(_YmBdEuBB2}!}jx0BG&Lo zqGW@Xob&9ONS#zu)vE1AEH&Hu?{x>_KXA;_Lui9q=bHjsFTPo=g<+9BXcM>c%dv%U zj5AxB3?<^~wU;nJ`rnN3z{$q1!x<*L_w4y1q!3mpVzv|1!Ke~g6Hhz!mr2zpIN(~E zbuK+xp32vqCw3Vpa19azC(hByjtH*&Qr;zCSg4sY4M!*qNQ_aT20LdLO7$ z{TI7&sxgIUj-toYan+}6$9-~5>I_937q3Lq8Ld+oEF#Hw9ep@4^4g14@)H1W4-b=3 zjNQ&DS8o;<7CJ?pe~Lr-v(HY)5=Y1z%D1)`2ruMB>8#sTFc-@LyNE!~Ikm~aYk%z$ zMm$5m>m?)=-w6;YHPAwD-~B}CY6XJ4{7U0dU8Kz@8M}u1+yUi9B`yCl>+;=kcq|Uw z;k0(PH;b>F+Szn;H~oZYB+6Td8lZ>XAyy&*^8G|XI)6h1PSWcf?o**#dar3 zR(H}mk3-Be6a2`14b$gqJzIW_D5f$>KfaFE)`Sr3cZC)Pj zkVO*#_dkDKy!PLaQ*t+{SBJ+ob>C^GtFS5 zCdo-z1lon<#NvCp)IH3?_QhsF8%&N%0e35Vx_2ie!Q5za`~t<$Inz90?%*-}oOho4 zE10`YB!+4v_~chL@tC&Sy^iHkc$ZT}mOXR1b2p@FAQ+T!7ZE3Y2<2_1T9<4v_If}E zg7&FLLP;bfkYI%2#%SVcN7Cl9yeqxSN=4!L9xH>q!(2*8TAttm0k@BMQEuLZ}i zwubeMUZHkaz?T+zp*>OnM=}ltIjsX}nb)4r<*86b1KdLR>0N>eqem(3l()GasddJIdGj{U5>xb#T6ZHVA+?h(yu8X#JScn%3L<8}(DKS6Z3D{T^FOvYzW#vjcjv5q`Cvd~ z=rMNq65CBm`qPgN&Yc|yWJedLirFrBnyPTju8V>A#2?AewU!IdZ>Av``f_1xt<(-q zOU?~_ddy*lM~D7v{W60mD+#J;q@Y)JawpWwK<~Tc*{`aRF}z$H&fmUdbe!9>FCrOg z7+#^I)%ioefL|ed{>qHl;=XbYA1b!bE6RSp8!j|Cpx-50gFz>pKvL1RAg8IHP_2L> z7oL>4M|^(XA*%0oJZ6Rurfj>vsj0Um8<7D*Na?LBpKVf!9V~mA52}GO8k;S(QZUh6 zv^6!^3|-q=tv19iMdylq(WlRAlYJ-m|Enm%e*-=IChXEoYSxo)Tp= z777)R0e*~3nF#{;f?{^eVa^R*VaVI;T0ez4&GRIX*kj0@cgIP()=*XDMXlq$y^t-g zK9&u*BhlcvppYwdEw}BRow8J>$Np1ihlzdQOz!_I&#$!^omrC`^ogxJ^pILavGx`v z1wZ~uSM~qshSsh`7cNqGX~n`gbFVySWXeT-F{`J{>$@cV#p@{>dlfj-1J*k z3LojMYeGkn6#0$;2IlrvCO$^~SFN0%3tLAWc8;GTupeM(LP+B+$Ae&G@&IM5^Dl1j z5n_>_#w{Efr_y)&srN^Je3$7wq`MWWCs% z>vnHEZE}N6q=y8&{Wh+oHS5QZsp7;?n>l~Q`vG_RI~_(I`w1!ox1kk z{BDuO0V^#XUSvjmGWGGezcYGH%B`slW1syZU-gT2I2aelO&He^mY zU9kJv0qMCd!6++9D6XR3QF(z9Ne8td6_xdj9MW{|R^7CN^X5HY2V>9kbj+@5$5x!@*w7NB$icUi*cAx4gXz}#sb8A7bPc?e zwsl@pA<3$WY7~us>EN=A?zGbtfS8>mggilbPVo`=P~#6!m~C=87^pFQ(6=5s=Wgop z^TSYEob12|7&01AhoWt3YgEm}7m8(fKfk-47UxmU_y4eOvMEgYMmZJ1deG!Mr`f|V z{Ngqr>HqHW?`~sJx0Qbqurh+h1gggxo2y>{@IjeJoZn&xA@K1vajbt5pq@k@7&nyU z&hS|H&_g_BP_c#=V1S?`!ZV!lOYdykM_?#u5Dg!S9`*KbF#iiyijShRz)TrPG-M^7 z1mHZsyS&vOmQ8je@?c)2;C$1pXr?Hh{SG;?0=$7=ct)-3@zAM~_4uRhDapUy|GOt; zrl*1GJ*$+Q2+tGpMYXh2Nq?5)bH1@HOXV6QRev2+UZ9b9k-Z2SGa}xQuN7FB<%nC_-y->1{{5GndA1q2l#JzMe}i_p=wG4-zfhV{QsKC z_u402CCPSV459z}<$op!JgTt(@Igx3JhlF3l>alHC`AGCb5M`VnKIv>ZP`w!z$)t; z42}Q8i0sUwl?(-Gp|Cf9LIaWeXrR7fWuJ-vuX+8S5kFf3z_2e0s$u@wR-^-%s2Mc8 z&uaf+_VQ@h(6F7&9H)P1+rXg!t2fQ16-xYjCI3C5krn`qJP#J+{hw_NNx-VXCYJ=6 z|9v3-y?of?05C?)o<*d8wkbCuJqM|mUv66duf6-95d)I(DZqH+L+>~KdrSWf2=EvH zqZX|!ajN4{3jg;w|6acTY~g>l@IP4iZ*=%?`t|=OEQ~X5c6-VIAJ88_Al<CHd=i7g;lmj+p&-|Ve`pPe0Z5kk zsRPcRq*at66==&X9%}oODm_IK2iWP0{%)Taf|7FNIw7ypxdVn3$`m;e9@ojcMTpf*co!PyayM;ca)?_@sutaU7XcehAn}Pf(hL)-A5)DaBJ&e1iOeQGU z)!JOT;96Eb0=Tlg)v{O z6k^SNU8`OrnG$E^)zsrm3C0^KlUn?pVbDngFl#pqtZwX4vDznbwpO&Gs!W!|>V>&y z7gb}=R9coowOabo2?pcyzW-%8RTP!-S*VW54iCbC3X%v(N&7#_&i{xF^M7ZVftZ$Z z^FjuTQa6zVagu`=>Pd!F1u)oH^x8r7Hj8igu`5r=>hjIfpAn;iJc$z(exDk{*N;5P z>MfG$mbFr4upi2AGCgNu7}ZPXwPO=Se$2(XHC78k8a*it!iU?%Y`^pJ4c3@8+tB&1 zvReg|%IXicI5HTu_byijGzatzT;5VtJ&=Jqq*7Q&M%wS&r2K%u*Ceo^&zaI(pfx$3 zZ^EIPlpdAWnUq?-}SC+7bGVI`w}ja(;$LoE1fu0 zix`H?kM*VX6*RW?2MPpOU)+raYjIqE7XY$fA=f5FK4U3KH8h$mDS#ODzrwRdAetPL z7bxUEXyaG((xC1mro0gQoe&08^v=z}p$N_eDbrKTRKR6hXVgRYHsdt}BJjgxJZ!vq2 z4G3X){T7Ar8R1(DF|OZ1`24A}U}$#O)(RAT@G%7BG*R^7bUj{4GARE$Jek+gsMA{C z@7i>>+10w!dBd^NOw!ML{*pDCKiFHz3lzdAA^baLtm=qv70*?>ww%41ELH#VrI3i* zCfUTq1JYYGtEA+w7eO%mHarIpQOjIv$OLMx_F<;W!*f#*y5hF zSKczbI2}1zsxc6Zio0M86d#LyxW8?=b^GT3+(1t+v-N5<{L&?7I$HQfT2c6VUA%8# z?c=a+zRbM7_tdp3h%uT0xy`(rB|b271L=1Je6B_qprAQhZPMdaoS4br&0Va`GExU$ z^1ek!)Ty(AOq6Pjl!arC3akXOc| zt)05}JF)wW8?b-Sq^MwRf^-z3wiG?qV81M`F0B|f6kz!9{)v6Zy<1ju$HkO(wdFWX zmhncD@w~6~Wz;EDG8Rq)R;PMN^!xr65h64RxbtJFkl~hwGV;3as%1f$wQ6IYVGv)D zhKFPr>G~|j5*&CmGq|n$=z^J z*VotKP+8fqjCMwT4Yf#OE|d9s+gxYC>xzj&TnGV#%xn8iF{n(d(`K>RHD1{F21$^h zziVf_P!0r2)DFgcJF)VmFw?6~8ahCvIi=>(!d8e~{6@Ku?s6ApAhn#qfG{OTj$T zv!KR%kiE|XOQ5R(NCYQqW>zqAaysJ}q|Uxn!{D}*u`w#Ufcu+H5+F+z)51C8Ik&z( zgN3pU`zYjnnV?x?)}w>A0}H9>cxYB?Q{>;FGVgFZm~+~%>bprtGp}~bLW5cKJuBV6 z&5p3cb*wsvh9$=!{JuAzSp!;iTd3IIyrE&ZACqI18A|2KXdX=AlyU+80`l8t+ygoM zWh?aB)&azixxbueweW3m*`X{hE`B<~V>|cm*NHTy-)TRs@cSn~N*xyUQV>vRnn0ij z07bpk+1}D%@9u0QdOZ=cs|A?(N}4>u|2MV6tc!@qlVIW z;C!2l*H>%Nrq`z%W0i*Qk5-E8ty`?e;9^XLGv&HW10Q()@8O>!t;$nS^NEPoIm1gn zR%zX%v9|s!w8{dq7L53L9s+f2C3>UD_4gS(XmwdHd-7ex4iB43fHh4BW4?3jB`v@Gv1Pz_;B(xCLfu?b{sO$zU*FUSQ8j4Q1;jD=`oKN7o3q;9mjQ=pW^QfD>kv|H_W?{$4Tz`D^xpnqPh zz1;5WdwK8ff=RFa>5*^oXsN9QprLPn78{X!uSN3sl#fn6bKTDMzd7o7G#L?syI}2A z_5u}uuSuPiBrV_1>&E+%|Fu)!(~i+qVifQA^mkHqLa$sW8wTZ4ADOFG*fj@5e+k`Q z?(mPj;N46%7T+Y|bujpG2Gr_}=eC^-9>g8)#~|VUF>l{-5U;*9`{LEzC?E8lwAt3u zSorxIknZzFhkPgl$~-%KaPEse@Wt?B)dK$n#_xw-<<*ZOkc10Hr)@##&~Qc-XD!Rj z+g%24eV-GFVx-i-#jm`5h4Lw@@oh5kG-E&slPu$<49;QRZ!L^#y^&|L(VJ12PZ!$X zPFwo>l7DvsM7x2q_lv)ue?YYR{vD|p#5F|kc$RpO4|ckeK;8{mZ)*nD$LVILaG>bJ zR}-KH$XJcJ(vhWkOA*H%tF#P#M&@5ft!Gh6qjO&w--vzl4;Vk`BR_H$_x9LP?mU&A zOH+)m?`X#|;-lW@pig9!{$pU#>~o|k`vrypkZsKFoef|7_{aXH@ouV^=bb*I3P8E{rj!j{(wdsIOJFtSVFx*te*XO=)4$k8n!j*^muoc} zwe1fY^Y5vwN2cVu^z`L3P77R&>Zw-=S2c&c1)EZP{2{F2paRL*6iNyTWfcz1o33WhG2h*t7fi{lk^E=rOP$HrtRDZ)p()Z z&-M1L`G8TaM?1fm^>@^Jo>1O-6RB-_U4oG-7U+We%VLUmT$8=h1twcPDRS+L-&01gCgQ5ouEJ5ckZdV)z~3 zvAf?%C)p`XmoHP6PT9r@)|41O{2Z2-slf3U0_yK|F)(&9{@&H|^$N^JdGGCYeqwrh znK6o*0w9r?fU*t3fdA4T+~*qjr`Ybzg$!YAg1IC-wovvhrPvx!Ufu(_^8&MX6c)G!SXI8!Bb%>B;4Yj4Vu2*gaYwVr7<+(KUU zCiVo0R*Y}A*}^8DsMnpp>o3xhumUM1t*?J)D-Ww+0w67IEBeEOv%!!ZP*O%?P)F?@ zx^oe~cYZUvBuxAh_BKr2%Jys9#5?Sba~)@|3hFcp^%6C%fReDKfJg)N;Y*>m{eYtOZ~2U0L^qffLCinawPJ1S?_P}&mG`myAH36{tajUepI#ttg^7k zOp^cYll<5Jt^kkMDS;vRKR}bc063r^Btkrr{{YM}0R?!2wOjc2y#r8JvS=83RdmF9p7|vL^{jad`P~oc4(96O zzj?o=0i^CM9`C>A-4}hcm~^h9ReP)YF5@2xGjKk5ZaAK7Who`qjPNbDT^w}`Lx9>3 zEw|LO$-?)Ho;w?k!}+dzX0V{IN31gJIzfNVR@$kRczt7|Ke;dq5(|%JIe#8sD35#J zytNd2dS#5&UtE~3L#HHz=>uPcbC@3rZjFth2SV~#h$(<--TE1F@L6wrzTF~WL%g%; zKxEym z;5lY9u$&aV`(vNKLq&C-mHJ(s&}NCMm)^sZ&{%nuF!}P*Z073~+EY%-#@DQXsu!4d zH6!iHD}n0NzEYFLs!}xV7tKJSMAf*sI9V<9=|8yB69iLA?4+&|a{u&=Aa=t$xv}gn zvcoE0xGlZ<*x35b3J+f&;3|L9@o?Ef`qRpvCwdWF`OALPvjUVXsF3z97W4I-93aH( z?-+GhRm#({=~sIc1Gfuw`D^WIf#O-Q=jZ1!raKdcKG}+Sacg7ym4Wm{N#vr*LZyMSj*HgsZEa>MM+HZePW#CHWe0pOW6O21 z5{bDe(?}R6?s@-orQ^NNc1WPa%(VX7B2XuTUV2Rvi>G&QfR|z$@5}@;YXD>LzUq3~B|H!08 z6@;SC0u&>1bF0^J+~7JLeH3;)o58@)TJ)VhaO&MDoPGaP-#?XW-ZPWSdio}P zdL=Ac_%wUu-iM>|PqcvOQLvLCUoN_nXIbfs>`Q94c}0$}A23f$pA+v+A6aY-%%3sP z()J8WVr_W9%`aCY?+}IONkz%E^Iz)301P#AhYv)hD=m@o(PS#{jBlmI??8 zwnVT-1RCD0b^AZWw%;paV?2SlswBy=v~d_@%N~`9HNJTtIxt~Lj#L)5$>;owFDl2s zK<22OwPDGv@X_${4+MJhRD*&gNWIOAY!h9fMk%N5-~dW8I?8-g;7y;@BN5)V&S?H1mxg2okKTQY=7@jN+v6ACH`qGmUl+hn z!F*e-Xg*#XzIXkFcJ}lcF20=~ADj|V4?VWR$N2K~d=D_De+>)RamCkU9UZ!JIy1f= zEv&d}@%Z9wbDCi3wA2)|a2CK_kecJky`$Xp#lDRO;DwHubv(|X$uez?PY0PId)EYd zan==0vMMSnfS7)r>#RJR*Od`JsLZD@#|SkY@HP29)WCOpW5bW7_U#!)dd2jz#{ggX zBcLXeIc?_%E!@Z16ooeou0;z-_6n+Fg9Ubf2s=46ykE>+_O`Ef*;(jcgxgtU?;_(m z$jJ{zhmeg|RJd%rRHkj5jda8SvBlPpjP;LoJ|<=gHvv{yx(q^^0|c}M(V zJjWIlMP98ohuHET7`mQ)CK*z0*)s(q;Z6?*kJj(ug*WG*RG{ntXZle~ioT4BO57ES zY%?iovs7I9Cjtgb2kTAuyNGg?kgwKuxjpR+R8lGNH`WRE8~d z{sA-k@#&`Cewu^VQ!~(qtt>51kZStl8EbC*1d7({%Dpa>YyI8xyXyFQ>k6&|YJ987 zZgFIXKZOg15YdH8=xKeFVnI3MFJ0eBywm=^@%3+(M+33GQ4TfR>B?uDkNf+qOG2$9 zLYm)u3lucpINK~Vj+)aM{TOUfvRi8HnboSbs7h-y8%jNN^R!{ote$8)AiLOWnO}-* zPH}B(srK>g($QYR4M5OUNaH>+W5->sxSTad`4bnE0~+@<0W%NFbw>j&P#;_%9e+Yk z&>N^rSI9MAYsow!!Xt@kGd_wzrI7UiabY`?I_P-xv(q~eJHM;B#!YHLx##BK+`D>x zvRJ0uQqPJ0K?8fj<=Qk!I{Gm5%ItK0Z>Flv8Aub4xTt5)g&^XWlsJ3%6s6W_U<&WK z>!dgag4cLQI9i~!7f`U*Y-#+ke%Rl1Yrh)Bd??kJCu7=d2tj`@{>$q>M5-beg!a+T zydHM|S8LkJj5P{+-)yxcVH5q<;Myu3PZ@bXS@1KC#F`f2obTC-Qb$+vQ_)v6vgqtA%6WLMzE*(EDF)(&rdq=DMcsZYPXzPM3{%DXW zbe)nSs!OX5FW4FHaB-in6;>V_Q(5%o{$0TbL|n!g3!c@Ti^^{=0>xO{1QqqXEYqPn zO)yU+jD>BJm1!M;iLKaXmwx>wyDZrjrAxb6*~`U_NWt^t zua{~+H8A*R&pWq6H;y_8=5)8z_h#nKPp!sm?D_J%mE61C?LSH*n!}fPU^}CE1eRFU zDCAy$wX+FzpYMf|o2RHhzWB67OV0T)#fD>J$mzxwhhtB3PQpNo_!2nSN$LShb^3~R}XZ-^4ORia4iSvip z_hWb$KpBFK@vpL+J5~Xq@ZFvD=_OJ3W!IJkulbDO_GfV}PM2?^wwd>)E0VB;ucTd~ z7!`;``D>-z%>^bz3e$aZBgh|9w~zT0LN9jTZe=>x}J556*`|7k=9ryKVrdJqm)0u9V4){se8;b{cINM!WZGj4P=)~`SF2a|8T2E%f zSA4z#P!uTsK2rsUTExne~0>qD9s>&$=$kBeB=3 z@SD9gUj*Ku5s_)l$l=S>F+Gu`Op(hQ>vG^u2S1$$7G+w6-D2};FK`|Ad;Cqy(*liN zh04N34UE$WV%Lh6tmI_6ujGXEdsAgeN+P!ld=K+S9W6j1g$R^1{sp#$hQb*~Kmi!V z61J)NTwYnI-*IXE~YThAANK z*|Rh*Yla&6CqFSG&o{j{cWCp=%;^f?r1O6$C?7?2FcBxrNZ5Y4q&!^rIqlNZqS#1a zl%O{qT3!2XPCJQ~-lJ`dI>VQo3*Yes;63I!GwdK8d<(@m;oCTvO-R-HIUvKi^TGp{ zww$UqIt))0`P9RP`cZX ztwi81dnsrg9NpUZKh$-mmCej+=z2CSxF6|0)pZH!fiGo%EyQn4l%gvD^#XR~$cpE* zZMu2lwfjX1?KP_#a??MM?OM^m6cS5~R*@Xx?%hdi6To^K7njNqNx~Hecw>Su!2Hud z{eaS;X$EK8uy_YNY2-g_tlDk{=@4M^`Dq=X_W(jk=4ks2UC5JEyt-r?Tw`|f*J-~aE8@f#z5 zFi3LF$=>Izz1Ey_&E0hodr9Ok-U_VI{&tI#~hQT<=c8wSnb))1&PJL0%69N0NVbGpW5%blzfP#uQ=Eoo3V+4ST z|4&Y+5H$!;OvwOGt%zwbnQGvY_g>jaXcdn&J*Fc2Jr^kIARv4HJ}x1FnY~9*>j9S` ztn|L})2B~OB;7*EcDFwCrGYszlN*(51YMHLxMm(mrg!f1I{r7c0ED?~mu;&G{pdwwtD=S+Gj3P}nR^4jyGGLSOiqsu+-3Pj)&y;#jy-%RUf-W9q)bZEPH`)MX6K1=` zY`@j>%o?XMMZmTaBG1Pc8FLl+s+b~dVZZe*9rijKK#OpcXb_WkpsE-5?i+ zo!_CaU#u}{aa%g67P{<>vy`>yPX#*ux`{Q9zWp|Y($8qCs&ANn=1$U{Jt%`&(+NB# z;!Rr6`;A<=Nc2LrgFiee37%MU zs~5&c;l zQf$o$@6)5syZ6`NNcRwLqM z=B22tYI)FkLGAXxXFU61NNcNUO`v>KnOX7!Lsr}TcB^#3-bb5H2LA(!>b$s!tJ{Dd zrVE0^Bue--#`{k}qEID{5DqC8?|(xBm-MBR*>}B*FaDX$kPkplL~muFmotUxdor>h(BxlCONatdTz`zTYExMi0~rha zpXZ9k3}{Vw<{J%cL(b_>-8@KyG*TMQ1jHYJlQGbnJ zpI&W=EAA9FUYf7`yuKtZR`Xe!fE_c%CP>NNUg?ccu_Kj26~}YqfbHgky-{_KJdZ*Q zuuyN`EPpWm-wz(3-4S#dOcIg8-wozE<*^N+uX88RFa0_IuX$!=x8N^D-JwL~&&alP z-sN$t0S=&%$&Cv^Lrq=TGK6`7ErOPFPJI*Q?wfX?o{91)t$O?B0O4&$paodT08Y|Z z=RR)l_cKI3b9Anm@qS^Z|BI@!+m$~FsLcYkZHBTC;;Zk{-vYFKW=hZt6u412IqJ#J zkJ?tvZAMKz?HPAbP^s}^P$d2MDrUy{iV^6nbYz8{=|>?%ExDbJJE3G_1C}EzW;ePf zEmE+c8JCo_>9Nfc9Tl6e9ERV+*r=&j-owrYmSSUS&k*LpeISTxa+3$5&!ruIw;Z!D zG+SAZ-WXNJI5qj?oX!(AH8mBqG4;0kN%duj?B8c&lm8b!-9Oa09F64c>B$K$mvcvD ziuv?9RI2l1Gf_Il@nmaM;cUymqnqRCL=Z)FQ`mV9 z{P<+KC^stR_+ZoDWz`wnUO5+bQShy4a6OQKgb$@ER2;Cs`={E>eob&3TnS7hd;4iy zk=z4YSLJrmIy-jKtwuVDa;3bR#&mCRI@gT7B3q`*o?QG?*_O39)?(hy(p7<%p+#6#4l2CkDcz0b4j`4Z zo+f1y(+AcDi^XN}8i$$qEbuAe4wre1Gi-UeHG2E{;t^TFhGqeFtwS3; z1GPx_*Y~I&@#^CD!8=p^h}JoF#g<72&_EWa?A!Sj+!grpbOHX+u|6wQY^qgAGVx@m zNtOTI2892n57G(S%LWPB@#viZpS~7a`6bT;2t6D&Jj{G-`VY;5PVMw^mT{zmQuwW> zoh85=@<9F}VX{9H&=(!=j;7XlpA{^b89l^r9R;W6gmLDhu9=b<(7@hT5~@_)3ELho zv!ia>f{|NSN~HI#CY#?_9sN$NBb_M<^b1r4ylUb*lK4sHO1e?~v^k}1 zz45Ie{53TZ$ou;!V0r<0M_XB~h*+A6yby6lQ zna&0{$D=UHnU=zzLKOP~g8e7~?5yYBatjU3L8YTFKs{NSCZO#9wD3uj5ly6C82^fu zcsM z?UyTOR3ayVQNptl6%0e|yEQGrYXERNSD)N+GR?AeAjB$qG9)gfM=4gud?}w46mXI` zldBxYyKLq+Na1eBIP_TN#x_}r1f(tXu{NdH81^`B{v=XpA&KsL+tNfWb}_sMR6wd< zmA8Ku3E6E1`;VRZ^ejRty)cbVGpzci60^6`3cUjR&{fTAn|-sb&6+Up5r=xx1XaR8 zVbzM}y%&Jo*p0GW@M6tAsredFZyjhh?g3tLv>?*SyIVCJXQ#Emb|+Y4I>j<9jbFj* zgb~{m@}*}D>hW!rP6UBw+Ey%p{^Do!)BySI{Tdnc7RJlDI|;AG|5JWlV$BXtIQ<## zVlA)cT=nRIWQDc;)S3F0Dpl!q&Lq?upjTz9av!xW{`i>gY;7Q2RJNs6Ho(!#b1AYt9%3!S}*HhL2A=&qumb$|d|$OKJ~1{|U$bIONY?+=J|Pga^VP2a+K?8;>J z2T0ou9(+5K?}MEnd8O0q^m~9^xAS(`Ce8W;fh!wiPw^kfJC!IL2 zA6{s8bu~&%UUVD?$t*$!G$m#;ji?w<79-N8>1U|NtdnU~CuPp)9YMHBU@=%t$a8`6 zv5AFB5I^)t)MxYO+AS#@h0jSF*x83r*Ixr{vr+93zA`i*VEH9o5(s%anSg$)v@Ect z!5zufHBhx)7SmY*sE%3=#6iQUk_!={SO{gQcnL~TElb}VtV{xt`f-+e-L56TItVk( zKY69G7C=n(hFXONC9saS&Ma`VA;eTmu8%yivCPl_~c;Y%$c3f-5dGYOj?r17I6|=t(&A< z$nd$XpxrvQ1HxR)yqlWH* zc>el4`p_y@bvH-0!_Q*O0lf~+Fyoqc_%eliaSY!Z?ovN|8F-YaKX--#9S@4^?{QY1 zQ{X;BV4(^o|N4x1{7Y;MrliU4E8|X^^Q4G_C-}=D-K=7jm+$00PwPFG)}!E^VU_`_ zc(vic{ddSfN&Xp+{9i`14HMu^*QP>9Q0F$;Qh7Lu=JQmMcubM`5LkGnfg~uLlHo4B z#V>Vv^v(mH`NyAz&9(VEFzZ(8eKV+5MDTm+B6_#c1NX65D0wEicXo-PBo>V;{9&DJ z$~<18N0Brjfx~_Yo<&hHzNNE`I|&MmPS|N799Mj=xKp1a=$Xa2{U%ot+3ph|k?C}X zSXRG0AJ3NIT6UlHIEnxsDB+Cgf{}f3s!|GG>woNFZ3lxWk}}OsO<{1k-RO*Y^L=}1 z{ji^D_nLeFz343Osi??N-g1VhgS4nln*9~WP0&0Zr-&EMmKfY>)mRujop(Yi%;X21 z9alg|%Fndj-8&x#a;NhvV8-D`&>H@qQ)e+1n1qv(;Mqg;a!*_%=a9&nQ@a4`u7kE4 z;{L}KBe1&J-;8F+(&hdvJgAuRjNChz@%mvO6F%iVTp1((%VO2Nbqf%WBtok_!mDj= zMyORGA^e}+msJ~k@*2$(zeh40LAdKGIf>0c{yap-78SB?j@c!{Z?Xs%Qrp*&=4*uc z)jU!0P>lUfkS}=x+gNpz&6}zS?xtS0h@eB5$P4}rjZX~Dt*TjqvRO)e6b#IcmyJDPi6_SI>Rf9?#>*@_L5YNl7Z<-YYE^*fWQz4x+8I?={=H>Z2x zdg*vZ;)17CC(pa=?$#a#`P+W@t~dU>wim`owmo4EXByy{av|S{hl$&9b@ggDgS#`a zN3#pY<`fAgZr`qg-Wdu*(**%Qq^r3Co7&ZFJYNV2yeo=E;$nRc(Vtotzt3c1Kb60u zs9A5D-iT=vwNc2Tn3xRcG&A0*Z8VV@!v-=vs=NcAPZVTyep_!BT?A28m!R0QrD_6_ zX?{+4eX>Nl5V@FZ*~QO5TGvC%8AeLw-smN(-&I?DvUB&1_d?tB&y*eTJ_1Y9Kjh_G z*)@Ad0wWqYfYS6$sr`_-Q@Kl2+GQ!>i4UVX)&{2b zkW3LDJQU^p5GjgqtblE_%}h?bQ6?gpcJHJz@?#x9AP4XJ7Lb2hEs3K=<)bqf+?BEp-t=1oFN)x-|u zMk+xNup)m);YRA?Xr=C`wk~d9v&KPa@>&x>VdTk2SJd>O>qZktcAz}L&bJtM@+I!o zv1L$Nj%FDy1mojP>tO~_P z0-5f6WbQj6gqe$pqBq?0HbjafV4k=UB60vexsmva#^Hh*(iyDF z7^uT!R}IgSG;GC=SvG}enNqJ;^`x7JJ5K9J$yExWM7$b$jP=}5PJNdUUVz%(x#2BE zVA%H^vf3QkJx7?XvD4l}?fq?7E?5WwD#bmv{J0@eB*U`(Er0uoUoXE9vll>U9A6PQ zAR$DK_wg^VMbm5^6n2RTT)+aB&caaZFR{NK7LV{!?whK zw4)zbc)>WPZ#`xleg|3M-VRJ|hAwGGI$k$m8-ir((QSsRDf+14;ULh~S>JvejSVSHZHN!H??G6>Dp5*Li;;_@LB%r{WCYbm?VaD7J{aG0xS=L$4?8 ze`dZr9sU?^jJ_{U#gW?Mtc6`>Gr0%OUbRwv3VjD{SbpD%p`?7X0 zR1OYs&L4^yWU*;)bex9;jQiCCPhf3>exzDmo8|Izq4m5F5{j4p!QPX|*2-yczZmJn z&rT3nJ%X=rY9~93WIZ&{1?e`!ryBOkKNAN-lX%v38&>5>j5*f8<@{ZY&4ep)(AS}+ z%b9G$3=colnW6*tdzFV$AV{hRlJ^3(`R4O^9_pF?y=3#UpMo8$He%%=HR*#@y#U_g z?DBY9prDjO61%mmHYAlkVl)#NIR10&9v@Ripvwj}fy9T&&o}SH!*KtZd%2nVKX}sz zmTV7Qu0uroJ)Vt;BWb(2k~gPE*z2j1R%#umP|6KvF0AuCDSU%Q+gY_m+k$lJK^Ccx zXw@*9Ib@GStl(mPw@Tq9)!@`2eW3+$af68$jO~al%}*>Z_yue=7Cc|Q6ED&$yZqJG zCRA6`K1Es<#INu&Oc&3^b?o*V z&3CX9n8HxD7o<^#P)2%TwoaMK=r0H#BrKEDfG0X|FT!+dK|@@gl4BPsh&BUEj#pKu zWfc%mqZV%{cyea{i^R>v=Wa8i?Cc{)7s{DFK@|yeMQ`fS*gTbqxU5W$%*?jj>5zYn=peSOYh7I;Mu5FZ;XFnAnMG{`IU08;5v9dy-p?l zgOQH((9DJK!TRKc1PfZnAJxg>8TfuNxdAik4d@(0cy(<2aye+%+@(j67|{xso8~_5 zpEibJK9cLK(wWnvh;mvzK3V(CQ~5{YT6Zf*4pHsOD<3$+g(ch$Ky`y5LMzRjvrArw zHbzDze6MxzyzYC7-D3D_qtZFJke@Z26&`!`l@mS->A(XbT(48%MQ-_zBI=U1#kZ2s zd29~%h;(8>Aa)St`je@uCWK=k*DvC-o}DDrrH#PXh z`fHuiC!8ypP|&)p=F7R1@?&?LMA;05sXf7c!!+}r`O*~gwyzf`iTBsoBevrj>eVLB z+VOTn4Uo*l!>!x0-^!PP-lH9}7vbW~qy&z(yA`XFOJ$-(7mWjh?O_%AmHStKx%ggd=&M7t;N+Nc$aIWsK|q6EO0*6dD>ZQcBf zSJO|yx*KkRv|>ETZBmZW?BK}2@s7Aa{5rOnOUZDbHXOX0=`);;2E7Y9Vml*zJE)&eu4)oEz>BCO zsciih3f9i_4+|@;Nr2}z)YPFxv|^Gj(^l;PO2XPT3bqX>3z;4xcx&WS<~2v2hYVHC z$D4g~&pN&ohp&xLQe)>&UIiTK;_iw0tI&1Bz%hHods6SSHP;B{b+eWHYa_F$47@hS zE*q%$`{SxwUa|QwsqCRSE+tmpN1(gl@zGKSMWFD(Yk5p7QDSZFsokslaW@!!;Oj)A zg2b2gXfrr3-FZ8j`@^iT>nWHD#IZBqZS;LeUtRYHwO8dKl@1jxBey#AqDRSk1*a$SJ0)l;3zl|c4h%36LOh< zQJdb>PTxFpusQD02(RS1g#JwGSnhb`_ur39UUlIU8g4ymPZ3EB;-l^j z-l61bUY%&k)66BgC>29of^5DjAwsYkooMCFeZPF@-u~hfgJXCp<)vleku>7Fwbk0V+K{XsBEa#8ex_gpNGl*_V z#x<${3eg-1(-jA0zH3mmtcs1S78j4TXXIY7K8eq_p#>f$rj~2fFf-+8WrwwXROTIL zqqihD&x(hc3x$ugSg2#FdKkv4yV3PV^(8wMjchUCVMp5wgDnKvJ_jN$l5-Per-;FH z)a?nBvI}qj+W5S)pj}YCXr)8-$X;RSAOAbs`;H?2{(e!MKCQba{QhD+%YEjW`Tgt$3I$@Ra*93%bURw7)+#lI3_&4Eu6(};f7D-F%#3`Qpfeog>H_qj(<)-FU zKVrEG^D5zH9&ZePmRW>)F;ra^43$AqKh(&#zFKs7_VVIcwCHaA?Z}!}lr$rL8vGl( z#f9HnEhIZ`sAQaWMb{1hJ6Sz%(j#s(ZU}gjN@xA~_8jVE)A~|r;ATPRJ3`8bDF)!~ zD8`*PqMh}s&7ny`>wM0q$enPvbhacjD3emK5Yy6`b~xqstZcLdrmn?F=uSWtvoLJJ zrM``5h;-gurK(TLFZk+aUhjNfX>=8dzmsBb~ZZ;J3>Yz z0rwSem6PCTzf@iq3)BuE_nz2~b?xLxbB5aPj^%yg891Gl_++msWb zGKuc5wIU~Wf&|%=otUi$DdanPDeyA9ex4LH`b;F6xrVSW_IoiEMq7#_!9& z+dn$DmHV;Mrzi`j60p9=Hn)%K>?>HqhLT)6aM+r3bC#SNUbq%5DfnGtV(JPRx&Yvu zlDhsg_o^YLOEa3xY1jrV>?r>Wg#j_S&SUw+@kghg3P-_qrj$B23@Wgi`<`L%l7d4SoI==y+a0gdJFh)1ZTFLH>g4 zq~5BPZJ4A0Ye|0W&YW`4*|rZzIO$IGc%JN}BfSt#fNi*-FA^j2DcR$&|6*ZJI8)#j z(d>Mn%Ll%#LeS98TG%@GsJD6AVywCFQC;EJ;=-(-jx)hP^MXsu#uXnhrYSjt2I*NZ znptNQ{xk64;0oIiwYoS9eHmbbQF{vg$wd7lfZKwKys_hwkP>WyQ6faFUW@I(*ix_c zu)T4?gKdqf(O%SZb4*(;p>e@)XhXn#Vv4SJHQ9XSN-@mLsChFr$QPdA9LnSUBLP!Y z(~aWeY@CspZu;8`z|?!{nHuv!J3gr|ft~4dr>3dq6AhO#S0o(=gtDAobcLhoFs?My zQB+*3U0yUjke8C~PBQa7sRO(1&?qu6E-7K9Lz6Ug~L0vo<2TaDlmikf8H+Kw>bku{WcH!yD}lvkW?V>oB%Ui2+nktb1zqy z`H$e#^3qEk<>V!F#U3QLcm4|{8QT28a26Fh&9LQpcGoq=+Rg$n3f#fojj7tBBr|aq z^#Z*0wpXHT?Igros1x1*Do*;Z0hrY8**n6R3{0#4ul&$E z{5fkSHSl$06Cz{dN@LxE`uv5&fx<4bA&J6lxHMRy z?P~cumu{{^tjsr1F1F2shsvLPC0gcxSnMRewt*lw72P^^$crGOJT8oDP`0=K@G)?a zy$L{ZGq9g2?*;tl&c<11?iV z63T@WM7QGaB4S83(ZB-ll=Va^qSb?G^XNQi4gDu*MM*z@e%Q$Y)NhQG>r&ympD&gs zjMb0@PiDspS$J*Q5e|S_Ph+p^n+eFZ=BixMd9$x}jXizhGHRYiGC3%_u}AFwcOYdc z>3WLpQX5Bq;mkYWhsLo&&q~PD`V*OgQmpm3X4bE;!{)^)R6AHSs(Ouqs(3dou>L5f z(f1cjW88D-L8?d?S~iQs&QqWRVeb1avwzA4)-jq2!U#bv1He}|!i(H0#SvGyY9!vE zW&MFBgA}TLP%6#zeC@NtR)0v>g3#l~QoK2I>){Ic71K=3z7b0mKpoN&+>m`DY92RX z9B$JlYFRex)~$f`tDp0HAk7c}+a=8iaEFUq-x;s^Fi>VEVs*(XVDm8Scfz~ z5YKgq=G3cYRLI(#RgN^T^A=mmR??b2sK`T+V{M)}n)vrvY)scj)M^!MZ|5hkV|zPU zHHt%Kzh7jXgIk6jOOATZRtH9G>8`%y$LHncr8Et^ji59H524ZfB+Unb9$(11d;JiU zo`hhG=*jdTQB)>McRveFmFKxLHbPWdNnw<%fO0~{Y#lY(M<2Cs=4~9Cfe#l8189AQ z14K{vK9LW`cTtLc#Xs?B&)fclN(DjuOw9cKLisG)No<^8SiNw}cE5N~Hz^=fa16%J z39ZP~x^E_%2U+oWt&xirDT73QeYFA|B5^U#^{nC?u^Oe zs-2zgACi{w3+Kyls0wQ64)l+gd6-VM;;gGsoG}(_UmoH+v)a5aH(?j{P9Jn*i_Cp% zR$}Ph`t3!(;mpW5H<(u%QsvF)OH4_nS)cm|5Kz$#! zx()zR41fuVm!yGz@+a(?VWTs&%&=upB#-Id_AtLomh_Bh)7}Rf&VgRbjdOk3Q^UH# zfp*p^gKnauR?Y~T-kU4Zbu&gYezAtcnuMBQK^Hv^r*iAf6Q|r%6(cq^I95XwB?7fM zNUN`zsC!>wR@g~0L#SM=v>bTtb?dNYriU_0zeSG=^cj?M#oASRAzBolLZU|qt$aGC zwGb~lt1@Z^7Qm8mylo!LI*&~sUCj>kGbn$}CM2%5!qFgIH?8}?q*q$^>7`&T)Ar{% z1_Q_8B-H7Gc3b;xk4kQ5qLfgjA-;j?|6+naV~_PsWSfWY|jXMZg{SHLSp$ zN9G#zD02Lhux$sIUYH=6HxIZq4k)r@$jy4S>U8wuu(Z$Zj<}w>EqD06kOto9MIi)gOO4jRu_0eSy-qf43|F+;mrUicx63M z#*v@UP8;7H-u+TV{;v9CVtIK)mwD;}-8Z*;JOqn?u%E+({jbT@cud@O>>c&!knMEH zEI+4e$SiVo*m+wyx2URjcpPstG-!UITs9m7IOse#fIs8Z7Dt!eGg!qszdNu<403JT z+H;67*-&Mh85dFc%fb9>9H7WIu&3Dkw)g>pJaQCOY#0ucRvS zpKtxELFj5JfhgW(fu&ig#1Q(qEG}t#wre)xMlhfOUaA}@di-~J`FA&w8yBE33<_*c zRP0b9)K!}zDp|rvJgwqzPLKNVANlXUTPLoz6L|IWLUC*uTE+@}BgH=0lX^$3({q2? zg@)X<|5inRt$)RMy@)zTKCSM`Timmfn+)*exeFtczHKky_e9t=-F?=223t+z5>hm_ zkpJ||5Rk8<^i+>qlKxSb8(gF?@UpF~teY(@28jCbM~4V0Fh?0Y5M%FHu@J3`{{s6E ztwL0KI?c`{VCY`!^COC}DbDQ8`^i(^zZAgPUkQ#|bhQ)taVMHD#A2!9cToqp?{?xTyi z#&vjBy}eE%7pp0>fl$3wRN;hJ3v<$`O_@f=` zmH(J$6umsxh4cHyr5@JXb*c1Bx2g&{^?2j%wT`MZtNle1g)ET)qT4i)60N-F^`6C9 zn)$wT@eF?x;nLY!=c4qHtz(Ei&We%Xo%Vm<1AyZNJ2@{%mpx4D4>s3y#uy(ZFQ!D`_rIc9x=pJ% z*xB2Ao0`O|=#&0r(qDbw!irr{k*HLu-V~-4m$B#WzNb`ga``v+w~Is^HLoJ`^H>qx zdM^$Pmn%TJ_VzVasj@>Q27&6DzZ+4%lex&Ctf<@x1gsX778!Tth4$dkbSobsz_mC~I3xB}g_c0HevI?(MCeNCz45B=j`v!o~R zwLYw_`V=m=As7w0XV!Mw_rMBqdN9`FtBQ9yXtX$Ja^BF!=0We<=_b|L#Xq`hy#QNC z61q9;VFH5C_}R%7Tx8YyeQAQRZEVrO*xSNa@G*B|#nicVnS&)0rOfYGeO4llu=I?M zsaAYNr10D8NoGf9Soj$3EPPDTDQ~*PrCrq1SG(Dysn({D=247j7+7oG;7#&1s+Y-k zCK^CPIF(=_!=T@O>A!wEFVW@AZ(|X+jd?fdbbO>L>Ui}Ob1*w$2fMGP3-Oe;gcElq z##G<7DBisD$7@kwp1Wk>cfX&zFP*J1%PC2p=3zEo%dH3#vX>$M7h7hFH!M> zJ!1RQ1zaG@yKs@dRO{#6#}WT?0sk`s|1$#rGXnqD5!fquA){YzRL-iNx|w=FKzz9f zmane8+MA>abkyiPP|8>V{0$hfTT{8Bo?A;VYi9yY#{p-FWt08ae+b_|K!?RjE76w* zogL`cqX(?-N<_;aW3?L>CiGT&6FKvCd#q46WSgzgU z=92f$?CQsor>_n~@o&iK^p5z$N-NuXd7^}0U&o$d;U!KpJ}D2MMPy}jmwFe=`9v+a zOy^1BPn8kPYTId>DOORM-iE(aI5yh>n_xAk!xim%t8Pl&ZCHs7kwKQYU&d!Mwo5H* znb<2cfVXTh;I%4!ByLry;Q7Z{QXnU=vn<>5zYgcOTd^uTK1R9(e$>SSoAhpaVkIPz zPZdSKeW2CVhCqq{G{VHqJ<5O+xig48z8Ueg)?}(?v$*hWF~{`hdwYk=vg5}Tkd!{| zOtGU|rL;Uj+6RF}?Z0>oKfONQ4fDBQf^eIxy0#fcCAIep>~?(RjnofysQ?6sH@+k1 zYv##b02|lMY)QOlCNL|I`<78G@{*-CLw-{k$~R3Wt+&T(Thj2o4x1aXxhld(REy!1 zx60DYMh!9oqPpHjYp`VbfA3c1e!JmYZZyZ7EdIykI`8hq1xsl=oen%YLE4z|_9V(= zXQ6#ztB%e#7Q@(nR$w&mT6@cFRSw2h3B|p&pQ(Tr=3HLnT`C$^bs7VtfK~J%1 zWV}n+`-4S!WQIe=t2qX8Ix%M<`jZ} zzT|4v2_bZxe6--?>mu==vH@xTYYN6zG|w+hxlb1tiY5mhOO_>ZnJ=1H-53squNBi3 zsA(aX?x+&yDlM}PjJZs^K6v6XAItyvc=L#zdTgdNxwH^M?_AJ%_x6%%=J6-F_eXBK zKfA|1ZfFO>pvxHV?-V;bcJI79G?rd)u=KLlI8M2X2y_G1+LPS|@H#tcrr@T-#coLd z#@XDCu2mL8zNoh?kJ*~9+~NkOg^KwRB;!De$P7Ypyv*-S(|j!` zns0!DG8&C4wAi}V*{tM+G%f!+`Qo|uoPUJcu~fZpPx^J0Irl!*!DB7!z68xT%O5|KJdfBuj4Vbbm$&8?nW+e$+iU_=1T6JmN~U(P!ti8ULi*>P1EVBdKRhMu$&qoIzh}tjAvXFvd>J*0yl~v}m!pyz zlc!;K&-it`S6Ueu%I~PL@c8(heC~4!Myr8v|G=V#X#>C`gZqs@e$Y61we0suNM4lW zvF&(O=eUiG_f6}syeu1eo7a$vJ9 z)t8+iL6|p>^%#X+rJ?&fC)ZV?^XwO~D!ie}A9Z;zYyJ6Rlj>pdH-J`HC^g^%VbrOX%` zAvYlbP{-iDLy^g2Op2d{%Q4M_9DysmZsa-U{?+_4O-#0}_77FgHj|?;=QQuUbH~6gJwK1()-`bYQcdT@( z{mZwDf-A$3XUsZm5*c9W#B;Nbpyw7SV{t>bEkL9@urH~6?GynC@}7Mvab%2IQ@H!5 z)lNZ*!0vI)=7{_qEx-nfhB!B)s`U2Dxo+h6(l_OCU}u3<@g}9jsGFKsy-z1cq9Aa^ zkxy*GgP*%4OvTa#D)gZBM&-uX>f_xmuT4S;T@51a)+pkxfZdWX5OR6#o2>gL9)(5D zNQ}T$>4n^B#pP2QoMtL$jan`=)X_>{dy#{gVrLmtjG}q-Fl5C0AGJjqzdYTZbpKj@ zUTPI3`MrCft>gIRv#7qb3A>j4j6aU(0a@Og>fWF$jAFCJ=Q&21-|J$3z)evPu!`a9 z92CReW)uD>56Jy;YU3fkr@8XmD}$NcF7yni0DE8Pjl#{8>An$2VV1(5Mr_WmnswX0 zc{2&5BMwVl%&Y`fRJ5LrK3ib?S^W~@uWQe<6&`5Jo+mJL<~rZlsc8kmN0j7xx#2s@ z1ki@@pEA*QB*cuBm1zE_GZx_+Z)2Y1E1x{NX^wtPtutPdEd0wg)Gio|-O&ZsP>dVm z{~Avt>|SA5I-hQ0=Vr#;zBDWIfd;5&SmvDXPV=p))OPDtY?=E&lEG0XMWbSo z;Q!)>Yi-26FP1{>nz;e;)nt5+%z_Q;l`~iF1Ukml6qEy) zIW~yCSz_E6^<_-pV^AC;-omxm&3AyaY~-c#NsDMPLxoEYkp7hx5?1K*45g!d&EaLm zd|bb@B%a!ClJkfL5t2~q9Y*MJY!-Wn%@1%a%qAm>BN>F~!=|coqCx4NMQ+vFFDmB) z&tMKCr=n6qJmGH3Y$fCV)8?s93X6H#4q+fS%z4f)oNj=iDFK$*<2-3+ltbYc2-PQNo$V#z|EwDMaer53C&FS=c`pZ)YKqJ$GU zexRIV1{Do*b|$@<%WNKWkZAPT_I-3t$;j##COHx-Syje@7P!3xt9&TkDU{ z^OxJQG~yLSZcS7RWy7bCKR%15m(Fu%)Z#&5sKjep|I6u6<71!b$}g5XEkqJ;hl_i?O7u3%{mT0C^myVPcb%HSfM7h zg9wF~-TYNiDe;qbB&lTW~uuNXC_iS%pD#W3mzHfQ)jbpr7RqXwz z0@chid!#^VYo6w^L#C#Nsi+S?(s&DXHn=S0cyH8qSbPop#eJi5+S0;nx8tJjQwD+v z8qSic2AVg2oCYXa-fEieqWF`W>PiX1J*RJ|QR}(Um|HS5N+utZzP#9~zH!qv2Cr~m zW0r7Q`nA-S?YnDs_ioO49<3(#SpVo=`WC@{`i7;Oj{VcF{2v#NACCxbxs>&mbu${- z(Q%o5+SweQ;@Q<^dHc;#F2#Jw^AFhI`TC>ETqj~Em#9euw8|{%o0SRB*d9a7EkyrZ z|46?k60YHR>bh~eTsnkca0#8#Fj=HY2=sriP0=>J?76-PbU{B}d}`RmIYV^oH>B@8 zSzuni^zxJdc?Y5_(=M((kC*aq!o~8!*{jpM1V17@Qcjo*Ge}*-`z6_f9*H!zGtpLHykFU_< z`Pt5yZ|mp#C$(s9GE?9P?jNLUkVHHZN!pK6xXJR!FP`|sZ?*sOw9NXrs!#}mcz@?O zkx%=`L$xn=9({_Rc9g4An;tdo$8+4KPE=3V{{%7pbaS^2cyFG_@$A*r_Y>E4PJIz+ z6~KRTu6CfCK#92GPMpXyr-{=#iS>TA!1-tBo&h;*>$ctF&J3ps6-V&-EB8mzkmNFuH&LI2p3foYk{U z(OW%>7!Pw&RpEDq#-JJa7)qwY(Cmv!)NTzVlO~}M`0y>Ij`CV3#`BNKf9Ri3ROo6e z2#QYNSdQ(uMzELuWdx_nMAliW!u)e$3e)lv_!t`lB;ko(50wUUZ@rGIJ4JA%$BZg$ zSM)9YASd_SzvliH*=wU)ioznF9+@GueoX-*w&?1LG;Jyfq6&YZXJ85H_LO3qi9Vl+ zVpZm`g~r$MEK1v6YG+x~C_)cN6VcZ30?rA<9XrP3S2U#grRUlMtY~z7(2(8#V(-1b z>59I!@#sVi(R&wNL_{Z|h3H3#-h$|zgQEq}2_hl7=sh||??j2-d*>YO=;ga5pL>6} z?|sMk{sHe82E*RRve(+H%=ye`KC}IqS#hPGk2CDJeVz%~AR!HVCL2C4&v+^wsyI9D>(g^6q{A`$%y=^Z9kp8+8EB-$0DC(QC z0kzWxDznU`HMQ;pUM-}`&p!rw;_Fa60k3}ji`?Hu5kN)fc(^QO*7PMuZAV69B}`ck z3^dK{>c#ZX^2L)3NDQsbV%Wn9TUDT&yb?3?QURMK`ZPVl99r;m98Q;zz? zYaP<-i6TKTiN3(o#op>``CAxEyUoLGc~PX@^G*qr&A+>WbD$*WskibvhibBdU2&y^ zx$2`GJJKLxFi?`@-3%@+iWpG?Fn?kt-x9oXT=pLwsHu1>tE+&7OkSM76ig8FFdt%l zQ1h4K;oo`X9)2UhMjqWh*O{02@gmyx;Hh%)t53LR^eEZH;{I0o;T=KCTz`k<5z@Bt zwrL|H(pF8Ko6ya?J?vr}{!%LK??1c2Qf=u;7lQwV-jC=A3Lr6Zi}UC5Z_Pj%_~hn> zI3U1sbx8{T&vE3$^2mS_onxNjO5|Hx_?6fY{^#1i?*kKPM-DT3^zVP{!}?UTLJD^NsA|Fud8^pLaSW#r_(G;}6bF(?E% zJv8HwYN{ydTDiSFolBE4{$)sqIDg6jyoaRG+KrB+@$V$~9P-HZxhwx&Mv;usoOO8P z!6se|FVH*sf4wusl4OlgCKbb5u#2dhvj=YA{=0Gf1QXQ4`g}{lqWHCiWrQzRB8`N( z?Q2oihZ(R=XRPh{g;6gwB8AFMViXSRnqlVrJN}Z$VT_L!a8m6k3v8)jL*EbNZeh6{ z|6Vo?a^|*&US5yLmV#OFV{|EY;ZK3^|IT`5e!84?`8kN|#9O zY`SMZy5x?x@*yf1PqB$bhIsZ?)<_oM4j0Sf8?&%zWXhe-s~^B~JB3nj7A< z?-V4}{Rh|SO!D9UIJ(dV<2GSZE-(KN0x8dlG&+&h7_=eVF8{;WWappN7^{PvRV3r# zy}|M=UHd|(jybr3T~%sc64f;0(Dh-=fXPmUR2%KS=O|)nFX5pt!a4l!fFV?DEyBag*V{xZ{c(jI__E3;;ER!2E!=+-JD_h=P#LbMez+lIQ!GXw)$|i)?<3^*p6<&e!SJG^;R}uo!8j_ z@$z6A+8a9U$o`#Uv9FM4c0Tg>@zi!E`g;KTKJd_o46&)DXUFHxCvQ{iAC}a^3yv*r z5YFr_RL!LOaXQWldvZMV@6OB<|5Eoo_w(vhbVTF3pueY2Y}3PnJfGpUeE19jZh+&d zce%Uo3$MfQ6jifC?T~rdXV=1M#!bhvg8v@B71fs7ZqL56wK^#!=R*&%;C~L6FG2n# z*-+Q|q?C0aly3i@m&AmDq$h_lwh`8A*B%La`LrxBqJ;|DJ`(k>*FspIVPCUT6`kU< z4|@JHvg<5#4SLb;_FeCyYiR%T7QUqT16w6}zn%MCz-sVb8m}41oxB>Uw|?$Vk^#l( zzc&borHzhe{c^Ir)^|2-?5poguX_mmV#PkU~w_a`u1LMopt*yGTmS@jnq z{&w{5Zt!nU`1k!!n53w!uva|Vx5n~162WUx-QxkDe}>xyQ8}Wzq=Px*Q2c$HH9}ATsqsT&1_S@CWzP zv46t?{PF+zl7EM+;80x2<$dP$arHy#!$ha^#hMnL701^!6^ZnvVG~{W&+M0Oog6*$ z%9x)v**}q7*-`#C6UmTkqHBhU;whIJyBBnbANxz1AlF}$glYe03epkC%gw>g>Qj3p zVeVvq7xyD9rVvRwYP-g{Eq88|`Z*KN|86V5)s@aeBq{S#MMtPNjR!F6&|Mm04bO8Z z|8`Dti0JSS+|XVqooF%O-?aIP+5}8AJx+LE)xY5+l%{)fC1y>cgK=#9DDaYb)9L8J z0t3G6kpo=a=sY^{H@B4k%O?q=Cy>|1aF*i^x@69M+GO+m$us~vf_>XwBM?(d`GDGZ zfvDGUA(0AaE`URqQ3%*(gp=}oRhqFdl&7=9LLHkpRP0>c^-tC8W;e&=Mg2;(qD&tk zM?;-|cePh$E44nHZj{KSCkqnJ&H|)jIo;Z^Sj{O}$x&S4qXRs801P3M3Y1)fz+CA9 z%$evCQBK7?#?SgEC*EfpU7CX1Y!Md$E+# zL_xlHU8Lez0M|Wgw;g-v+-Xhb10k??-frCiH7p8y&~c7A#7xg`}CSBp6 z!X5y4F4L>=WQlqy8VRz+_g>9p#9Ajqs)eW|WIjey5;g@l~($M`KOra%{r$@D8) zIb5LVwb$1^9KM7bex+5+Nbs4^M4bYEH2?~AS};Vx^z-a}-TJLJeD8p&gsHO3=4x5= z1YxDoOv*33%;!((4yXOw3hklj55A8=406R&rr32r&fIa`hiXDKb zWQaL+c&*65fF<4;Em^s{3s`5=!me!YDuK@|m;&Gk(F;T9^#}8rHMMOC>2GP|F+PVh zeNuE(snp8T>ul66aZ0EebBzo4{;S>zN-Du#3YN$#HTp}BqEnFr_?I1nivFU3hppL^c6+hv>5Bp}ckt4b|^#cpMtyfUbV?-*(tNCky%=yAIR_{bi}p z6GL4Bgw0lV>G6b4u%WMy`POerUm~`z;M>*! zl10?X>oHl2jA}Euq*@~SAFKZG@rDyOsy)dp;DO#n{Q{AH7ZK93-1@aaz| z5T}~m9<|^?oRX6vSd@RPCb(_Kw(aI>3BEFMg4ZYaD1uqL0UVGI21K<%q&uUw9*bU$ z!oSp+SvghYTNRBoS}eQ0k1k1#3a=5GHhMV+*JCOYMOZ|vMQGS0!dKY*G~CT~;SG5< zmci8ec3#e%^3~>j>I-K#J?`#9Df%v{FOx}mr|b}#t&|Q{I~QmoNBxYamp5L`cJ4Vy z3sd_Id$(UwUReKN2UX^zi%>Dxi7=LGFEVU|p3Yag?Y)Fb_32kTu9&8~i|sxkyDzo= zqXUSUO9ubJItD5VbLx{BdKSzOA`+B!X?dBcChp*+fn zfx-s_{}t>)EQ(rC%0W$Z+Ub|dLs_U`3C)*od*fP+X;fL^yWz^HiH#}o7b}=UtF)iP zsMEG~6zDXe)F10kb2`;!6f=Od3`jS74i;3e;nZ5ZuMFZH)o>e)2XR9}ZnJtI4{YLx zB=ecc7W7-D?hq|CfBtFc2ZZQ%Yv$mOzwxXe_(By;0jSk&YaqVCZ0)@W$L5V7wd`rB z=iOmP%*Vm$qunt2j;=l`zC!JFxsp8x`-RCo6g0@TYTHurjLU@1@4(l{F9RgOb7I82 zs;@-2((>?-$gUT_o1H`|n!I0(PZ;>_6vGHe`~Y|#1he*Sxw|SddQXTiHz$dFOH$~$ z!KbS4`l@aqsQmi;;5;JGLnyh1pYXI_FXv{SoJtUq1zsNwu(x!H+6r9IKN57Aq!QSy zS3~lUk)vz@&Q9X5ibr3H9lpW`^EZL;cEL5mKF)qkI|mUrb-PXChul|O>l>=lnI1Jv z4VAn^T}H$DBySu9d*)7ANPyUX)cunn9{1TasS)JouHj7Uuji+4mxmf*TG{E8dmF^# zxU?epr#>S*IN}Z6J6`u^?l4T!UeyidM-hgTEe&CS4l8~wDX=q|B9FkwkDjcVy@`;_ zc%tgJ7sIZ%*(|EEFuhAM+|Y^_>Oy8+TDTVQudD9Rty@9#2t%r+?-B8StT076xuK!(9d)-#|6Y=iD z$KZ519BNgh1z(S;Sc%u`lR$hia}#5GG?w@5$;O+qCOzKd{osOlpWGEL7X3>4rJ++Bh`l+K$5?sFKSMGy_z%)f^&fkL}*lbMtow?o`sRte2Xz z+h=uZI@Qbk^74!Z58<`M%;X6QS)5ZHl zI0*AiRuN-Zyp+7|cn9iXny~>Mj{%nSEf(vK2*pi9@?)wl4hBa-ZxvpjmX0 z96s2{)eT2r&}vTn4>xTeLNT9!lGQu_+GmNT%`Pq0p5nkX8o(84`xDRpvS~->m z11drihX!@a#E9;_69hPPs$D? zS)pLmEUN;V7)c2U3-T~Kvf)J_Jq(2%JE$nT@?9au-eCn4Z zTRO#)T=|ik!zn)>;&Dyv+g(^ETRa&6Pivp(sch-NoJam@moI5f6^hA1oCu=)V;K#6 zbU1ok52{@ev`76!`-;5!?N_$U55vi&k4d?0+gDNWv9rwXF0*5BkHhYdo3zIPrk5EB zjchZ0<;73xa?JvDBvON_jw4YQtk3oyWe#8J-{>40qb~aS{SDv~tuMDpe*`Lcf9wcR zK~Xkp1R`e2&N?_#MRMm4#EzL+Ri40VI4$w9ya@9lo@?@VXUL@O?+jXE;Fem`Ko>dWeZ?cisaYBPK;G1 z3fAJ{8UR%iwe!U(v2|UxCHboBw5tD>DT(36g_*b)YHMhDMP;w z4D{J;GLzsl&oDyP?-FHEyaYR^V=7Yzzl+f(eF?)(WQu2p+A68BByvK33O9Wfl9Ka0 z*-I9x^|>loNpst5gg3)&n_mo9t6?z++K;QQc)v#7lQdoEl(@U-nAe++3k4V#JHbmpcB-qf>8&IhcY(=UQ?<{b{X*Y$mEec( zG1eEoMS$31y&pj0QrFSz=K@kSBj)6GJfR194sDGe=! zeE_5i7ql9)U^e=#2JL*Y!YtG&{HI}$Tu74Zu4|_%(_P8wkS|-3-q-Ce=U?$^!;yz` z!Prja9r1d+mHTINvzM>7ey3=|@7&5_D0$>1F+Ua7N(1US$KT$gg3rONe46xIypnDc zB9Fj*!x9({bKow896iz5p}1qc980k}v_^!mw z0Q_t2HZyRi`>vrWyr1>%f=uUj@_9YZ<+eZ>m9%e#NNAGn?5Tu=Wn>zK$fP%BjmRNS zil3XvQ8Fq61KpKa9F+fw#gDizX|0N5dC805J~sD8mjRBl=2G$7agQx+tThYNxm>w= z(>a9BE+QRrY&1`jZNfJ@!9`{;lD{(X!BTt#1wbz6YBw#1@2!P4>;l%l>nSn-`1Mde^r_(S_@q7xHj;uuF`iD6jfav#& z?$?5Ax$jcgZoIIBPoT0^)pj1C{qg!T(3Stv0_a&1EeFNeyJO2@W5gRH)XF_i6#-yp zjVO-)zLxJvJ*M;Un|7=(TwrFbfHZv9bB)e#(zoqZq!S&tv?qPc;_?mW+dp%~B}RM1 zv`F=8t~l&YBQa*{*|pApCvteSu6#Fy>gNAw5+7o=|@=jAmPg^EmwDdQ1J z$g+&P9e0FUF57=-B0X+h>ULTK-ZpK041&||JA}$-0sOlJ=R{X-WU9zAFa2InvCjJD+F>1`R(EcCG042UMEI5fn};JU zNiVRnXD*j6KsJgZAK;B31}-hr{W8my73i?c2iu=^7(1If-R}hJ=n&G1=roD4ZZzBD z=SLK3s{Hq z6QI8;e?*S!EnH#?CJnRm-+C&MyXKK^Q|Qk_Hx18YF9LEU!zE(AO9L)cN7R*=%VK~g zXF*f8`{Mm9bgubSRAsZ~#2VbWmt^F-5fA4Alg>H*)Ztr7bz~NIe>#}N^WsFZ-r0hg zBF0*_b+2}9gCxc@^z3CfBWn`|fD=g4MclK9QL9FR3`M?p{>G4-Y`)sv%Mik;BAEo> ztfE2?&r!sjw1c!gkL%}0(xEt7(_W0R#(eYWxVwAS(anv*CJlzehQ6mmoNGS0sHT{u z-neDE6FeHcI)7PpbRf*r-IejT}`@*`IHPNLfF0C zc|fmd$=$ub@^*wxAE;Y;U_PX{gZEj!Yj4ebrjmFUFlMewJOa@+D@ug^&Y>@<{@Qvp zUU!Hoqua?tpy=kcZAAQ_o#i-^z1c*v0KCv!l%)24xg%zLBva)o;anXm0=}Sg*_ANYW!>=dNeE zr|x}!EFQm=`1ZqX^81H#mh-^i;b@v7?$O3~GuVnP=Zo(JZhwA<^r00xGSj(F8y>|@d^X6JbKepkbloirIu-k#-Sl+M!^M$@q95t2ca=X(u(K!| zAbIv|o8%YjfR_WiSTny3QPG5BTo;7W?ylKJHQP+l?hZ8yUy`p6Nx=0b%m5TP*ym3N zn&+Byv1+DSRluGncP~ti&4oH+z)qS=9@if&bLZmOik$~+Z=h1&g=*v4P;~NuBdAuHI#{4$B<>i%<>-I* zNq&C^LB}9+NgP3RI2HBbD2jW298{^MJao}{$_4wPdu(p zz}B&P_k4sAqNcYnE*nKF@*Y%X_|D|XnUIUo8NVhyp)l8*FMHiL($By+>{s^91HuOa_^x42TY~z0bn^X@_N@shR7>zMLD<_&+JGQKXO#7Kh}k0 zHxRVAFI=(gUpQqGvuixY^7l`IoI5je5%xw)&OEo!1nPO-xN^#V=l;VO!Yf08yvBy3 zX8^BXVpWKYF42PKen&SfH%3AXHNvki2GC$mKmeilCt&OOm7k+L?xwGupUC_**9WH< zLuAbu)R=ZX{!jrfb8ghVT=;>ySKgO#$-GXLD!hZcbqZw%WJ={u3S%}DB2pt#s}li%cn5Ke=^)EAuaNdC1&^K> zrjfJBN#}mE5Q4)$mqG@ob#@5CgC2#dBNqy$F%<~%wl#ceYU)uiji;Uy28RsWo~TLD zQC|8ntpG8n!No+0#+=V53(}!8hq28M!v>X$M%Mu<%#-&k%^wm1s7bf-r+GzR#b)k? zQzY%B-4)G`o+ilNKYQt(f!LF%;PFaB0=PA{>7nmW;9pta$ z@^N~ERdmhxeIzGlOt|&pz%LL8YOj@#yhcu9b3Q8!ovD5o6#e7sVl74*irWF;^s$mU zsO4)rzUS^o5kctH-l#jDSwjovW~+~|DSMHqDaxQ%l@EBPQLN6c+rIO3L3WwcAUx!2 zhuiN`(6Gw)<=UYPVy1PC$o^FEgG<|v4&KSbL-}f36n>y>AawUl^5OK?&h_ z&eY-1YIY0i!8-s2AfqvbyPP@)S&rqt8{e({T5#quzgdjE@kZE3y`p{R<6enL z!8SGlL7tpOi-8)+zA-UrAQZYFKCDW`>5hqtyhHoO&adYnt-m{y{YJN*4{?ti1+5?G?;~Xj#wjy(v zVP9$gv|p9;D5aXuuC>h~roWjVF+lfwF1KcM4~2R-;}bc)<}X6QonTpV<+uAK8>i=P=y)(T~ket z@=(u!{!ezK{qR}?hdE!&kouqSHK$TaJ3G0_^H+(rPJJI9WFy8qp zM%?=p!+4v4NxP3zr3Nfm`#H!RRA8I-CpWUS4ofEep#ms}*Em%Z-mi9^+kSjPAlB@A z?QZ7T)$?i*<3?(o?Wgk|c-Moz!%UfQ8tPp;3DT>A&3%-8W>Px_P)NV0Mzup707Ed> z++neJ@3Y;U(qFA08()2OxU#2y-OOVVQP}N6zyRufo{TFVPApQHYc?Im_xe3yuX0Ov zlzDA@{?gd7fPDSyOeoZP>|Rq`Ec|h{MUb2Sr}uR_)v3}@qgN~YO)G+*;Yk1=J#?W| zPJcX34* zyZ-V4kHB~BBQk@9MY%ZY1=l@^H*Fa{o3>|kj`=CeOKk7mbEkGDM_0Hw| zOH1&7gb-Om`Aa?sP{mq*llp|z43v*%{_5|g|AUo47cj^IkV8L6_0A^&(0pIHM`T|L-LR=->Rhps{A{ntlb)JYUm|Idg12ZaLo zRh|@}qW&C+Hy8THLHM8F>2NVebQ00=<7EENzkRq8;7Sj4w?S0lYtaAR-9P>S7uNca zKo|9YAF%)T0RL|U{@)1v{}_SwcR|mYnlSzDE|X^cmi_zFdD7YS>r_W>sDxeR59Se} zSQ^{k%rpa#FM~qzEUyS(Wh{M{IBi4`YVLJn_pqOh4idAwOa%xalGHAzqiunkbxRH{ za7~xZLG2zIJ)LT+Ma_+|mRl=};WV*bI8YuAty=k&>>m2t8_b)dp7dQnBV(qr$0c-q zAZ1GQT2d(C^f9$d)#MSHX5sB*+`aCvt%B@m;kVVcqvHwms=8)7&5Bb|7;e;FgQIc< z00}`@q2*-gdNneg4r8Em1*9jJWQpUROWe0RYRH!sfOq<-KV zu8>KT&yfhGpRx@?H$k6xe!e>U;jSbyQVl0Hh0W|oh{QKUbhpMge zEw*?UO00ZMZgcsC;cbpJ{M)n0pZxL&+r98IvBIj&W@OBQH(mdyEvT5w`9FCY6y zD=HWa`1#GXTDas_31G;stH6iBPSnllT6ZG8X&%m9f2bS@hE?mJH0Y6GX z`|y*6juwLoQ<+qGnbZ6_o&IJz+#~F*HZxQ$s@AD)a&uEUyhbBwrmi6uC znxz3*zzHL{sN1DBvcf>? zcN6yGii&TR+zoj>i=Fi1qU(C~4(6+LyRq0s&6MCo`sde**LoWk<4GDMD>jUD24a+|FctHqCpC}F5?I-F{6owm} z@kHZj45UUrotP~4N1_iDh25TXh?d`NVzuiVQ8+c+8<4P-Q1<3sJRe%WzAa4>k`%cC zkn$mO98PaS!sXsVCLgvzLg1x46ZK|Kb=2)w7QWDAJ~w+T+Z=6O}-ecmreK)#uIBTLRF ze0QaqO0)rv+b>D2J9$YO+WJnq{de8*Nw%&+9anK9(Ae{(^R(Qp$%Su|Sk&;ZF;kZ0 z)vAJ;!r6gyDgQ-mp7JSAKyS}$lp@wCEqYwHf z8@yQlm8%q?r*job3sL7$N-_Df1MwqGm+@dedN!;wrQr;D(78}e(r(fG@%u?OcKJ}R zwvlgk^SE^T$#zVShBCK>PMQa7M%*MDN=n!f3Ul_ASlq5 zxdND4gCg3JJS=)zF~xNa5Cf1r74T^TR-s`rwU zHVN8_dLGC{G0%xs`uMnaQYEn zn<+^C6qUZ&q)MfrtR&e^b(t2|AzKC@OSw22aDUe*m&jwJB`_{_-6p%UBG&#i_I@XA z7)aRLS?WF7-TyWGyrEdLk5C!Hc>7KWYdc=DVZ=QYB4r$oVZ!{wVTjA$BK^v3N*FR2(?< zO}QIg>lLXz_A8JESe(w_OJ!f1>(!w}q^|dX_V(#^dx>n>7lwGrKsIKORShu_KPzpn#5FKUYXK&csa zs!i$n*DV*TiYHJlC5LTCAospdVXX)R#ON=}%Xwn&=4=-E?aaq5SW{iUSo6Pt=4Qq~ z^ov`5i;pZ>?cq~eN?hMyGtZB{l$BF+XAn+V3b|X#G$t*(5^LN_D7<^2#XJ+=5!#(f z-H;rTky?Gec;Cv&>phivP}MGeOkeG?McW{C8gltfrprS6v8tm+TKXoFi$)QNcsI?w z&HP1QH!UqrW^Xug(Qm3dAhmR8Oa}qPEK+XhxaM?%(J7CyWJ0=8NZ{3IxDiPo2GxkV zqns%+50~n(ahnB|MoJBYFV>A#dB$lO^Vu>_?aYY#m*6V_n;q)yt94rUP*FGts0u6S zX7Gz8TKmQMl^(txY&1LlS93soKKJ=Cu|ok?LsLAfSYEhXy-jSnRqjcYwN;Ihw^PW; z;__OXSfcgSsqSO!a{wG2i2gvLO7p@q|Adb_%?5#S2}CX?7E)oFwY%NnT)fSqBH$*l zTAuYlh>ExXZ72sKYb^*}+}ztMEhr^H0ePAde;H*AYQ};S>q|?Ku?XfUYI}BM+?!Go zRSPJGu8}o%;^W36p@i-{me?~O^eC1)`V%=5qL7Z!c&)ii*u7Wh+vA|zyZ~p01^*^7 z1Zq35vQrE>HdgI+?e?t%1)lY)jG-HG+S_4^6INuvU52AaXg>!?>>s=m#ZQ&2{zyt` zM!aZ3mTFA*v<-s^)&2lpH?jTnQ=1%$@Z&e)W3)pI0h6yT*T;r`zg{84lWPjSC~<3P zDQYl;b8mi)8;%5B_%>Sn7J(O3tg#gEWv4qs;Bhy(^E>E&f@{2?;A+s6ceJZK9qu^5 zvKpaBQCgc%<#tDOnC{g0WJXm?>*AuHNv@W9IPWZ6yxc>BtKF#x3`=$wlJjpR$4th% zS^lmF#!gv3cwXn%czt%Do!aQ?ffN0OHeoMHkff;lYOPf<@aMA?lnITaLXlW}&abqk zrKD#UY{)9(*bG^4qcPdRVe7o??d4DI1==E%cZLmbtbIS9@ib@4 zit+0ktn$gYtz-!Tlm!e1EV+yzUINqT$ru2&b#$NTPPS_&4>&qtnwnk_e`=PvZ405i zf`oSef_TulQltTR$s2rPu?8s3KAf#GqEJ6y&Qwzn%1(wG_!t|?$^16KadQd~kO9Hp zo;4Ntw2U(iC&-jDn(IVyLQ$JFTM9h!Z|=cc25-tH^`JLM%fj& zn-1jX(+PakXK5Rky{%ZNUCQ31pj{Q15B_QLVjgZ0sJf z*$l<7dv+Sr9p$woSkM(|*VeRe6Q3q<>y%IX7@NKe-8zLS;h!}d1*R~{N`+-UZsAZ` zkscNiPcvbYL5sF4^+q%evl=pyl9jN9utzF4s-M5z08&?C>huZ@7gx+|4%Zl~_-Y%C zq)E=D$&K^dPpsU`6#3bY%_67uD*A7R;P9i25R}I3n3I^o78Oz}SFA zgj*c7Q#IJtX7?HQg_!-w6%}nuuUx3|d7)rZ(??m`H^(W@1@TWL$O=`X_1@S1k-f;; z{WaYD0$q?PNcPyKZ19-;U3e4`(1ZtN%hJh^h+Jl^AE2TA!K2!-awK2P*R1%p92&(b z@o7G{9`7HGT>F<3*}C(zQSLfIJ$Me!pX0eb&GjN47q&v|3T%2@59U`8zWC7NIZoYc zo6}(Un4Ma08?&uPVo}5u&tKwO+ca2lkCM4gNVW)jv?f4*YM+9i9SeaSc~ilW^=Xl$z$GUa)-1L1ukJkk^r(r1rL%2xy`%_ z>(Cw)Um$ic<^$QPDAf2J#eX9)9yO9^eC^Q}0ifM8KABr>dgj#BnrD zMPI>1mu8TLqV!o;Ch1GJqceAtS#XJQw9l+o@e%K%P4n9wDr#ud+yPf$dsKAk*fD#P|a#vclHPun)m;9c4`o ztxcpFMxF|7ho3#xLiSSjW;ls|_ZBfLCXuK5ySoFs`%_`9HyQkIl3HTDb1c{w?;W@foa&q;Ds|0nM+3|r+@ee~$? zsYO1J&v9tIGN_EtmYXN_yLmaNHux;%RTPNN$Mt^uF&b@h8!GKa0BQ)^jShVbK16e( zAXNZPGv(p&u?%2U*+dG*hzjfABZ)2eFs175;B0m5_})ITr7Ysz7$DK?KEogrPo`Hb zt_iGKTkrJB4nb6Z@oB{gletn&2tvmfgz{Q_&dSTlubxw8@@jp-N2WvvC_T;b!B!|3 zNoN-p`%aZJNl7|tvY%+OZ9{`Hp4ZAH2w2Zco+GOWFiwwT$TdGE5q-`i_TzeUIDJ@C zi%hCY7XQG`_XJnjRmtJk2G_0A#7gNL73&MOH-$U&#|%YUP_DcWnkly{z5`r>g|luH z_v07LosZ)kLx zCKTp{+t_d#O~QPDH~-swusZ-(Og1@j3^t} zPUfWDFR|bwMOoY|DN~hN+ib7?&(Fw)hQzOx_$~T5)LvBeM0>qgq3(O%N~u4$G^$nI zP@ig@ak_h3uDd)1q~p(AujK&ph1ur1&~t4)(X=9ezO&09a zhH8`(Hm3yCveT&HWVqBZY$~fD54x1NU2x-PUz+{QJO)A69~nur^WY>t9q4gCGOZb? z*p*(Ez7Ip;0&HKmh=0+8awb(>sd<|B03IUVANV-Ylb=Rd!uq&LMv z@X}wc=!zjW->|-zUKnluHOi-A)v=u)Cr#a=Gf|)%NDgrho*Z@A9GX(3Ek;;l_l=7e z+gMDNjWtY>6;(xtmL|D(G`Q)eLPuP-J#m2 z(J+SpT-lFyIkiVqpHGWChTsmn7Vqfo9|Pr2m+yFbxxlxTA3+nbtTZk->dTHBiPaN8 zi7dO*j?<(MMK!gH>xorsA;M&SI(ur7pv;7c)LP?Vy1cm{Pg341gIxOBhKk_ArHt>- z7;!+U@aBPnrFY_bhI^6ioxQf#dc}<|KPslaf^Y0Z(|-6gKUN)dlGc`ixGMTJNJw8d zs@HIU+tw)Jwdyjoj~Xc{ONrj?yq^$OWujW9f2nkG-p87r;xGdE%OSVX-zjA;XBMk2%SwcelC6a$l1P6egwets=kmcTrlavRFIU z&937u?Bhy>+m~r^8fBp4gQpLO<88>a7xs!)d@H>XwRUqW^VoahQX}6g^G4o2Dt2J7 zkdtPiXuA?4ctHq*xrQ34Th$rAphdNzK>Y&HT!li|v4YS~f}p>^`$s5D< zv5%ulsBCD!l*5=p>l&)8P{B3ewgxOA_eGGlSephEL35Mj)VmdADLPak*LEeofhJ5y zk=p9_SDvy?Plg*9)eU&Mf})#p6;SG5$VKKRHc(Q#T}+dP1#^F*sg^j4bB|}y9^k54 z0!3%OrP`Y;u~t4ZVQYE!>Wv;mL1KE8rcKoIkWoB@y;GWX9w6pDSIB^V9yMkNE7oAf z-*J6Pjg!im+UZG~V-U&wj4o>|bFD>K6p9Daais>tguYhv>et;h9)*wqEqKxv$wdrk z!r(1{yb>b~JYyIa-{_5AsHET=x==kvB*!v3^p&ToUymyDosI)ub_e~o@oy}wbMkdp z?{b4VJwOSnKjNw!P!;@R`Ueg509HDHG@5)>9BvAHx(r)gzCN)k8aoiT; z)Cx_}dyV+@wa4f05n5mA3B_tnJ>#r1c-c%IlcVn0)S-zJ4`l9V zGt$k)ODDBopk8htfBzyG{maB|sg(4Tn@QCRcPZPpiBv)^;NHs}gLHzeFnJ%7w$ zk6Svln;&4(UP|QTKrS@jX<-~!2U}weMZ76_ukAUB%RBf2%2Kq}LoGPF_h4F@F*O+W zhwRW9f~OWY^&$M9iLe2!IT_OagH*GEFU+YfeDbgGuGUeZNB+HwzSryHRN2p3R9+)- zm(Sp$+Qp0wB%Ku5sGeus5yr*5)mNlW_$&-Mj#rk8lq8IcgJ%`3M62Z!REhn(tylIy zetk$2k(u!^_P2IUi3Nl1eJXhR;t_QH>kkOV!g>6j)o$$Lt6Dg7eh^)fzE9jFZvTm| z1i%zk`sUCwaAo%`g6xhI#!moE&^VA%8uh@=m3cif3jPz;wXmqX6=m1dEpHvifr7)< zD#E#>;qIxV5WIZbSf=M-K>8tyBH^^W091+9g1a^cQLNcYH-H>}Aoah?Bx=o1YD1yL zqd-bD4wC(S+wNoIyPqU5$Ywx90 zLg~VwqZm1s+CyZ1_8^RCHT&FvgFD0>^Ud@E=B;ZJ3b1!>Rt(y~BD|kh#7*l4xJw*w zBS^S*$(ip>r0GRBaNC6)E44@?zg}RoP*%qYs?XHId!c|R6%jA_{%5R!Hl%Jd?_|vR zw$Vi>ragnBTn9zA=3%r#klu|~WOOSUIOhvy+y)9T+CVWRjJgz4uz{ecsRW zC?#73U9VBQfrRpJvp3N5#N4DMLDj@j9G@o%)~Igq zt(iTnwoQfTSe)SK)i3K8cIn=03+-K+DC>CD$_qmxi?=z`WJ+f0!a zhZDUPG@C*af_9bd%8vzqXICG)}Qo#U(eNka{ z>1a^|W&CD5wf#kocF3(`OkgHwWaQ2feFVBRu zfcY`K4pow}%~@gg%q={>tB^4z@;OZ~vFtkuZyRHuK#73h6bz+R$saxRnc{64$@N@v z6+!`TRyqB#9VeLX#m9V$r&ewd@{;k09>lGAG|y)1MfFiszZYL+S#!v-Fzp;{;?YNj zbMln_K~G|Q|M%a-XQGj&f~zu%XHhY@)sfjP!MePRxZO`)P;&*cPPi)IWfz;q)3I<2 z4|awAJqwp+o^6d@(`SZ~9MwMF(j4iE?Cu-;(^A-JvrnR7E_}%L!l4v486{%zoFcD6 zm3Bh~q5Rc2dV7J0wMOh;KQ}=DjvoItTT|Jk;EL)sv!}QAly;O71bO0} z-pdi&+#^Z!k$!Zkt4)f?dsK31ksra&BRuriihLc_Cq!>r9VMV1Y=iDRfTJ_Eco#MY z7G4UNNc_CGIj_r>{Hb69D3d$;=X6{6uM&_=9Ug1rv0GvLdN(#d2I)K`!W`b3|M}eP zOO}6`w*}~O;AYrnrP$$0%pe+Z%6T_PEQNdsEy08;&$7}N0}FrU^yt{zU?4G{-9VVn z%pb5BZ!vpp*IG0^m;OvQows6b*C#Rdxz+Tj-uAh@w)Rb}F<*?YkJn+5)mq8~XkGz_ z;5p&iNBhT&lO@z}DD13x4$ZSR^*B5j{Gj!c>?Xldh@IGw=|l^CR*$qilf(3nOB;`M zHK~zN@<;o(qVO}7uvcF8RC*ST<-Ds+rsLTgv=ohNj)*cdA=|5~!Tn%{v5x(4euuuE z{aD25i3y?S@>K`bH4dwOA=tCv)fKC8s7}S|!_2LtUxScaK@#4a2P$#VkG+RRJxP>~ zxb)XoX9I2j@Ne}V(sMur0SrUjScfbE0(7D^y|WNG16#%r4!bJC$yJ1Fhw|QHBVZRZlhUU4%o_Ew(X-&&$;^!1aNhj3S;< zz3aeafBD*7W-U0)x3x>NFq-Er|GZi>&~_&f)aY=b{!}bzHqGqIN9_3R3)?u8stpGP zAO88>$>W*vpI2c^SJS7T-cPv7a5sxCb0W^dKM)0Ztr=`=sXT^yZjrqd#Wf%G5Y=)E z*FRlu^6vWeA>%M^<6Xxe?k_gqP*$)gScG_oJk?`;bQ+8c>fEoT#iFg6%LwFt_6hBe z+AGe!*-KYlU}kgj89H*&IkUSddnj*P?05 zjwAF#h2+Xtc+zLaLC<*||MaRBGF1&shniLdPp23cq^$^aU`OGG-ZX5)2oD5y4I_cD7E_Fq^|#;8CgPmAU@}eJ|N- zNDcTjYr9HC3;_$t$+`%<#z>9IvbL*4X5Hq=JNCG15d(|n(ytm(o0EK+{EyZ3Da%i5 zkQ;vAn?osm!&=D~3_*-P0PF?wT6q0c7^u53^t!r|OZ6ztp4;%eGenl-8@k#;U|Qsl zr0amn1Wr=9g@@0rpCZ*+hrgmB2epX5?QY>XRLqxe>h^^-xqEvlupGaX{VDQXy8(lW zKP-uvi|3rg zCT-TZ64!Pbl2GoRBsA|@>S9>w_2ZDJ;zF}ntp*LC_$> zN|*XdgCIF0^6cecK?Zj6lyP7u@}o~2gqClaUb(`{4Z;;WN5O$qr)d| z=Gar@zxw9Ol7SFKtKB`C(OYy?c{Q&>E=COby8wBxZ>oUNV)ESE@m{V!kZOl_`PofG zNUOJiLb!bWOohftnyu+2>8pD2oY7=~IX9=#DqDg4Ttjzh?$DgXg{o=F7NBF zn^=lylMxbx9>4qE(h1doYg;!at73Zmj2C8iaQOBIPtHLM8Zl~%@J=oBur|5exhq29 zwHf=0s=Q=pEYm*N`h4Wg5;T&zl=b-u&-CL2Hca`iU9A_%d&$o<%E^NXW88Wk&<2}@ zLn15!-z7H+{Cf067RUoq;68Ok!yFN_(=2;+o`1%@=-7-k*^5}B{E;)AmX}bH{PNIr zr@;XLIKV>x^D<-&)8vtfFY%Jpn& zGyvw+5^P9`R$=o2~b@ZuyEkgHtto%TLv z96C>7wTbuXWGreXPXf42s=xF1y8<17K|=$7AoSf#^a1rNr7wU1E%8*W$V~M|gm;R66$&Tl3a-UrFBA zOAn5kH z%Y08ip*R16{{c(=bScSWJpIA~@`20gX@LLA12M0=WN_Jj_HE88;}2 z!;T$=(a#L0k>xPYL@n4R%G<%AUZH3{huxsSDMv20HaA<89<4H>#3tnu8sL(3N ztH$ZNg-b?1vD>1kQd3VAjA^iYqmuf)dPkyELF;)VLh{p11~Ez-Rrpa^gLnxJBQwZn zH6kr*3koqeTHrU*ac$CsiayN5{vH}ZL-*{f+;^_f1Et}!A8z%MrJ>1VgGtyOk~zK_ zt6_PLRUW;aW%dUOSE^c6o6qe5i+`lsjOoVK<|xGbq+6(L(6b9a8o!cem-0p#PhseC zO-5m!Oq8bB!&O*Q?`aia`}9Q@3iQUbC^=p5?X+vU%@Ip@06MokySxO|<2Jqd;=8MD zNf0R9iqZI_yCr?o9W?{wX#dKNJm5Z9%JRJB$JNX=o3w==*VUU@k7F1FHi|4Cni{di z<{*>ANI{?$njPZrZy@ziyh@q*`&%Uc7dF|s-(QePy->r&EiKGy%k?+NVZh)Gy;}8x zp6z7OI4?9Uw3WGgwLA%roerCDGIR{L#sNRz%7(R)t%sFY<)t?|>(q)L^quMWW#qhn}K2rX1>N?Hr&KPU4|s!`M4@Dx8d<%3F!4ZT`nwkT1UdvRnkq?l!U z3CC^7CZJ!$hYb4NA`&(9ijtc~tRw3z;1L(Kr7l@8BPDE`B365u{dDE_V#uX3nChTl zSl2t}M7RriiZwWjm`2}6M33n%sRZQODaiE#XJbt{FVT>E)!MYjV5_Fv)YNNSk#E|k zR+7?p-`9u4rf5U*y++MP9kLVSZo=nwN8_U$-=dzsynNEh26u`Qz-1ei#&>(@Tj;V# z*G+nc`#C73?_TcQY3f4?5DL*co%}Ftt}la;?S{SI-UTVi8sXD$$ofriu(hA1+u?Vc zVffkwN1lD(%eqJx{XsCV2X_7C{1e|$GuhSMfny%H_CG1ea%L zd={%ZO|rgcu(=2uVj`K^&9&Dk>B-OA=?bN+WY>?R@y+BtI@qr)*XOmHP5!MwV&%t` z|Jx58&q3aCp?bMxcox0{RYAWmu+Qwg#HH8luLHJDA7ed8t3)L>I-3E{`!MT9~}qn|9Al`NnodQ{jo|3B3k#$I8YJ3rBiozSGfF*3uN5qr)V!?-aDP+dFA+bFe+RuBY(R2 zIx%f0vjxg7ac{T4m!6e#5=-GD*+CHBtpx0B@fmTa2G=ik6_!v#C-s`A>#nc7_NM;1|Gg5td=9r*Gwl|i}YgBdGZ2o?d_kN zCw5Vn0}tGE?sARZYb+y3Y8cB&!z9J7z#=;i*OD-B`ABU;1-^a0w}@}8t>v9SEL_CGa z%~Y0iDB8ER|EiJzk<4op)Yqc=ot$$F=ageU?$Qa?b^z6O9}>Y+8bfQb?mn~=2)c4$ zBk@w8ZEeGbrBqjRws+jaFWvQ{jNQR!r~8*$Y?dpB%~+l1kHow#(c9`Ym96DGMDqt^ zVl}plR~R)%rMLi3C*($c+WaTUwAFe!ly&-i-EXYlG)dQ_XS;qk>nf{buCWtey-ho0i+%w|cWfC9c zF@$OHL_$Zg$&`AMYwwa0%G5PB1m3wDwacg#=72&F%{q%~rAr&9F!WWaiU{J(;q=Oi z&X$uV^Q`{t^$*Fh@8$Z_jP)-Ml!n!V76KfP$?{dI5jhJJd|3JAPDaV!tU?E>w9JK- zUlUJSki%XPiq%BDxt;I(gnXRDpxTR~w9i7kezfda7b*#Wgul``QMgH#)7mgA}ZO%+NCA@uvmK z`bW^Mu2F3C!wrT`*}na%9@lT(xHVV5o3IPa_!v*Y*;;DVVKy5X)+5A2F1<@i(||nS z-XbD(XV?E?RoV$;bjS^tB*rF{^S0+vmoUkbwxX3=So<(P?L&@2xxIN@=JSKb9ag0?}Tlql8( z#Izi4$MZQ$r8%>25J9J3aR<*8yFrL{KKKjDoMD)L zwU3Qr5W?RSFx>sbjC5XFnmbbbw|P#B6nHN2>-ijwsC&~v?z{DXC>O>`Ov1sW5n{6l zm47be|MOS83pcx((Xcr71>o=`{ADDO`;S-of7#;x=gH@i-v^6ti!gWp*HjrGjUn?{g8F9&Chq_I zfdAw1Ul1_-%>vfs|26Oncxz82HI4TVEaK1p=ivVPF|HU&VQFYqvw`CO8=m`*VKVgr zy0Uh{Rl~FA|Bplbe~&=?{@?NNJ@gm&Uvo96{eIDjo2pN)A))+_5B=Y7FOKy8g98v9 zdxcW{>JPAy*%@N%8`3tPiSA9(C;g%bsK9JqHz#rN@xotfKgDEMh&XK*dc*ECs|~xQ zEvI%p8FUZ-W>-Y0{{irsft>E(_mD^LFNFr3TJP~(T6G%i;*1$x04Gp@QMjGmvr_>F z3gtK$4y5s^L+K9J9v={<$7EaEQDSC*;mOc%?>napEokPKnq87t1Ok*^5t zD;;9Chmw?vH3W2RFU{Q0$AIezg__+5ZqMn{D{nb_j8UYW=lMNa;8IO7-dP~dWp+0e z>R-B(u~HPsRIvGQw^n6b;C(w`{v})dx#bl6>q`BA-Ry*-Sgfdi?Pb?(u8hG}{z$dQ z`4{;X1QU3+=cB3mY}IJ=B%YEe6DA>Z?2Q<1kw#r;y33b9EW>-S z_gROM$zSUVxjHNL?TiL#z$2m)(p$l0x}N=J5d!qtOjbavs^<6CS^{a1*Zhw_o@+U+LRw!g6@`-3bO}@tQd)6nUp6gY7grccFC;? z>L=r(LHEe6-Ttr}_ruy@_nklz-f7D5D9zf71zlwAEZ=udjH_8s4QqGetKb_S>^h5y zx09QP`Pv}0~czDy`(%7w{rmjpg6rLt<32Mr-25C3g1)OgtqQpl2nde)bfu{Kd1O=(NakHNX z1Rx31`I3yti|*kI5Pj)+Z)wnu`MO~fd7yH5TTRh!52!>>7EJ>gJOT&-OFHG19CAOn zw?I+)a7m#yB@`n*N?OJB;xhqHB2btH0ziEjh<`Y)|9PGp`ucffY3S>Ihg>kcNR-oc zvEqxQf&jgCm!@N_QID*4lU3dEVjWX@;e(!Po2NP$y24TIcH(I{zV6+zk?Xn{^5vFU zr~QMB`}%sPh18HowV$}#?2bOV;?_}G$4T}(`TQ{ zNS^*C%1Rz%Jbj32!w}@rv>8~bSAUA#tn2%Cn~rGs@OXbGS+7nIah&N3FY>3NK;Om( zjI9fNateLi%AyI>JL5T4vwo_V%v$X53_6Xi=+@<8AYNc>YqE7q%igsr21R-3`l?TP z9{|(A2Ret%pYN}0@Zkvpd!Zgl&H}+Eu$_3Q|FJO@h(YFU!RFmvu+KdF`mh&yGkNDt z3*4K7NjX*;Ybmj}Gad(D-NA#m#pRS5^28;>4p2#{uRfS2ydhXf94w~38%Y%n7Dizb*weV7gG?hAVL9$rzXyw01FARR#Q*5y6 zh{;=xRv6S;+GWwXI&WB0i2p5@bo2lm)G6NLE59RS+{=Rss&8MlTD4yKqiO21t8B%Cwvz=db!l$ziQ2_&Y1o>zrm>@sNQ z^xa*@-Rj(+t2B~9I7GxfymrIM4Lw-UlbK6BrM&T%VhSY94@VUc03#zTHmvy_{~Vs` zv{I+(6~yPbWeo=!a0;|o;zYb)vO9wX`$wrv#!WeqT^x=^m|IZ5S-osHLrC9xWGIP^ z37brhI*G5DU$XUae)0U_euz7T)3kX||Ggy;`|BeMrQ&x`cBl{^H@qVIu|{HrqR@7E zbX$hNiXp%G{_26lcx~c3*rVCCZH3OFa=5EZF6(b8GO^6lZW?-X`M;&x=~K1SsDQcL z<*ud)dnHH+l}WRF@`g7z`DqOa3{Q~|r1ditq;IPxKcK-%_9_Bl*7Mn!V*=#yOqv=9LzkIM5wBm$IUh*6-qoOm<3e%> zYqV-Ds(i~Yih9TyODY-_Dq5GP#GhoSAmyg(zmxQS9iwNKXMLWA=0~KNtbczLy$ zKj1W~Q7crID5h`n1djcI7me2GDFPeQ-1~0EBZbf2FHwYmF+MJg)hZh*&>-)uAGWa< zN2e4+!sR;yEv_;SLYUCZo3FLMyt(}~&|%Re40(ja(!G~r)S$1xj?L0Nk{2o*^QE_J z8uE)lVW$_ip3SOu+%miNR!fwQV2!yv%-o9Wy*U&K-J1;mlg{OyNkex={vjtVh07HO zVW00uV&}^#H)ui0UHx!2y*zN#{v!bv%m zbqnINXkKdzrvKuf<$%km%*0xk1rnjK1=$;(+M_N|oe^rxCk=F)iYU7Z;jS2#sDF6u%i{ce%`~1k zn!waxmz7OH^2yGtXQv#qHO;l#$;6Dp;J=mBx6Z zCb?Psv-m5c|Lj>IT4k?;eYoSR^z-FO@Py?WaLI43_KT*xZ!+d6-bW35T!Lkg>u^?J zQ@!2G^r{#5-O%9PEVoM^*tZkGny{DY^9|86zp7-Dd#tm>8DRV5`s#OlonZE*z!ddL-?_Ry_yiGZE|+)MRlGF+mi*z zzk_EOnigBmQ(Ut0nd zS%ICJz@oLFDI(r+3@#mXEB&R*PRhf8zwBA9!}r%R zd;H^E`$~GbZ{}{!&1+w;{On%@Z*QRe+PStIR2$M^GWrlR;wAtuvAkewzX~xyjX zF06S zQ)+1{+}rn zrQ^UV`3$$_wt&Y!+)4}&Ho^6Oy|#Lb6Dpt7s0_Uk*e`?YFA;~WBpb%%JzR(@u7DY@ zdClEqfHD?^Stxacvf_=}oHXx0^aorlU$P~uPVQHS)v;)M{OF#I6iW9D`kaAAd3>_n zw+dc=^x954UpoRtgT!1q9={u8f%aA#D%_#^E4dNbpxNS`I)Opvz94Ke??tG^P#R3A z-fS(N=)@C(W0-dUWHx?Et+SqU`d~C2MiPTErf``5&4z*4>``<`M*Br~fwEfX#*hc~ z%^&2I1d3FjV&nU2yI*;dcI_BTvUGT8wPUxPz9Z_IJgPK|7>s_rI_~KcRnjsW4>!F* zwT#d4)p312NV@jlNQd4-yOmsGqr}d`y{~(MQ+yrii=}^xfBx|b8yU6zS4CTyq!-VJ zlIm5qXt9yxG6uAIXj>Mns~hvwA9HjrW8S{~PLJ=|gf{v4A~Cr3CII&SQ0lTEudwUW z&!PA~!gt%DheuCu@75tyN1g{|SSu3fv3&LxOkAzp+v*vFg2DYDe5`+p*t*okk$ndy zzS1ocK6Y@@V_uZV!LrO;tp#h{v_;^~kS60BL`FLi`xopXxX}+C>R|Ac`-*GF>Hg+> zKBHSXBc|dHri6;Ro;x56{VB(u2Un>rX+izlP*989KJJ?PQ#a>fo#E(Ejd35$X~k9Z zpyIv9X?A$igGwCqX-48yQ_dUVh0j32@_A_~jy%0oOc}d69o#xkuCaI2OhphFX z`BWb39RP8k@trci&+n*Diy;l63U!}P=Hx`aL4fx%N(6<`uARg7!0}(y)8+B>{E zsn{&(i`PlH3o9D#rlw9*;%|~Y zb&45_ho%`igYm3o57ajk5`osE-u3KIS((>mHz=OdjSn3njt#=fKA`qY+U@xbz^u!e7qw4kZ|8s=s7do~Ep z#EoRQL+?%9rK#SKUifv#1<&;3lJZ5I_J{pvWKslzQb~oLy&|_aYtGvUbg_NT4;&`$ zi}yVwEO+6zIfY$bwSmplEMjx`)nn*QMV3-aCb2 zh1F)@oz{d-3`(m6x|>MWz2%Vx8NvQ2i7Bbj#6sdR*;>WfyWfzqlKSAqMRgu#1gZCL zR2B;^+V2b|yc+iXgXY}d+y}o(QlXi$2~0g|t)K9G3MWA%Mz-%<&+Shrt-rkUYyZ}_ z1f8xXz96A7=I|utaz1;MS^{4k*_$ediwl_HtGgbhm&mkq(h^I5%<-jFc>G*>u}K43 zgQ}N)_l*WysMguJX7G|nk$mWt8swe!45}WWFOH_GEgBO=Fs~5Gh~ZMT-8VTveIVx% z>e1B5gBa)O0aJ_wfJDXM^ zp!&1i#;J)H2_!}EV;3q*g397a9sH)XpD(C`%yb4=1@8kp1b3Ev!Y#Uk;P~mt|Urvpl68ZyqTm?5R&HM^>Tl@C_U!1 zfDw8G=`w^3%eNOLNyo0~vt4UdfM_rSV7h_f8iD$9nW-BUfz{JHzS)=Mp~3ey)5oro z7e;PD4+km;J1#%Q4{W`ht?59^ukP;lif~X{9@l>gHkVGQ!|$^ok6T5hT>~;+H`8P3 zI2I2g_n*XA@;ZIOo3l<5e9PPNn_NzvqBMv>#M~ceoEN9nk1pihTU<}xH87c$Ia1hL z(ewG?=HyyYoBB3*!d3Yg1YbF2GgKiXM1x4Ee01)Y>#0J0>Go|1l4g4S`I{Et)+kg=zvvUJkRPsSy z&u#WS-LMtQwqaR+HfzjksFY-F`xCK7Tt4IR+0}C!iiyF7u90 z)u&fn^=`x!MkrgOk{2YvWis4kQHlLK^So;P5kWOE5C9^Str9cg(=_90UY^ixn31PE zv2DC*r$B?{2zzhzXu60I^d*aInN}n3?SG(KhhS;$JHEdsxQ)f+14?0@@evGX_I(Lh zl_e;_IDGNky%J&A!EAQ7NL04&S*dh3`24h|WC^<~jWZ>4sen?y(&Fw+I<|{6@5TWJ z@rdHdnCZL}duhI8SUkjXGDX^V7QFI$Yn(sm0t@L75YHU&)pv#QUHI0frKvu25*JI- zO`kHS9zl<1KSO+aId)BCx*u-0*aS(j$m$5x&;BY8_aQ`cPuwy}q}dF5Ua(%zluKYXJB0i3w9Cl^Uwdfbka(Ai7nJnBQqp=vm7TH%J_pt zyZXm+N&ZQf;V%#=XAb{#ftS_Wa;WbZg?|wFvduN+g%k0^o`%|+Z;fN^53E9-S0PHV z(FzqsF=#W~9e1nNIyljH2Zee+aAK)H)TgJ(bn#FG4PuMADm!sTTsAtE5JN90K!{TiVuWd3V z!|mJ~h|W~dp*&M=)eDAM|F!uQQ2a=;E5#RU?m95}TToVh$Gyet^z^Nm;$ts|*QO}d zsADah_O|i|@ALetB@D{Af}!b`;4MP!8nbZy()#G%(kiBt78AKe3+j8Rz!$Iu22#fN zb-m^PU}0|=$YJ16eVL$j5Snk|glf{)e{cmAYl~Nw9JoywkjaWv=yY_F3|zT9`2xpo z8(Ut;y&1cO%P3e!ss$2 zz8UNI<TYL-JdE+hA$sfi0$2^NxE%Npp9DmhQaA?2oXvW1*ah ze{lGr+=qxUeL*`2@#`B>4z(O3Pc-kNS(o?JSe4xw5ItR2-Nl#BykAs7%rg>rb8RLz zdrr4GMS9g_dl^Tykj@$oGWhlw}{RL4NHU#7zo}PjZ zJ+ce^eb}7Y77dL@h82fa`3{VVqeb3N;2!5(v9B9?$dL#ALYjgc788H(P}! zFD1~&pae4fTD9i=HxXf0n7 zo19%7MDwK8|179$gO^c9N3YyW0s5*4b0&m{U)Z_;NMbV=`H)Fs?!FzNpM5g3$PUTG zk{G|`kX6Qdx^Yr3PCwl#!_M3KSa|We8=<1eFRw&}=xtHl!u4g}g(k-B0v9&RU-R@7 zPP@nvLg}`Rvlo0!y0w?c<-02p72VMr>##ebH4BoEYGBPq!xUCn@U|K6uZszP|1ZFg zv`I;bSJx)crZfu#8T&o(?VXKenK{>oJe52-E^qFSzT~p9!A|s^d#{{CU>vN`SnjkS~5U!+D?xta;Fr)H_S_+^7uP>PId-nVa%;SONl*Q_a z!U_AEN95ksgTlfoxTO5iaF8$`Vz`Y=O3}DbCAjdONWV}V_Dr+0S=YgKa$eDOwu}zTJMRp**r?i2_m`bvmIFUJbIti3YBYd zITx5ma)_Xf>dTPs4TrkT>jPRA*YB5o3n!IbX4FY^@AHg0nS^&|$=v0Fg@()5STxwq zo#G(rn;ubWp^Q@puEI@QxPL-3<7WD$v=|3!wzkDkx1(1Ezkapz^;K zGB@-xYs5ZLgC#NQkLfC~(G)ICvNCt3TMogop+Lm3Wc@Yx_>HY9Xs2fy=|20}-@(F& z>A}KN1{7e*-y>t%0t-M~Hwk!CeJ4giHcLJQ`J9iiC8}x%m9Ii?Q}3|p%8~js*DsoR zwMc@kjUT*C+&~|@c;h`+8vxPad}CUb32Zp!yEA0&U2&<=34JyO$qxk>5%7Z_WjNr&fe)^PC9my{2|o&L3;;M;6y6bOF0pO@C&IA9Lk zqkbYU*(X#J;())S<3aAm{cec}0f`3VRT>G!i*6T(4kqJ62lgLfWYgj8T^5Yt#a)wW z4#~5If+ULpkXLwG9d*y*$uEU!pJV^u%wi^f&lKpI`>Bu=%-L zD>1YyS!2yR03~dY45$LnhXo+vFr{@-qG)t~L|$n8G|B1s?t*Y|>PV+9E+(({Ff!Ki zqw&>i>5b$V8-WCg(yS=)Z`HA7K4U~`VOh7%43tz{x2$I}OrrO3QbAG|rCrh+!B1W~ zN(;Q4m{6_oTF?EBcF*fk<*l*M#Xhu~oNj3gp&~8eLHc54RQWyIDLA~eUwU|QzKF0M zqsMlwiY9*jdAAvl4p(kNCFur5G3NE^)m)@byU}mO=G8mbqC$+i*Zx>zG3pZjoSn6A zOh4-sfmA=|D z&%;dYaogFIjmVWSFNB)~Vyc7!q0gs0cxYtBh`)4qJ1UJp7{9B~ZaUAT)XA73ayCi< zuD4s`*cqGi^B1J0TFYqtorkk?KgKsPVSFHw%ewImCLpu$-;O zg_u5<6A_q-^P^mh&;1kvS1bE^Fo>i>vTLgJVy90w)J`+!Lvz;<@Lk~qPv|Ja*7tZ~ zO9HF#B_)DhyfS$9c*G_)O=S@clJDbX z{c4$V`(eeC53tl^>gf6okupu15I%}73ybGue@#^E9r=!>;@>UQ z<2QOoslkyVncT1L2&$(xM7(HF%J-jmbf)}$?h>f)9Cu>kOtt7w(k(N0SuUbpCmnyA%LZz zZ#+CH8>nkV$*+Oyb^p`c#UXyE)WE!)uf$q>;MxJ|>scr6ed1%gt#z;jwZvoYh5;oX zYcYD~W|xy3ft;KcDU!ga+c>r>7c!Ja=`!VznN1{tQsRFd1g^N=GoF_H4s;&xhTj&+ zm4>^S#`OwPv5(%bBB%~fh4pM8kJnLGUiaD}K<4%7iDN-lRC}Hl8Ff(+Mdf~t6UU6 zy(4+ENU5xrZ_(F@!7tO~lt)uML=A8wq6srB5N8%GTV0I;U<0iYwUx zn82w_l(_6wu54!V+3Nt>ITsfId7$T@&YmC%Z25%Efjz@xdns#YrYrp@xM&^OOZcbi%d%Jjk&9_Fv zW*(Nb4D@;xR**+orl|(S;fbGQlQ*pShMnxVKPAHW2EBi?OBy4=OtP7QlDn!1Isa=j zpnF)4-iPw&eHx$rtSgVnzC-8O3FXNO7wwIQu!vHn_01;c*Z?vJHY@wAq8XKF-mtsY zxS@OKm8=3k{j2Q$mnMy?K0cfj4FKqoD6~E!!Em!YI`8U$KyzNszjpPX$hB{K$gH=2 zPDW(65a0o4AR##H3k^%KW$BL;N~m5wiSCU~LVc;xh~5*(#8yRqbvNj+Kb!*DcYnPq zcj~m6F$WMQbV3{XJ0Ed(6-9m*@`1USeyw^AjmUx_GRYj$Pt`~-F07H;8}->`am3=x z<5Cgxx90WOnxf`}_WXOsciy){O3VvzIXoav8c?Sp9)J4Yv^tOUTYn3vrxvwpyC3Yw zQaRI&e*SscWBVfsp`8#NAn@^Gw5(33u)IDGva6}LUK0o)mo#-dT4b<0oHqG2tIbpU z`58T*9@S;y;{xa0d=A$}z)J5F0_~d1Wna9hZA!PfZ68YWDQ}2XgDQ4PjQfR-24uqs zxI22DRj3x!SC3rBS4Yh%-K4Ax_6B>d6o_f1y46r<^53-kJPfDQWOP$O;8J$b`Bi~k z#jBS9M?uVd{@U89X9q6(l3yO#r3ILc_dPNI9(A;RPLXmZ#=Qk~&_F1j$V~0^sIkfS zC9tk_*S31S6p{dUb$E@CY6^>rt-g6VsP^O^l|VG5($O!^U%|lYMk&v4^KiFCeq4a% z@yK8ae&FGs9;JWf=&*%dwtYQO(f^Y!QC^6o3sPeiksz_*D|*+vW8vM-YZS8gfm;wF8Yb;dIF`F{{J6Spd zT_lH*({dw6Wa-a$EY2+TeuI9OdQiYQUJOvC;1 zBc0`Rzv1ZR3Dgtv*g0t4!t4faOHRE6&%}?3k48@Y2%EVyyfB%pYKW=6W(=n2Aj$(W z--gaK^_7Eu>Ca|d!mmf;N-C_1W%MIS9*X0c4>}$?-Tf5!+F{)>N&gpnUl~=^_bm#D zq|#!L(j_3>pd!toyM+VN(p>@~-JQ}69=ZjjOS(G_jUMulZy)^MJMOsm_y6|B`||D> zXYhf&*V=2&HP?(a%Nety0pYjS9}%NDNc)sA394IWy7=+&l5wjA3GHJ}^ZDU3KjA{d zG(GaG=f}{aCuXM2L)_24ivV#9nUmJ?OMmu(_KBf%AEoBA_5gc2C}1Cw5^&I^rOc81 zZT%2;&5PqY{%Lx2CLPFX)U=_lM-MhQej_wq)0yHW=;KV5d_41IfG4S5UO@kJ0q{(3 z>{W=w+$x+ZQEa6$&ySvN)Z!e}$k_lPR}Mu`o-~hTamSs!rve9UBojSKfC12(m;Zt+ zRDNRTJDN;KZNq1;fUc2!<9@WW*Pi%S;QoMCWo>tm{TBI`{Oo=9ENjtU{AFB)J*Ol* zGOf*SYU^d<6IGFnzI)TQae7TIviNyKOhE80l&e^gu)A^=XxWIJZyv{}C$s3E*Hw$V z#wH@S*YR6hZUlp_IT$_7PyyWlIpoQ!;_#y{_ASBmN!Im)RAsVUkEV)>Qko{4-53Z4 znDVupYEZ#ydlU}pkn?r6I9Mr$*6hfnCYvU-;hPckj=C=_X98EW8}Big!x-7^2i z-1%G>cX#LL+2}=Y=ktQI#){yAb;FIO#n6=urT`rrS?{Ex^};|^^pcif#8q8AZ{&e2%^C~PPT@b(0FW?&K2}c+ zA`+n)K5q(qqm>jjjfjLK*sqUAbggp;m>+hupOKK=4-j+25F2(@EP2tPdAl!qH?}3d zJ6%#Dagnz>dZ?-Aa`Cl#63OVCHzj90M+K#u;1Q7D@cG+M_WznQKx2siql4CAVmDjU zbp;XFL17jBoP>|=wLg^rn0t~a0TX|HIB0vmzcD@wEVSQPlEe&QaHneKdsPK3QNn$l4 z$Qmk6)3P1pM!^5`p`W2u$}dy3PihaZSU>qa(m;Dk_zp-uF+oRurrvGQ!@e7-CW!rn zC5deIt9D9zughF%$O{_@aTFRnWaQow@PI~dZJEVr3=Dj_CU2$i5Iv5k$^1iUzIRUOoNWm& z|2g*q(bK9JksiVXl8``*|A!wQCV)VyZ#|Yk@27v9C0hnCbqd!^O#Wf;{fyBFcRGQ4 zbHYE5|KBD2?V0ue$wo*8Z>)(oofR_u8h#fvag;^M=dOxVwESJ1nPvb;o{Hi^wJ(av z-x_`ew}(bk6%}13mKrcbFdBKUtQheZdIv?4MgxicO0D!mu|Gb?h^^uKdXAKBc*u~` zt{>+~Elm*hJshENj60In++pFsq+v(yKR@ZugUEd6%U-f4!97eYL&wRFT_r~x)s0Um zinSD~GA`H7WFj<{XTfp3i5fD{AvU*|Tnz`+zVntVoc`9EAbVG9jw=LtM}d%FH(z58 zF39*CH8UBaMAfi}dzl*;m&f1OMIyG*{u$zXShDWmNzkO9PKyg$TV=sA;R2ABCS{1C z*l(-S7V;2lH(DK2SC!v&`|JKfi^A{8^a#XQf^?)f*cC2m3{~LqUbK3!u73Zyc)4Ce zII7eSdCKICCcQJ0bo}h+Rzj5q>PkxBW!wdNU|JNEUyG%4bs9WmLQ$1@T_20dnhFbL zX0S}#fjNaR#rWHdr@e)kILviXTeg$FC5Eo-mdcjFTWQI3L*#~4eTX?{5- z_S)KZ-!njSRA=%*_&AhcG>QF4;r^R(NMSKhM@e|bxMdU^*u}38fvJxC>S@v;7I)Fq zey+bs5d=y@>*s~ZzI(#H>7l&T#KHx@Y>2`xmoZMZ=>e*Evjsp#NsLPHrWJiH#<9OR z%{HD^(Ft}xG(eIP-^3Ut35;0*bA5uC43#np1M(WbA1^D!H3Wf@4yvea$ZG&sMvD)d zzaswg4?jv-;7T)|HYphcF~>jr#Ml7|g7t-<^B0?@dY_T%$p3HUbT3B+XRg;t||!~Rl>EK|C&ba9#^M3vP*e^<4L z3a)b8olCTCc1w51qMy4ivRBPyuh!!JTAE&oO0U+a<=S=(w~-GH+ls%^S&m|OZ;a^q z*g;eIX_ZQ=($J4ZdH(KJhZ6AG!~>zBQj^7M8xtxD@eX@w%y8HyHs{%o#BfjRI4dB= z65Ya19y{G;h1vD5%0a1#0UxA%3My zMse!TZ162xXyR6#QLkF_%cp4fL4($}x5>wm5&4FA6J{lz!vxcxCsmQJ^@jbZ9Z z1p&q}DoMwi-Jw)6{USFtXl#G+S6A2vg5EyV2vQ}E>vTKVY7hmOp_{^Ikl=cyxEL2F z^2|$BCf)S|V*cyv0XB&76%LvF4fd|&3%!Yob*g|zAMDP9InPD=l! zzX5Tq8k`b;@h`X7W}cAUq{Ol4zr9=-dr1erQ4G(AJW-?1RMyfqaBfg#(viR+6{5Lg zHDK~#)n&LNrvPNZ3bGsA|9o>3-9tL$g^{@AJ!MCEp#l*~#uV?L)0;sGVP`oydH8Jb#R-#IoW3w@Bv9=p4Wd0K-oZ|NQBUzjdfr!>`DJXyarD+ z&=2daM5Mc3kY1a2%U~+6!LY!d98~-8FUtUuy8F-nCWS*%0cHZElFBUy`IZ$j>D4w1 z5$7nWrMl$LJ_Ry5%QTiHmB82Et(DqZJ^yr`e~woBL{eMud~6cE94PTy7Gv&k8}F3L z8$1>y4Oh;~mB^FnA0F>hZvJGXvHc6@oS}(Y^!7B7MZZeM`Dl^3h>_7=BCp@Ro3TJ@ z69T^3S_VD*0+emPiY#4*Xr1lPGts%oA0GWPd+9gHg^qH)i(J2LqT(nS44 ztz~$yTidi-yaj|ro%7Gsbo4US%aicV6?mOJWzl39FHlJ2E}8CC;-{lmD`hZCv-<)$ zR+SHV8aZgXDiMDe8S4|LCWCV{TcshB!19t?bs;GMqf6Tq=S=4|3Ep%KhZ;by>p(_7d%VEI1(?!2n+7V+snIgPtQ6N7TE){q@!YoL6 zyft!)$$KZNL|s4DoWk4q*2ZFjfgvwOfo6B*C{K2yt8Uqm$aL#^`R=yNMK*zn`h3~3 zDouO1sdJspXCU!aJY!pa*Hj~%dkofrEk6OPka6f9T&E{9h|VlfaEcJnot==>+GI;N z&%Y&b`=Kn04 zw_(Q_ospkACsMr+F~N8c`6{(e>S!sl??ugrvV*Xea+F=Y**XIX{;PfM;T@m58x6VZ zh=RFxDb979gHPD;YG`P6fMm9l_JyUxgjsFnOqpacHilzVlQ1w%-cpu4QPZ4MsGxBX zG9b(cMK?@gWXnPJ94?byyydU zxoj2^pDK)pE#SaCHjfoJhbZy|aNsoSA=-x*`|Ar0WACN7_#WL|A3q9V5^T}(xp_$e zlGN@1mzq!T#k)n5$`bTCB~27n8!E`o4?Q^CH$K+6ae^$ND4WRKr_9AH1IWx_=eemvLH48B+oy6M0N<;`u3;&JUbqza=#w3Waz z4eLMNXhA+CiwJpcXUQd>HnAx6XDnD~>qwtEkx& zRm95BL2Zb#e8YU}E9>2r{>hSw>X=y6v&yj4*DmFyI&JC{K=FE|-SlXYpO)hhWUvd% z&dndU)jQ1ZA|to)e9%DZ=UeZASEo!Aj-xLX=ZZ%@eE>pD3$}*Sb?$&}1GHx&KhZ_0_rk3r6Firjj>i2YQ$rdZ7@|G*2@S0KZh$C&(r$BHt3V#0R)R$1i?NUYX! zV5k`>dZl-`&9d0&6i+8#{>x1dP(no7toL~M-S-P8%Jt?keSnVbJIi;^;kcbDkxjt- zGSlTQy_2uXW7cRx27uDj=_k;kOtxt~mojfOAadg+kYV!@Jdyp<=lZp*LHHz+WaJCR zX`tiX!Y5-AM$O8s;DmWpdc{JqrLQtlaB_SReNrmqewY4pJlo`49P%R19?92DHxXV< zk8RH$n~+{P_zlCG=%Rc+wfqr#{abg8aQp5|2jX<4zO$UC(>s0!kyQ$mv2<}W)~dD$ zWKkJk47=Jp5`8GjSBm4RAD&|GcNx#7HHP|Uwp=4U)Rs>E(>42Kh1WWB+PcL<=6;nr zmuPQeNx>b@^Sugt@0wjul9=GScM@eK;X{pjDJ8e;V=`q4#?r7R?IDk%LL$(Ep0Ejh z%jwdDz}DtuUJKk=*O+oe|MI z`*G7o*4K}#KKVHY5|#5SZFU}Hx38w|jC*Fc)aiLpygO54QjH=EeG*$I`q=s;Q6X0d zuo9%6p;*r^vfMGg9})GrTaVj;W_lA}uT%<9=1)&*IHM25vq1yCyBJ*X3EXCwwdp?R_Y5 zv|yuibO${VaoF7FtIHDGTp%74$f-)nw(e(BLB(QQZ**zXN#U`yz{vyXwK$FYw2;ZQ zgYJoy_GFokq|ZVpdMnV}mw)xU3QYXyJXwK~9H9V+@epKrKJ7oZYQ9TyyvCEt>?9O* z`v9_k=GS5|fvbMFSCa4`2*#+Hm=44c<5L%8Nm)!b2ad#v#oonROia=*d%qGkct-hP ziGp6Sw|=p@;Z8tbk#07G$pIzg+nELrXHc;Nx563P>yD!=$TwI4#}}K0$1;8<%*A0o z2jievCbi<^cW`U(OVuPmGvvFSX(-d;1k?-=ks4VtsLVF15jrs{`f>c7Q?&g|LD=;C zSu3mcr|8J+{*40Z^hOJs_Ta539YDXLq4SMaQKWI&8S68X$$IlyNu#1oL_IvJ@f{k@ zv%>J0R0mT*<2lDFX8Phtg4*o+CqwDbuD_q13-kL} zCDrGmRys8nI~x=^m|I1owcYJfjdt-&8ZmShPVegc(~HmQs$+rZn&~z@AhO#3cM`%l zu_pqwsp{Qq&k?1%SDYjdP)N0&JSglVrd8kH<8;qe)-0REDlwGM1lMKvPv8?tm$5cW zm8+K^59`*8<=@;S4r=IB!7?$?-)-R*F2|lPFst=zurEP0<@Y$t3zgM}w3~ZLFRgHk zi%4tdYb_Ir1`|il`YYR<_lPq+SD0JcE>tEQ`PO-C=53t8!SQXolNPQKA#YbgCnrR& zI{*}F`go?~O5ZIM@AUJo2U**2@1K00gI4j3^B1t`5{J|t%gt3|6~_JESWPe-})#Gah~EbkG)c6W|hz6 zl*}Kw>`eX=^m`I+cUi#w>eWUBep#=rkv%kZ~5>UgVHU^s}GnmH2XzPX4zCA^&_uwfjyN%qE0Ym z!)l|paGH7_`g9;{htp*3C&kGG-@8KFww}#N4TET);)r>LeCpWKJl})D9`ZfQ&u9xa z%dLw&Ht`xO);e{UuIMD^Q|r_1GmT1yFSG5z{TDxb9w2NfDKx+$>{A?Ip~iG2#PDj3 zbP+m{w?~mMQfst=`>LRw5p z>otM?_|};mplATYB_HGp(?IedN)&#PB}PZx(7c8*z(1fE3CODv-vS{_vcN#*j$;Rzhg!4H_MRnS%_8$ z(TQm_82#Y8c+uRm$Cxxie%CVX4w7|(8;M0K>GO&Tg3mYK|E*eTYNG& zQoK7KTYvtbT%$cH*ZG7wrS;iE_0_%OnVe*Of21c7Y*9+}ibk2g&rZZL2@$*=;50O1 z@Qi@?VJdnu!19eL?>E*l^uc1z#d@&M{xf1!BZ=p01bWVf0mG35h?3^;-m>Lz>9Y6JD;?BL z{?L_YQshWNYXFkpOLEgYZ;y=KY>^i2J#lj|IUaUQRdXuIgWC4gmT_1w=~fJ!S_HM| zlQLYiT=(^47_GPgA$KQ4F1}LyS*2%8KACdY`pmx;c1l&!2Yt43E26eVMvQ@d-Jtw* zxl>^20jk_VrQR4wcy+Av`*d%LKZFUDNh80Mu+}DBVE8udd0T&R{7$GTAN|CzJ$JVA zhGou1lWkip(4xn@N%PFD;-<=S680y9%N#QDoqwhzPVVc~@rcMYe4&Z=pc8r6;8#{F zCf#zx9jIAC<3RcBaZ}+F+@``vu?zVN@6Qh59?%uP-}#A6>#MI3=KaqQ;9Lo4^`AQu zFUW!rR|jS-J*XtLJ@4rV99sr+wb-0; z%Hb6`xId|(QoSt9N)&N5n9^kCf4Ksh#D9J=ti!Q6T?9LXX7>Kz?JkS}xeI_y$OXgq z^j0p0FZXcaR&zv|LTek)@%;Wo!nW87dif}gB`+P*!iYFKXd#ZCQH{(uJ7bYs#Vr2(f3{liww7X+yH%HiiBnF)E#P_mz`!Kw<^!&kD!Fa<2PY~&;Xz3mS>CwFOZBRpy~O8iUW0*e^d1w z1>%YlF=VgoPM6UOYuDNjuWGr%s}we;MZF!aW#Sp>Cz&&ucBP{mWRh#G_e`O>7E7KS zu+df&9i8s7nff=EkA1r6ec~rh>5E6@944y?o`v(`Oh;}nN@8hu&lqjroY7wtFRNiQ7K*oy+TtA8i^Qk9^`!`~jH$Q9u*)i2@`= zp>+Fx#yMLcV=E$^iE#Vc9V*-A?d560XQh6+508rMfM$oX#v$}ue4T;K^X2i4s^A>i zX;z>R(sa^0bHHC73#tfm?*}rM*V5*5)CCR}uU>0GT!Ys&uF~f0FU0j?@ytSZ>;2cg<}>*MG-#y((6UV6@Y+h* z;N=b^LTqV5)m$5>x@IJrp<4#1%UvYAeG++tuMdBJ^M`oN^W1c{9=y zSPN8$DwctBf~Sr^zaKLV=UFr^6N4ERoS&u{7|ik7P*wVa7s47Gz>AmFZ15)o8gcGs zx$|=E4-5*lxwQ(3%u*v1UN1D6)s^bD$lRSB$yO)16&nQe&}}b|&3iEj_`R214si%9J6q;GY2=F_K)GJh(y7|AqBwav4MazpuE82l20734E^ zQ*B0RV0JUbGxt3L58f6kgGlNedYp$wFKVpZYX(JIsf;s(80~NJ!sQfgsA&`h3~s-E z_~A&WBMsL7bQX$zB{5}e={*{CGnJ=4M3K&(4{4JK6;d7Nu={?12#ZpI-6bYVFJt#j_ zipCgY6uOQ9t9kF^pH-WoUGCzA9ggSp7U_DjjNKgikrL1)VGl&PuI;@)K}ECx6~qz0 zDj$x@Ap9zb`94e0uiC|_J}}!8H<7dg`<`e#_|D}a`h~<0dr_DC%bDyiu-N{CKfc33GW|!c+a+IJt(!Hz%)& zgv?aSm`Gd>lvKEQz!)m}v%MBBr`%&(YqA`UaaLe8Fn7qd(7-Se)!P(BLo%h1WVPZ4 zrp!rlZxeayr5cw%<)o<=SO0;{cF?J+<)!JgITTnericWh`YCp6a}UR59L&iE#?W8t zn&IrKm(exEubzS}+{4muXD2NgHq{#+S4i?C@H64kdW#g!80P!70F8ny%hOIUth(c4j*g8v;sV1O z4Q|Lz9@WF)DRRvOEmSDYI4Ro|NV=iGY-UVt`K@>`&72$br{INH( zE|DM~qqSnFg)v~RCH)4|HWsVVZ(EsjQWV**^&1_{0l>oxqZ)tv9ZYHN#xe{`t;^JVUvXC$u1!X2R*`ziWA#e=rZudC}(|tx!KsCXW5mRQYwmp>z& zsmNd7j2)9fR+&&hr#wnI`Wqj(u zY^NDu#}uPXGp9OZ(DWH^KB(B9;pL>N?1~(3CS2JH_NP*8Mc2Rtd|`Gn)rzaVE~#&X z6dvS4`pX#)@BQfngn(Nzm^t%3BCg;g%BrJjVX4fv&gVcx_yQgZFrsN2sM(8ksFI=g z;(o|KdxXOf-b?Druzwrz;|Qo!fx}@Mf4w39^Y72EfK%i(%D~5e7j*O%QFL~D(DmWJ z^RWK!5)k)H|BtaiLyFw`h@f0XixOdE|3bCjq{6uOaQlhVL~X-_cP{CRg7tHPFNh#= zfVuRVJ}O2K+J<8fHH~E1;>98a2KCyZ+S7~8C&Gk#7M%y#0T_t!q)_4H7%Ia@>8MQU zJq_1xFxQK*P{hSgOsZHbs-k)x5Y}=oKR5=^uq%O-G}5dT{7|dr1W{w!9wnG^`GJ}d z5R-wGmy5dA?tq9Nh>AnSD#+iuurJ7m8H-0$h!Z3H*}JH)tsyuh8YaA3_mCPf8tL!o zcZksi8`n5mSy-F=2!Jqxeqdr|^^VAb)%es#EP|nt`ZL?Wc$#rWBvT3;++?=zF#QD?s`9ku0#VZB!WJdo`*V zk8Dqc6T$dR;AM<>vFzS+vCUz4ORQMlu|v7;?X~ytuhe@4M2ai0i@=M2YePX4_&|CM=V< zuMMtV2y7#zUe?ffOUxJ@;<&uq%=x7`{TEO!|G&+J+KR6D4K>; zS|;S1f=4a@_Dravw_xT+)amWOjf+EN_ zzm~9+qSL(xz|}*E%8jzs+^H~>A!jU=P=2Dya|M}ulTRTi(#%-0DY#!lB>c^nRKQal zhfG1;txYo?Ab53?=WK}G#s3|%sPC=SMY~ggZ2IJ5{(U@6pA@B_!Kc37 zRPen|y_5>LiBZc2I0;xA-swJ4**h_BzQ=)$Yc@*x#=Pl%fWvGxBX@a*dc(nC&)W#k0(Eb(7o_JZFk&*6(tOzJ6=TLD-`Bth2>PI#ol6gc2%BKe|6_yAr-=?VOF)9HPNg3{2b8z-*h+?zfTZc z3E;lu08H~R0Yy-lCoU0P3KW<(Udzbp(@amZ%meN!OSg=BL{pxjhRN};vFq5N%PitZ z!pXl9jok108L-e1@n66}?9h%LQVx*^qlb98WI3s^A<*78HtLT;|N_KE9_PZdA(UAsJsAw{&iP4A%I1 z74-(7$57is!#Kp^?EGk%t;yvGr#$WvaM}?}*9j%6{n^_`a4VC#SzgFI&F;}muVVn` zRou~+7l^w-nkZqwOI#Hsr~+_SNIR@BG4WkmF^1fRkCk(dN{zjk`R>GsFcN2=2` zoZ};TcfWc5(9A^eEd08ADq&AzIQ1B~MuWU{gNKGIclGwrPW22keYVrkR*C3A5oWEX`Jko z7vD>psi!4jdO$HcDsKAAAf+e(3Bwl&!!^pcjU-I(#lS}AVl?}eJ#{R_L(Tk(+_kdl zwGC4S@K@b)unm~vxCw-2golKThau++HKaF7?B;-f62YvBgf9luYyi+x;(4J6u?g}w zae0YAJ>7-k;Ce7wtw!6&4!SPR2jk=Zk2Qq#P*XYkFBk0G>W#LqKT-eYAEfZsj0qwi z1sjOC$I_;Q-8HA*JVO;j0tn65n{%{t!60Hla!qKtKaJT)m^G~w9K4V@;$}j=#qqlB zD(4BGyymV53Gpg0fWY5Z1b5HqP`GE4+AevVpVesaNbUg+>W5HY@8!g!PgbW$Wsk+h zGRIHZp|K5m8~X!k!#e~CmhU|K23n_eRm0RC$+QaHB59tYazSgpJZBwn>ea2?mKHAU zqI**&e~Qv*c&BX#>H_1uHt zD)OsFLgDD|pe(qcVq@GBg;p~95|)h1&{hEDs`yb24KiyaF^*oKAJ>Ni7_afZs-aw- z3#ecDItvm;!RMvljh*gM0w^3Aanf1`oi(w21c)t#X;;~`jlhD0O^1mjn!l#-=5}xVREdo zo>%!)XPV@0=k=lZFRfvg+3zEy2pq1Z`i?I!A6IdwuXFTpJjc{Ir5zTS;?UgM(A2m` zT=XS-n;?DwDe7Bl!<+Izbu4?g;QunjUi3e|qGc^OLQ?W9#dCGWtYCnTb?3vUwN!H4 zuem#|A{fdU-|C?hg2@>X><9|{$%O268!Ms-CViTGI3N!iDJa;p$8pfkf~z|IoIaMG zOLTFcSwr0`tPr|Qizn-XenpGHRkg7 z`qC`pwbs)#5~Z~o*}yxHk6&_NJYt7Yo^JaBX7cOxYTYGyKkZW5`1b4?YaS)+f#>t8 za0GqK<6m4nMI4b2{=mTv&*fh%)wH?s4_c3j#GL60d^D2hKFfi=2frDDmR1xvGyupe z_c1s$-q-2*#Rh zJhF`RM)5O`wT@+y_hr2r2U%G*XY<~mA-K0b<%+XNRjnElg*k(Zu>msN-JROM#d~uY?_Y zVXMh;C@nymWB_S;qMjTjkDtHxtofGvi2%c?nI`jMj4oKm3xo^yPPETC@EFsi#d}tM zn`hk6`_pVDo4WN;NCutTteG(U(?VIS{o?LBT*Q_)gM6XfaLsuO`b4*1ebWPmWXXw6 z{6Pm};&iPdL9+a5-5`ba`d+ZjQ!kLiI!znNqUJ>K`EA!)Qq~Y9o;k{OOJZaVt6;+* zA*fX!ZI5rL%GsQX&>*ujj}&BFP++IKk+htQMv75SO}R|sGWRQl8%3xU_Pury#5(OC zTaow@C>mPZp~R_fSFmYHBx81Z4KN{sthK7Jy_otM{eVkT-hbBqI*2uTS~k()C~`!HsP?NjE65o3mg+XnHn+|HN+%)w%B8GQuW?&G zkkPQQ2F{+&@wUTj=(?j`w~_F$z<%j5qQ{C)UPf7$I1`Pr7O;e7K24V0U zq4P-_{i&;x$Oe@pfFT%p`MB|AW(?ac!ez%#KR=eVCNZphT(yghbR*jniYTWZ2Iq536h>)AO4JNA~d zq&?UAP8HSb51s?E*b0!A?22sSzzLjQch?-7HEU2wDK~p8OP<_ByT}Bs-`+NF)P?Pe zY`LVPbxX3@!d(|vR^W=oMP~fef|``Gn+dtEbzxAv+YXt< zs3fA?@|FL3V=P27KQU*I@-I!(XN`bw=a}5rbe!I^iWKY_1?zyP?613MD@33Wxh=WvyN4@(QDIK=EX|}*7Z+PDVYcto5py1X3kzu_AsY?6*G!`+SGLdyrFskm3TE^6ICup( z40hq-`3^?Otka#@W|R007tU@p;WuJCHclrT_-M$0jQvFT3o2Uxd9ZUK3*~BZ)Z3Df z+)@?J>^*U=l1tn(8m-#>s_WS#6w~dUM^&E^HgU29Ix{nQuG>G9df9x z$B%>MJblcw35zLIm`hGTSVXOib~Elku`$*<*}DGy{%P=3Wpz7~B7rCm4R$BT#q+O}TUo2VEN*KaQ{vq7A}T}PNMIqB9`9LOxfet25c)}e8%aFQd~weY z+O~2tGK?lqb+Z3dp8zAI#`bn&iVKkGkl5KtqjVpB0DKPd{KS`yjTsrkM*sI-*V_AP zx}Y+T)UfW%OJ2t@d~~9Y*TkW@yt}Zgb^P-0`sr8B&^B@lC`_2WoW`7DWJUfQZ0QO< z-!t}pYS9iK96xYH)!?WGl)vV;KMq_Lq;Zd%G$aTP07;94Y=8nfuB}L@`c{;G?2&c( zAWvuhxt!2$HZ^{Sc~dE@puJdgTB$upI}r zvGaYU@RRyQQcL=EA*!dMiu~@4dNbvuf-RZ}b`<_(v6_c$b2un}K$LMuReI#n!M2;G z^mVaX$2)dS{8ONAH*r?)Ec+A3rb<12XZI+?2YfX!Pz8JJn+{-Kft|VA-*DzAfaM7l z2*v2DEIR=B=(MqA1vOH+e1_I~`t7Mk!b~d`LzbaR52!RoV;UfWR|55^uK};?)+U3sRNpHw2`bI1WtQ$ph~7>nV(x zMXP<2DR2Ir04A3dgHGD%d0UPAbh#12NTb85an~+mpHX@|DJ~S}Wn01I_|RBTp?v|I zTi3qj#u+@C#>JrF@H1?Mf*QHw1&3-@ zW9GvvRA!wU@3Ae$Lhvai_26mS`f#-*!U{~e7jXKIG>yUQh_O3lI z+|lbE${)1}UkKccZHU2U{P^L-kT>$VVqPZD2c>_fP=aI5T8!Gq=T- z*~Y~S4$AIynj7jGBW~id6wWRgSlk#ty=N-O6?m1Y;|tY81|mM@HvQ$V(zSH}TNTHV z+D^tLW;%Cp?gi2W_WfyniMK7W#kR;eBz?#gDcF}B4NSbFze;-G%+6b2_tjZPkpiI4 z13U~a^b1)R8HX6bp-GTE-5T4Fy!(OT`!a+&uXu%;1}rzZ&9Q)sz9E(4Jw0h_LAct& zn}XED*aZV}W_BO2rW5`>MY+=8ZJW|~ofSaiwrCPL(zguh`Ju;&bem{71<>GWc1~3B;KO>N z6Q9anl+=&<*~s0F-Q+be>I4xNvgkeiI{=K*zF>&5b18eC*nNFH)O0(BvRM-$p9Lws z{QB4AB*5T0+P%Tb-D6uccrm?dyL6gJ%uum(Yv@$w_7?z6D!h+NlUD>%(*})gT;d0Yyrg*~TT>~v;C8&|qt8~+pl#j1p) zW0|)+Oncz%JvYvl{&7HRIZemwb^{24@x5c-o=aKa3~hfcpRAD9x77N(J}%7qbmN1$ z4Y~?ZG9Pp0OhEFK6&dVxGadS-lE_e|1A=9*h*5Fl@8P8bq!E!6y<%g$0>V5NNQ6rC zXtS*$wk%2tbzyMG&k-e0#|uX z0`gqrmgC_VX528iY4p$Z7sGKOqtZ2rfq>152Z(9*!00(8(06||xo|Od-VCuuzp?b0 zhbgguMGx%me|_j+Pq`?z1KLL!>hl{rwf7&LJbp>4hvHdQ>Blr<`K6~WJG*1j!N}1g zU{36Z2kK|I_rtNralRF(f!ox#E)g`_G>Hx6 zbL-aZO1Jmu#BX_=>M}NU@IH+*O-_**I5oO>h3^_3a~4Y5IP0F!C4mBtP1kB!>fHS< zUmknZpP%-K`<|Xp-;$t;h+;`&g)s<-& zEk!CyeM5av_#Z>o+G4kjexWikIjcyUKR;k%_%@{D@*F-srd$XfcaQs~uG`)~uQ<=k z#(R!(7_K9HE*wO6e|5l%Jb3g+xV2`q=~HRGy#?llRjHq=T0> zj(NH^6(nA~H=jWlnBrrKU1ghlzI(pyOxMOi1na;Rl}6(ZFkSn0W09aH*+^Mw3=ZF7 z+(lUNlajh;7V`F0g%M}Yn0?ryx0r@twd+VbKKI@)#hWFyFW%QI38JGhV^1jBinVW5 z?FtSZC2S>+8lKV`7s6>o?=5QJ5^2h9+FJDUkl7@z^U`B=gG+o1kv?2eA=HIu#kDr` zpkrbd<;*};O-*sjsZw61Wy^}Kxa8zFi%rhznKz8Q1>cq>{v>r8Za&2)APn$zT5_5{ zFg$)(a#Ty!zU?*z&{Jg-e%X9|Z0!L<*5xV3@0I6+Qa#D6VQO|fS@`?I3D!+vulyeG znJFLBIZL^%hScd(qJ<~M}8PzVtNdcS6N34^rRjjr1&R59O=9(qei?Cz)^>I zY|yEf(bh=79o~}}t_F$-SB-7ENH86`U6k&UTxv5=c;S3v*e1cuqi$s7Rr!)22w?D2 zicK$M+`esK)_C2*+~zd99@l}S2Gp&UY;Txzy2ByioW<8pIOcz3k-Cd7$mTzthmBNf z-yW%-?&&erEHTqBbkU2ueOszT*{m8qvN8ISGGYz?b-z9!@%dwh^BNw5LE*7uAv|pk z(`^Ief>U+qZk^5jU#_iu*^ zp5WJJRq+d1sZEqpGM8qr+2@Ff$dTb7R9zH&DK)eC@63!vX@V#_)GaYcBP}q@z+k7t zTISC12$j~9>Y;MEzk**0qxV`XmY)x=Bqi?E0k$gom}R7I+n;o9sEhkyAn#`$RXbbP zR7&V3M_O82V>(O>2POEe*w%<*8~wMfP$Gl>!`@do#nEm11__d2fdIit2*E-K?iMV# z&fp&0LU4zW;1US#1fRj(At69;*MY%paQERg-?`_!d%knuU+`{K7ey6J_w?Sqcdzx6 zwT7t5?Wa@&%x`9YoUwHL6?^HTa`?q1u1s_zldu{51nfsYv&x)E+;RiB?F^yA{6A{} zz}~US9Q`Bga;Gy&XOP%6EME)pEj+GVO*{+IiCPV&5kZ}-h>4x~b26Ly{Uv$jkl6=w z^V!n!IBSwSF^r%V*4NIce5O=nv;>@`Y7-hei?{^Clwavt3|F1Z|R9^8YOH!e|cQE z^dGZX{sNxqt#=rxhwp&@&q{1lX0NDZHhA;)1Pxs*%N5(*g9A^}ynFE7Yy&jp(Ifdn z9O!SPI6&a9?O77lX3`<&=JPUv6CK4r@8$v4 zc-n(ZqD~gEow??R;XA*LWBB6-Skykcg0u*72=ECO+fJv7>lV()qt-(Mn86VmqZZGg zU(o*n%smU(_4ulxqJgLYXY=3B&|gO=P}Iaux6%pNL#l~_rPeiEb(oc^upb*eT_!_) zC`JFcxaG=@3NOuFjzH5o$+tC;>jK3R)?bkyP1QCWii?!WFGxc!gfv`jt75y$Px zKP6a>0+@Y8tZy1ohzSl}2c#f(*}9kxg<-0RxPh{>^o)gnB_q|?Pl-)Ipz7otk$;`W z3pJUns1`eh3tXaI1+P^qsGVk(z(QqzN1N1kYicKN$SCo^6vR0G4fB4%{r6)z{Q=(F z=<_GicS7%JpWvw4mv?Bb1rgrA^Oir06)WD5Z58q93BM^7at7LU|MgjuOtEYMbp}ql z5*?XOP84dnV9nhc&137+3V)c108e2NZ`#j*dwKx4^sWbeZ2vAVwM?aS9PVEuxELm8 zA84!Tse<~~^xr=O0q=5dO_4c6s z3!lt*u2N=?F)<)_uHZFH@XrAV_)V{(rehAf+|w6?^>#dWW`-58YJrIH#u;-Gu*drb zX^}9R(y#{+Akb@qzb-uV#1fv10k%Evprsd~pG?BU939!`orciF>n@9{lnRv6 z1(GhAnVC2Dniy0(JQ|ps9(^z~8)dDVTa3fu$C@U5uPmQExT`xFmHzd2Srsee>uio- zvy0k?Vt*}|e>^S5wBHJlBo$7P<2>!iThGzrnKl@G@aq*0C#M~nMHbj+C&;^?@Q4~E zT6+G_N{+EY-+2XV6}GBL*h(ixyVhE#s(9)PCI3iy^|o{pXW@<@sHn>jS9qLKldS@+io=J(<+&&MK!3iT$i zEWIL3Q!`S;^ri6ma;ouG|0bvMi#KC~{53z7NI8HB@4ZeomD2Gy-=X0Pga|kn&R!8Wb=WN&?F!b8g)&6FCCc>K^BZsT^W&tB4ydly z@Lb=z!`RUg7k`dR_ zcKu~ppGQ9?L{y5kr=nM?g{Dj@{@{s>EizqM{YI{>dP5J28+^&EE8E2K=p8d!GN7_V zN4zw9lD8u-TZG2QH1AeZ3-go%`^wPUn5$k+=2bUY2E%NJ(E`3eY|vESwl*>H7vUZ9i|)9) z5L06PCJzuerBM3>JBynvpfRKJeqYQj%Dk)SZc5BX=X05txLMH+p?K4JgJ4-6qKg(% zIEQ<&;R^0u6ynHLv&@wH1}evg-|4KH7N>Ao5w0->k~E>9;pN_*opVoLe2A+06HMc4 z@(fb1n=>dBiI^(e80$FQhDLyZ@{Xsn66ol^#A(~USk8_T>Qr?gT}GA)(aik)NwLdS zT{~+ZtyFzb=ck%{6N$)gu(j&MV1}JC?gG66IMBw$Xa&%5a;$n@Fl%Op#ahM1`rxXn zacaizznPP4^7RPo<+<6CqmiQ}c!O$-V-iSwE$iYsHhAfvdKEJf3DR45;4-L@F#GE~ zg+uFM6aBY3Z_n z5%W+MCh5};c@HB3%8Si4w2gh>-U{8G&ZbM}M<^i1>M|V@M$%@g;-7D`*HoG(W*;F~ z%0>&Tq{BV+E&-o^qz?01m~Feu%?4nzzs(QO*s@hr_ehA7Yj zkPH@TIB&vO&`odNx%h`bfDxn*I2Cjw@?#L-KK09lFlM}K>YMyckLJvb#5$?QUe1aoQNkmk_cEw|i&oebH)uBH~5g;tSML6op(h~vRk z^TWqQ<%J=W#o7*BPGv1A>gLiIF8i}HZ&moymFazE^J?$b+72sck(xD=s8Glkyc>5~ zl~!kLVukCdN<2X!>PDgjk(bpK)uyFcbqfK*mo$(5W zPUQz|m!0Vm1{#?Q|0B`y22JlERlsUi1QjUs-UpX>pSbreoi8Znj2>}F!1EO;U{&SQ zA+IpqEG*1M!~-!dG=6|TCwC(7sD`U9IE-o*dHsdu7 zc~d%|5G9d@D+sfaUX^)Cc9muy$!`x|qz6xlRz>WQ>t3$8wXTt-3);~U^-jMEB@zVn3k%4|3Jn#bm1QlN)h~tP z72dFp?G}Q|M)kf)C(2XwzQ(Jmd#-qM``o>TxmF7xR4unt9gYb|Tggp9&+e1s> zgdS!w=wBg6QG=MoWo3s4dh`DH4u2buvwi8P<@WI5L+8E$-m&uq z<*`m_K3?8LYQGz7fh>7ay1~i2VCszbfb^edIWX=?819Eg)4)lWDFb<+02KFciD*iM zX|d;35yRGuMdn0R(vsjlr7}YlrBxUI!J!Pz*kd*!hd&tj)Cx`;gWR>pp`%Y^sAadT zthmm)C?qA7MQVT~bc&F-m4mt>2(t zfOm2rLBz<=eLg$lLV$rHh^qVOqf$H-=^w&Gf9SrX_F6Pns64iCG(YN_(wU>(yDq)D z;l&F&S|~JP*B{~~yVhBK816W}d7#S(m8uJI<})lQoE36J&3j6!N>vOA!!mt}ph6{v zGH0^3jQyCklbsOgowbuG8Hd-V@0UFQ+O;y+%`|rwur}+2Y7*d5bNs&8WGR!Am)}~i zFtx)4>0wc%y=mNhvCNBGxq^*!-xVsP@w1=F9-MKx7uVj^6A4iM&;DBo7#2?}FuM%WZ0tI}|L!G`vf|h4%>};Ai6h~VY_vez8 z%UY26BXPY|5C}Z3u+E`7N=Vq+rqJYTXZT(Ic4=EOVAAcLW#D}AuW04|7XmbNcbE7p zLn<1zU^Umo;q|h;EIU8%XJ-ix$Q||Bi%daAZ8At~Tz1&@ZwRE^R62lmQ5#VH77Kh_ z6pxFKx5FFcs*!T|^R2_Ge?oIi(4kEcsNKswwu4Hv*B0%YgbYk}o&af%0lj6j!Sceg z`gnzUik%z!k<{G1Tw+u%ar-u576Xu3>h6b%Q@ty-RPMuKIX6QE3Wxky$lpXtS)_*E zp$48?X5{S6y-A;jgzvI5E&AWWaxvX0=mZT8xjj#2oZ%iDsyWgPjbX|cV3&(#7viZx zRSPnXgkwcIT{~<_o*h2U1ztlxyCD8{mZ2nQ{S(N%f!luDUAC1wxzA*BEfi9ZPft(p zyy%atR))4j4n^|n<$x<&Bt&m7aDDi;SQ}T)bo(bXY|F{L#_Tk};D(3e+c?7_4g{8Q z@x$uXycj4Y2TxHn7eeC_GORhtfyIwqUvK`MVvnf&8KeKb?kakJXWFP!_h{NEJ|$K` z(9h@Vq_;2Uu8v?^*T8g<@1~*0hF#qGm3$l|gZCnh+2=a5I=hQF`vJkhvw&RiwP&$Z z4J_>o9D@;dQrd`+GfOKJl1vW{tUKwc6%ldv`5nqI{3nJ%)!aOvUq=ezFE)eM{flDa z5ev_1$o6~_e)^{)+4Y3BvbL)*CBa)~>yp8uS;O?1YRe(r)9G&=8$zMih(0#@LNJI* zkqogY?)v=|$Sz%Q=b&SAuxXFy)bk7>MQ@h(8-vxAMkz%S6R()-Q?i^FYNEa?zy8Oi zuOb_|#D#V6nFc(_{&)aRV8#K^{ag5y=3`d$m8uMl8MQ{+>OOt7{k)F#$d4mq#N=x}La!{Cx^bxX@xb_ug$#|)7xHZ-=S$+L;aWSG z74k-OoZrZ4Gb_;ha)#u=b1X*G^wff31Qopz3My@R@B=>v)P~iQoTB1!w{V{wVwOBl+E$d3=+#ppKS-v3Z_snh0i7vg8*pP8? zZ%ki_*CB=2g}P!^z_yy=aaFole7?>qi-ITaI?fkTJAvq`o698mUGzh^SD7_vVkT!u zBe+Mir1^1P(e)lIth8;9nREMmL9>GWD?p#21@Q38R=Wj$$;_wP6ic&2*MaV%-H)Px zRj^9>cx}M=H$2n~S`~&8qC;+qKY4rH{%R>y1n+UTXaUiDWn-u&xPqNYyQEr9QTB&( zO)oP)ec&Ux?PqKmv$b}qp4h4)hxoTg{T-33Z~JPISC31)cT}&o(^RXK>@J~Nl_8@O za?Cs$^c^b1HHVT?HOjPkx@^6hu(C6Xs+ts7#Yt?e*KM0J_xRE;Hnnsp3Ei?Z#k(PrAoS?|@tgVFJyTa7Tc1)qr8 z?aGf|8Zw?BXs_zvzI}}LDV}TVJq2m+*4EZefI?_VARNIxcg~&82btohzrxF&DXxG( zbTVMOvSL}UCfAZV!d|oDS*Lnm#a;>c?-w6cQZ+pN4A_fE_uB)+ei6P0@j%4wy!Wk; zM!6sK-{lF~t1=TK$G>3!zz^+LwaQZG`JmzqQZ=|HFQkuL=23kRMrR1&u%_08s$T(P zDlvV5xr%FBc;O4~^ovf_8zB`vJzGlLU(kw+j;Q0Kf)}jmau&CeR&vqN&tz3;_*0o> z;|nzuZhHxk4+HpKeoGKP9c+4wCkU>3^HVGDaD`Y#*z=4-gw(+O$$c)E;jvEUBu|@p zS^aV39bz_*1;9t7`^fl`%J6ncx`V6nHDD*^JN&xVSNNjOx*!e(zI4o7Ccm-9%B$b} zX{`VD`PXObF34)`hpL;8m{DrFB%NCHnB8oKt^F2WYfHiScy2YutkIgFP(iPgLh>?5 zY-I{pkq&CQ1%I5r^J1VsQ9uxx%jPd6S@j1ee%1xROnb~SUKX!3`|VzJ`$rB0E7^uj zX#ABm((W7fT&r2{rE2_J?zoJJ=Sf)g@2TTC<(OEy9HfbqoAUBieDO_`++_ccqY`;5 zx0wCZ614aLX96rNBk5w1w;!I4}CdCEd2_Xslm=mZhI`c5)ZHW7Y{WVIzs&y zIn=%KW@k(}bYZhQN|XK2L#+|k=46p_F)ZO+=(&=fHpAuAH560<@d-Z5XLbnidcSq) z#Lv$!($f)y%NYq$&o8tK*~r|ln%FPQfz+PXSZy?=FJ%YQ*PAT|u zrKRUeblt2ctI2wv?^Rh0{b(b(H3RdU{K+2A5_g7u0Pnph_qoRNJj~O?9RT8nAk2Qi zS4^J5+jgWFjfuCp;jSgT9clw_Za`lO6jMRJFc#j#Jj=57=fL_+gTjM>ZbJ|v`e;+N z`H7L~1HVPF_HgtIvT(QT@|AcsN>aM`i&1-a$%;(ZX(Ksu?=LTZzBh{n&cf2BV9%F4bYoCfwgo1Epm$n66HFT~7-AV;`{l@^+zmoP$>juSP>Ot(d zb$&#I^TIpO&Tt1Frx_5N=51tp4Mx<#>$rCd@Qg{PAQK`Y!D$;@{n-XBUht5|O?Tbr%nut#vA+ty;&I3R6LwY2TSuOmWIvjnj@QRFReABW@$jM5HGC$q5VZ+}*7Yt^# z0J=^^TXpsl2Qilk2_9rIPbyv7$hW#|^LVaJ@Sau24?#Y;)q?loEpbmOj&NhxnlCtR zkB2I)^^@35UkP6=FeqNK%g|T_J-LS;|8)eozic03o5ZImlgtm6>Js^j+`htP+CDsX$G@Irx_Qi`?i}+(^JTLOJ9h_Ibct(w9|b$Ib8KLX-+3wJ~q1U&Ze*oi(K!h>r{C2yKGJe>{0q9Ds%6` z>I$N$1g1NCME2h1#&S(6j zBZZvGIrj67!%@vbS7w6__psnVrN=2~d-6KP@!ud~G1?fccG~#&IgzcO#Rjl|>^W>m zn_Sel`EZ22H;h)l3mTZ=amHU0n~x|iDa9*5Z5*YI=@}!wm-NpS7H%OT@AoZ+t$)auwLDrH#1e%VRd+|7t7%jC-wcRIfIZ_Wuc2-h?GwF zmF2HIj#*b2h3C(A6*^>a3V?2XR~l~PBi$FjTb|BTnz6JXuVB*OnHL^x+JoXxmJrZ4 zAcxmQ68p`%4Uq?tybI5fTJ!OmdWd?(VMsV1)|YTGsZM*HIx9?KV(63vX=0J49E198 zWAb>ls&EY#O`AbbIA?F@83PoPg928w;b zUUZ1TLW8S&zjHwQfWeT)7b|jeU@EZ3=SBCBqUcS_iQ3goI9s77SKzQM%xjC4tE<8hSkRzIimq57j(F2h}-w;bW6<}_X+piQos#VF1JTlC?r}zz5grbi-fOQCH z<8N3A(_^4Hc#Yvj$;(6Z+qfKx6jQi@&!sEOj!9O^D$c%v@3#rW$Do>SE0@lNhwE%DqhS8k?!o6iQS8u%Bg6F)&cQjud+= zM%#~$dW%A~j!>b*l8h9yVu}E%21bVcc<1ky2gH^_go`wTh>6p^zD|=+k6)SE#PWe_ z6SYVoC+V8}8XH)`o5qV+>rYt!V5R>4vz-Q|xcEp0)2bBf;87&7L#l~C&orAzx;ojF zv4|pJ(9rw_nq#a+}Ia@%y@_D`v+Y*ic{;wh8 zs69PNNYjtg!>bR_)zl9u^d3mQk5WY~y64ZNgYgDjY1`QfKBEC|nQt)}fb-yM(M+pOQ?ytPfCuITy=nk7V!;)FvyuUS0BLBzFkEP?4|{b~183|O z8Q?+tLc91J6;LxQJH#+X2H$>Hh+~%F{zudhLpPicBKpT>8n2=%o9qafZ+O3_@car? zUM-@vAvB`1O>IdX?qc<*hL$G*q8B0x zBagvByCkZSMpZs6$uP0wSEo$cWck2mI?yKJ{?Il&AD1$+D#rS8??XT}(Z29vDR)Z` zoIc;n&)~(j25btE(%g19HqGbK44$mLR^NzQdR5f>iL{!Xb4G&E1bCt^TeHyq7Vpp! z?4y_CzJ<#?yZwJm^RpKKcZ!2clJe-kDB!=H;ZO;XRJhX^pZsM``1^ARP-7ut{bbPC z@1~_a?9TjIMYIHd=&M>H{Zxm%wVO}=FIWB7w0RSTG}NISQM|ms6(dIIUE~U!m0d&* zWxp!}PW1yE@1jvV=E3(*_w)7>=;#pu740H${fK>8nifA-74@N9!w@$jZX!PC^UhamQ$WkkS(ouL z2O0#tg(cnnmm>hCS*2pWBM5mKklc4<`Ky3MkJNAQIqT-{3`K?8s*Kz3N8!wOBAj0n z{5?Zy6&uO@pjzd$sr!1zG9lQT^Ur8!1a*3=A&^ry7na$k>?4%p#Be&)e)&V3M*4r(0Y8 z{ONU)dC@>6cx2hnZ8{N%G^v8!-Bd+f9M?8JUnruU4yFEw+1CQG<(yU9?t z`PP%@>>@$m3%sS==6%|fkyZ=J3`p>4icHllP- zgzR{+6;aJEgq!D5r)K13+Q#*i!DZ&IL~M=FZ*f{mbaL}NwGPVbZLQuRnCcHO5xDE* zA%jgVY4{`hg7IvizqDqnMQ-<;DP)EXy;<3RWB71oKL-fVo2++o?tEssG^n^wumli= z&!t=0+S23is+rVQQ8YL2X9>vW8 z5-66dO1J@PXH>$)wOSkA9j=oU?ot^cx0hkp9_y52D%{9H-7T$Co0fVBJnHwPhMqMs z$kD#CpIY=ri(a1}I5?Tq0f6k**E``NY6J_7$@E8;;dT_RtDW%81rT%#;87vw#X4=R zqqM_$@)qR2H|!ak_`sS_5Q_jc>+i3Amqvg1b;HUPi1hOqsQv!v5l=U{;ag8wvyuBv z-FA5+kNanUR0%~q9@mTjE57|F75Ol4-HB_zfnIGY9))NsvmA2?sH2gG$7KotGgdHo z3SiMTcKOdAppMX%d!Fhn)3i>nt#ai$*3A|A5mpmF+pR7{hjtrACjp1guQ6)UR-9kY1lubp`s z1{JB}RZKu01=P-1JQfQZ_bxYsz0s+(C5n%WD+-=}Y904IUPS}$;}$z&2$v}XV23{z zr*qCLHPTnhoql6NHMV4Uz{*_QNUn;=k05tRu_}zMH8VFK4rRdO{lRGg&LDnnwNs{D zH5pQCIig2Ohl5(9WORGg3-a@}H+mbjVUbZVGtYIVGp0gsM(NeBvh0zO-hUm7L4$7n zz5RG>N@a^$0u;=1TIdCBG{!bWFDaZkn(qwH!u+}Nf?%^S5wO-6GZCVnN5Gfm(j@tvE-x12(Y|?xai(lRXNaeOTZo(Q$+q6EAs$B&b zBx>Z0fRru*BqUn3a@h#gVYl3H&)5j`^?+|&0`>W9ooZIlwNJ2~CV+>bhGje~&8HWW z=1FK*Jnbz`9Nwb@C;^rPmg9_x)IRR0xEjzObA0gi?!AkZsJw^>#MJ9PzTL5*{AaqT zL;Tk`RnsY}+>d1!#rgpxq~F~}xn)P8hN|2@Yy=_fzI&+XZY3@F9-8Y;RM__*8kv zswR4?;o*Pfn=Ww06{!h2Iumm}-Jat0?a`}TrD1;kI@h4Zvyd12?Cs#?Vb2#VEXmLZ zZ7tFI;p+oiGA_8s;>{EjWS=n$dGAgUGwA@ByL&9(!X>|Mkx}LWuXVZ-8aft@0J+A| zhQA8fC@2FtfS_}GqZM{Oh(@;w2(x{63Tr%D>(WP8PeS3N0p^spPX(R{&@Az5?%bc( z8>7epA{!~8D*te56u8;qB%pJR<-ci1yB6Sy9h=soZ~Vb{sB$Dn7P`}Y^PN&O!rW{b zmoe1)NzOj-asr{(+Rj)@yVJCOUoniIE-`F_%Rc->aPTz1Azz6Tj`b&!+;vA)1Sp?i z*F@IlbkhRNlXnz)(|2V7@s^?uYuz0GY=1v7+IQ&SUH06gWQI?B*qYpH$7K=Zmy0J1 zneHK#RX&F3FOIAFr(6fdHas%!U^Ow_Is!Oa{Ft-~L1m zC;`4VPaY9*+uw02{umW&`^y#5g>5_A+_{NDvzi*aeByedHSed;6j+bSbKjYfKQrhXRK)Ly`3y zJPPT)vprmD>1|!G>uFZK>ZH<^J@CrUE`ps)nRScgvQsYXV6B812)Ce91Y(AOJ1A|&l?bc=j^tJ~Q zsvx=aq%<>O>$j=O&~evxqHC^fA}=2HSh?}Zp9O7(ie*094IJCS3p`^x$bmvbik++{ zt_L~FfN;~=;PjlXJOc~{n{!pa^2k|GcbE)`2f-;xs~_E`E$?0o*E`bD(Kv)JJ9($g z)!GS0n1IY1aZN! z+D#}v+_X6(WyNXZjS)T`I*7PbId{*tmg;BvcCC0Lw5Yv% zqc|@LJJ^xUZO_@)W*uCk^OcI`;xQ(w!ApRuc*h&n5=g>UI`@d<@TXUi;x=LanvJtu;EHpzSwR(lR8ADjNX~q1oAI=kGi&2j!$*hztW5_6S2XoJrXmPI4`|T=BX1A7Gs7vv z4eK}A=oFo$RF|_?<2eXtf-9I~06G-#bc6_^Hsea#N8HEuc6Zu75AXHnvo$Konl6pA)Y!!>y4sCMHtM3dZNV|nt4ndTEg zIbHduHRhGs)oo5Thl2D+w>2BV4@JJ0^khg(X{)YVi1?>(*a@CbXsY9~4_!}VSju2k zz9*X2)==74*0ljUjX9PKlyjG>l4asu53zMwtePGd&8=5|*6xGU>YQ5b-+)bRW`I&D zxmYLr3sa0&E6O5O!X|-rrhXSA1wm1pa=I_6UuU1@`NOVW}&N1jYVgeeR@#QU+WSm%)xnhLP_p%|j^JfVsk1o@` zd+yXwT)YO+FzYu`otuNava>3>d<^x%kdB zVST+iHZ)L$CZ!qoU-x~xde}?P@!*8&epgtIY!@jx%hxpY>f4+oHS|@a5_DB*o8#Ig(X8+Wp95p9VHth&Z0=KV>1@9{tu zD25AWW@{(g4o+M9ehH&zyB^g;A|-$>OwleTUEb88alU?C`&`s*-uw?g4kqfzi}IZE zw62yb(xQhIL0+3_w}rAT8f_D0AdDg@7G_PdL8OhK;;k3vT4LN-+(X?>_<5q*&(dDr zA~Ums;Sz@Gyy_2T^8g@(MRc*bGGsZn&N^PQjlyL#B*$D7i!YbSx#6bMW{h@Q2>!GI z;cIiLFDy*T_eOHhi!MEvsF69hci!W-kmKn99B7%J3Axl{SnC!zTV67vI0IV(FbUt< zn?3x7lWe-l%Q)+{;i?kRS%UU7HbUdDJP9Dkdex38h0lNFJ~h#boIM_|kPuY&`$_Y7 z3j%fc*u3uaiF~w5`7TkX4&L1lWRQ}9?dGQsoI4^{LNqV<8?et8qyq6pb9FvhSuxGu zW@3&*USHeI+q&QG)8VU}@99Igg{E~K%lw8 z)m#B*@8~X#Vz5HV93ZDJh)cCAfr8CW_haRwGsi+xeyW%OTKFUMSIU>YNv2+>J4&}# zXQg0cS631$ZfC5+XndQ=QtrF1MUR_5bQlzDpL+%diV~t9a`)Ki8lhqX4v+v03jK$F zv3NJ;-0&c^G6Ve@=NY7QO45hnZFl&+YJPbnhI z4_w=g^c0UU)Rmhze`x`V<51swcoeSZGGUO!B5I~{l3S{qFiJ9uUM(ebfW z-Bnq0ahjZ*T!fsO`WPdB!>D~?e7s_EvUXfT0tp;Lvanjf<$aBCa%^nucmZBser{oH zfeMw9|No05pi{A^`@}wJJ~hC~{3hS;PG90a_o=K@o4V9ur>BqS;NU1SR*Euy@QAqz zGj8T0At6C7nanEac^{hMJ0jq9{XZPEv;p;8;P`}u(Uh8PtFe5=R}T!BK)~MJ+hg^f zsfVe{I=8r3fvzPh>sxPMUr}*$1fVZe5_DTng_de%8+TAz4`&(ollyRRj54thNwXZA z{0|oauH0UQzTB$?QdJeVnX=2prI;iNp5eBRlOIUsrejxVpu*-cDp@EGs4XlB+;LCU zx3@PnJAiD|VhwHmv|MX=E*|(lp7u5RGT^;#BGW`R%>sH^XyI(NmEsn+kXKkF_^wpP zthm<1wYis>fgv%FeT!qfwtc4*qmGf5)#x*j! z$^6sS@p{Lc@+kQFfUf0bSiE$y%l4!uOD%clPHlT!+y`qFQ3?!bxxsVBze>;i{c8fq zvI$(PlLbP;rfvNUa-njFw+o}NQ2&Y+Rf8kahuHT+s&43l2q^A7FeaeTWIh})JEw*v z<9~cRzy$p?AXjUDH!QwcicOg+DMRww1@V+!g4k)fRQ?bOadL&+mi!v>FHEe8=7kyq-r|Wa?`LofRGs3ZyVBF zo6LrF&A%rjBO4*l@IO_!JG8jEIWEI(Nd-^}#8fnrg8J^p=?S2R^^&HLjuuHjTPQPa zcMX;|&eMr>@3pd^=)8yhTW&@>Qk-_atA zwGJKX!*CkXw#F=yJOBrvdC-#(yZ~0+x|FG_w!6|&@YbjqON7lDzFGU%|JXua5q$50 z{8%<|x;9MZDFN|Xckhlt;#JulFjwOZvKs`dZ7(00S(1~+{tN@4jKsde=@!AxWy6mDEE8h{>WULJC~^Wxtz!_-|2dD7AU8t9Z8 zMkd`(fu7;Mti7Vsa0Ju`nVqwf8S z0Y5m<#iV?hvtX{c{83$Y7);l|K-_F?E$&Cevlv(Jgps-hq>+ZxOx}iigTT2BeC}Zv zY>VWLo&u1rTMjJb%ff zR`1d@!6OH+?dnn)^xI*VnKfSvJQcmNtc>9~`n_Vc;KNcEs1r|mTl1y$%9{C*U-;&% z1<->H`=j`T)-5%-c-{eqD4P#9uv%O|LimA(N46v zOi=h}Q$6T4h-!7;xt_o3YCYAe8X%HZa8pYr9^)iUsY(R%wF(&@#g$q1#QGIFuIK^Y zf~_tX(l>)Y6-bj6ze;^|UJ!OugMz)tSZEA=cI|R-Whfn=x$KV4*yhyW7}W544LWdH z43@>k#=g6qfD{AXRrTZb_@G0Mnt3-My^Ku6>`xb5cpyD&IA^gdxgf@$fG+;_M6oN-u1A0XO(*9^nd4o*fUZQ?FI|+!k00Z7fM|W1 zoa->4@1H?(g@MW%aj|60hZ09krr`S`lWb^6}gc84aKi@gLZ4fQ#5% z+<76*8uw;daC73LKZhw~+|b`++Cj#S`z_}`QtewiWb(Aq?YiwJ5ZvYz(}gRh#iPA^ zi~hG;qkW}^Oo90A7Uz=`Y~(B6$0aMWrEIEcSG!!EyQg)VF~JNfL)7z1fN7;vfxCy+3Nj*v17L8cQQEqX{9aeN7mBRlPlXtF*2E4rQ$sW z*UBQNl(+j0-O!d2Wrvj>hRBaR;Filbc5~Iqp3eIu!&)FP*#7O<_C~qyEv4I>s>rQT z&dhYGJp&8TAkgRed)$)?qpLP-w@DG{hSMD|t{@lKriA zhZ*ZO(s$04U5noiJ0ce_9BoYCjkr!Ne1sYonn-j3+>rXdfpGt;1Bor*BI+!5%-wSe zDk#1OtxH`nZyb<*T*cC&INCt4l>v$iAk(B^d4JLzgL4d_=Sk7R1)*J?bK`U00Rn)N zeonRvJB;)Fu8O+rlW1Jl+baP2din;;B0ng2$Dz~a8@N#UP7%k|mYkfZ#Z;;K0#noe(LmkYmTBSgk+RT|d*;nb*Su7d>x3;eC&dcG zx3shm2BZQO4dCg6@%P(;W#%hr*PSYz9@B5#cGx8b_R%PIM3PQIGj(Es@{Io-D*y`N znb@1f`zy+xDUEf^kg#iM*P(xETPyLi?T2iUfqto;dvq#RQ&GeziSB-A1h@o7x|~ma z)S__14gx`l#6H!%4NoUd5O<4aP*@5SEBn275=DhFE`1$lXH>vZ93O>(c}-CCBOTy5Kf zM!#PYTyAi0iQo9En))t9p6+ErIO(|BwyQB9Mk3_?6VH{wx2-*VHjY#+gti*{k@ z+gl7u`->K@Qw9|PFJKiOG>v>Na*?fSFPD27^2c*+H4oG;( zOx0x{{%&{bIMp1Jr3((O=8o-K`SeQUV;RQ}9Ih}6x~+QHUVd`b`ZJaw_lLw~1OPD=_uLrL?JlRmbBlplv~a0qx7m#5o922KpvjWnMv6k$?( zkH3An*WxCxPV+a+Ct#ZH{R-m$+yt#wn0_{8D!5qe=Owrk2#KO6RUQ zol3QSCcGuWPTr(&`Rd$y=JQ_jt$7ub-(AZ%j?JLa)C{k zo_SoyRdc5T_`^q`*u8j7I9GU^-JF)$`|X0-ATT+EFFRA<&hpo-TdII73LKRIw(OqQ zvs)L*9Z6T5Sz77?M|47!rz;#IG&8=yzH;!`!vLj_|6Y^dkL?%qrYzXQrQt~IT{|XOvjzfth@4$;c6q3% z-M%(^N$l1|R&W8VS>N6sy3L3&ZZi-BL2B$ZVFZG(5 z;wn-i8*TppN#PZN{DX2l1Gc)a;%?l?1HncgZ-LrDUm_HH-C_6uE93+HTK}Dh^nikVqbkrXL}orzq5|yZ`f~5wST4WaoJgWYx?!^ zAdMY@)o`!xk3yk1ZCr(;?);GG5D#dG-y%-nWlId%6q>hxi+39U@Ru@Nev$d|vz=oz z3&HZ^gjm3q(YPaJqQJFvTGTKZhU=~vP z92B7bV!`J;5Z%X{$KSwtiT^i@v1PpysP6UQ$Sq>y9dyMB=6?E;qO!88s-)+yLU8{r z-}?OwWq&6xoc-g7O=&B)GQLdN!bE}D7E!B?TsqMXJQ5MZ7haDdKaFB)G zmH_2?-IqfK9sABOzX3?2TbY2&pQn4+<>Jf35*Yj$dE5X>Z%*Q?OYXp3<#ac7-5@6_=x%OhcBUbrX-jifC& zBoW_E-I9zR>zw$#*i`4~l%2j(g;;%vP9;#y;1?tdf5#k!pODB4q8|3}dqM#vOnjpY z6f8?uxOdf9GIDXc)4o+9^IPx&oHpMJG99TSg&g^CDO{iKmche%bRRAK>qYsGUzWqg z`fuYiV`Cw;b#ZErvY{4W&7Nd_E2}NzCV}GMBB!5y32eNBC4k8dPA-}T(X{Yx6Fg;LZes1Sbg)+#z^?5ZqyKx8T7Yf;%L*Tkyd>xD!0MdvKS*26;Ps?{m)c z+c_WLJ!?H{^&hilF-`Z~)pu20byZ#Hhiq4A$|f@zKr+wAYQ5Od|D}G}g?!;+9%mqx z?)yWXwPyA94xlWzmqZO%1^Z}YyP~}ON&TM03_3N*z-$9!=BNTrAM*DmJ5PLEV*i(R z>1WeppWZI1KeuvoDvYvDC|#gi`8*I>ZT2W)7Qwp$9;Gve-}+rXFy!h!b?yl!^Ii-& zcl%^dMp#bB_x^=^Fqzxi!-5DQUB~8i%if6O-tYR0f`ZXez}OAEjn$6e;!(=D^mvmW zC)ocp=>}%>hO|261T`yNr9CR=U?pFM#d6zDNmlvHV$`LBMe)eDLL1w6uSC+_JH$4P zp0RWhNjtXDh|xA%a|$nvUSZpB_ozl$ytul9j7wJ{YtU_Kdf&EL)-2NlRp7A`Qfu#= zzD(Bi324dMfzD)d*YONo>W{%BUsM8K<#CK>TU+FI?}KFUzY-^q$6x+7$$wy=<@_hn zNZss>-_PyDJQxdZKCKKD*cr&>gFm1BpK6QkKckMvpRBZZ_J>X z?!3K`Z)U5E?{br^oGzEB;ijSKV@Rl%5Aw$=mjjARzP-VEN-U}^HLGwoDN0+30R{PQ2$>yd7j6iS7H6r32&rR+#K(aQ^anuVpk};-)*K(SnAS8AXgz6F>y7R~_6I78c#kiy9i#nMY6nF3LC%oU6rFXE|&a z*B5$m-8k46$6{i|#tXOilh$mfQOZ584;IN;bkl1H0`X0$rYax(5D9TUm3n=Sq|)Pq z*J}xK@9Ux$bx7BrZ?LJoM9>Ks0WNqk{AQ{9DT%zYRwgpp?8?sikcPvZjnl@|hZULh z4-56jC&HabP#~6-$n?Ja9-a2e>h6X6Fa5o-mejAkGx4ob#bV)GrWt|gk8EA;`j=^X z(2o)Zf-w0ym3_pD2<%@zS1s7)(7jssm}jONM{awOG3&0ign#vd{T~$V_uCA3!FzY7 zsfA3X$Wl7{3O8%4r}e(|!zaW=W{`Gj`nIWx zZPMVhkBohCXcSppYXwV9Nl6M@=qy*qS((fD#K82rBQ$!Rh-UT+m-B(O zNUN!C9Mo+zqYi7`=`AkEbZbrN*a_Zc-{}Z_O>6clsO-#kbi zlNOD4_>2V19kkXnH-7;sjZ9TkSF=}FugINUrnflI7>GO?9=SSG)QH3v*&;wrQgBd? zBMS}HC2bFq*3R6cxt$XE9QWu*=j#&;#n1(|VSc!u-L=7sRh)F86`9PRn_P$b!z7kD*pebvg;mde&~rQ-u#{ z8&_22DK^Lkr>ogrjrRmZT#1P(oYU)f5BJE%_oKum=ln7`j=M2%9XQhk@;>Gza2;K` zp4k~uir~7T3HJyihBcQ0y~hrvA|;;wuL7wDb4_~kkJTP@35pQ-aX^LXKaU(P-#4^i z6R>otQOe-W`J=?GZ%59UbpNPqY&)s2uO=XxjPH%yYa+ED_MZV4 zDN&VNbDv;zMzgviOhMGNg0P6Kt4OqDB#8tpLeARVwYH6JiM;uNK1Zg3E(<@ix>`Ls zS;T{t^61L|;r#ObE>;vR$+$iPG-mg2l|>T6>H>;BA+WbKI!ZARU$zx>7O~r zlFbzI*^P7b*;Okyt3NZJt4f}m(poI%hFyG7<88Cq^f}#B0~3X2iz3mT+WAqr@?Z(- zZ<|A530$%kIt1u6go>pK*>EvJdb**2E*J*cMH#;B*ksD_%r1GoDRT2dV#m}{I}0FI z`5zA;htT&8Hzvq)qc5Dsd^~roFq%Z*cJ@oELxIFsCAcY$_TXyu0X~Kck$9fOXC*&- zTOQ+5yG7MKsZ{3t!-d}CM(3gV(PNYRRLf9V&ns9%=j$%1Up}Dp^?mS&q_mkz8WYVm z3g*&%`4MU{YC(xMaW7JJHh_yxzZmb`n5)L>+T_V&*7$2Ub%8gCHrEDR;4J&%tm6r( z#Qfn~a1hLW+3HYn3lNjr!UKzRnN{bzLP_cC<``YdNK#a9>`e~lz(iysxN%ufrRoo0 z-S;%>MQz*<^IdX?mjzxC!}YMs@s80$P+kXhvtbptOU^f%bTO~S@R7v4gZ^24>{S2XDi8cH{J)#ge?hHm1g z0v?wdFp#aC4z+Wl)2cVzOX6Ow(ry3zHFvV@zZA!QOVRy(yqkOp+7~iWgbWI|w3Naz zA1h5594*1*Y z19+s^18!}*pN1VZ;X5|AXx45jw+#TF6~zY!KjsATaPuEWkqTuag1a9iQ#e9-o)?#5 zxtEqbI~|LmWp2e7@E??QL{a_rOogzrRW4KTm3qsBtTF4-JCgiZ8hO(+EM(%>!j(l9E?lxvWrD3=5=;= zxX5YoOL+FnzceQQ(~N(AULAWDGG6|T{f#^vJL#w6)_d@$HSu8S3!YC*g!zwRCfRht zey-1p$u{*37Jf_g{c&VdgYtUlw5_KZlBXt%bN$R&Pn`W`Ccd$4CC~t~HwOst*>%jd#kNbZ=ci_WkWZyRem_M}#4;NgFS04Q7riby;16SwQ zNV^8Q3p-IMRPvWWjSJx{rUQhp)ApG<2 zJpnkh-E{Eq5S8?|ZX)G4A>d#BZ}Wk z3N*TwHssdJMHRwJ#m%6hlIoc%B$t?K*jL z=f^*<_xCp-Je2pAl>^@JOnNtJOuEf)5}Ea;=Uk4Lm6w&dop@~)U2*nXvtr*EyaY@; z`?9-gCc4DDTOydn#JgfZuere&2Lz(2a!$T~`R5w{{w5Q_FhYGZ_6hd?3WyZ4miH{x3%P*F{%`8U4qe{XYz!VjPsEj`!bR|M|+i`|qm#4_E)&Z2oI4|9!{) zwaNUy{+3XTKWp&33A1{%E?8{w;2wuYlK#09yVfAUky)Qx3Gkz8f45f+iY9z@b#zbT z>-+RWX9Dr!rXxutnB_5v@Bg*9aIaUxpY+h_8NW1mz!rL7fPl+u z-NCg|R7DEe5w+yg-K}28#=}?pj_kHAbN+^^>b^l$4*-FP@!GpVr-qr^-NMejKg? zGNx7eZfHpTZ!i2_3+|#M>}CDsb!cprH4IDKcBTv_2{_N!{joF-zZ`-Qa@c$)i^C@ z_b~wXAFJm*_omHekwH9~G_lF$NWvHHCRZu%?00^ZX+4w%4b*PJ1NDiQz!N5 zQK>+Vmbv~To!Mwc?27Y23=k_00EVTq_>Cb(9!mbSUv!U{s2PavO0=OZ2jKg+E=SF; zWs(%mwtb{B22!=c@ELzi=|OO>aLp#aC(?aAYxfo9O;GBM*roEgvDhBcbY$Ng2&pRN;|8X$7wP){SG4loBW});> z+O{3WkXp!eM-r>qZw^qZLcnncb5(oxl=uEg>L9;p5*})9+a(+X$G+qDZg{H?nLbQ! zM-Z_xkVbolwdx?J-_;(Jn&IK$eudJop~3wU(kZ@%?~66__(yg8KaCC)I&Bz#kHoCe zC~_R6T2Y0r`thW@Us-9sp?9+B(b%!zXESJj@dVd9AimSA*%NB z>ujY%;&SH6Db0Acm!R)rk@p*mw>7*SKlo4J$SC--)s}bacNg^eF zCz~2raE1N;D3;xHCC;IzParywAkh-P9~OJ>UO#w_@LO8#q3w}hxa`#=TQE3*jf%ha zU(REZvx1|EVZOtu^?OSxoEge(mQ$q;!U`A9A)gMtmJiAUwLOR>j2JrM?XN4YNZODy zHXG#s+)QODM&6k$&yBsJkv~eil|EZ;fk6=u=bGf@GWZz*E%H;^H`k44^lD{NZ4ds& z(3GsHGX*R?Q$FX9hY+cT&C!heyvsOYe*s+|*>Onx*s3rbHl=!%v83q5k>8d0dEx>+ zzOWy`UVB%*#G2n3kfqqeE=dpVoHTk+8Xa%guvQ3k)^{E65KxV8!lrm5&l8xBTjAEY zR~VGYnE0H&N0Bak#3neD(UBmPc|jtTPqHw zIGXG>^7)k;b5%!P3&K3hB8Y|N4)(0Sb_Tw-i+MlqkLrHbET^uw0>3Z)38TiLSja~R8L`^!H{DAoUm;c@R-qmaC6b|qbd>D2> zhr3f|;%ueADX5F@@Mx(i=A|1NkJD|94HlVwU>rpP)nF{#hm-lHd@%ZZjbAUy=_7I2FLByM?`Ohu!SD^4X=4Ug@@0{IGs?ohMMej0y)ZSThodCrW+ zoyw36LU}y;xWwiG(c|k-+(zu+xEXRon~weH4}aR2knhK14bIi2dJar>Yiu%bk`kBs zI-z9D>q@=clTGNM}dMCp=Ljz9J{Jl2+Mlht_;H_y>pak9Ym)^aUwZ1g~8Wz{=C}HJE3GDMF80 z=3~v3MSHbkImYz9%1%PP^Z{RD?EZS!W#4( z0HI4@0lXlyUis^8mBi(iMoCO6@7J;^@3)2Dh&+0p*3$)jY3C#?=S0!z3c`v5&;(7+ zRtc`_{0svf<`cj-1&~@D?n6HCH_*TWE-WPGbz4qhsdvb_w zY4K{GH+CwA+nA+=MOhk;@nlFxqg>H=JQUw>?F;tu)Nb$%^5SX-w|=`;0!82ovTNH5 z`F3EyeS9c+v@-|r(A(6n;M+vIf?CT1zIP_P3pMuwwivTW4e?DFAH5nxl?E%u>T{|+v2K2d^l@f%1&DaFh`dntH@Jv$RG zkdsptrdo9%D9?(2)<+KTA?URm0PMml1X|YzeJKizm;F(sZTmIbLffi~={?eZV!4$5 z$RDGLai!4~fQmUD%G*|q9E@&s!rLlyho3669&RCcJIENy^%QV~{w)5xc1a(gW8$wI zJ>R@i$;RNfWkCc6I>N2TFz@H`*MT5EF32~-XtT4=A*3aAA;>26ZhDLr0a??fDUhl| z27{-Y6}O8`S7NdUKG0hBy73)s8VaoinpmOn<#!(nMW_+F$AIx?cnXhls;3DDb^2?D zmi24l&^)_uNC^so@SqbG0cM6e$(|@SfTQ{Aee`+$wJ;8J62 zj*nV|LZE#Jgwx`eKK(KtfA>+Lt6B5{OD~1%DFW`P) z{5y0+@qHHof_%C^!S@x9V)a3w=gvm6e&0PQG$G$@$%Yh7+NA1H7FKJ?FIf@+3&pG> zR18^*?ZwnSm!)ntizwA4<;NTURjP6BFF9~Ns#ZNN@h(?{KZjZHancNA zkMgpY<$fUX#n{mKHM>P)A}fMz{X!M2F}d6urEa;@6;C0YJre{1YClVA`*J;vOnKXVsAxRR`YRr@+(0KsYe=v zjEN(Ack15H)_AO!HbSlI56|T<)_{rN-(TD=1K{*NFchalS*mmhMK0;uI-?-)C%8_= zJRQ?+N~ZLEuP~m^uxo{Cp#r#Ah3QyB=h(>orC%wmD!{^m0mItBJ-0ykGwIu`a{MaK zo_FUZU0(lQNeW*IIMxp(otvGn`t&*m5)}fuUJkHqVzQ>_gFNkxM~f%6GW`(Gi;_fy zUyiz1pphB!xDkO%*}+-u%GA5HJ#nnv_opTy$OytnLG4Gq3+S*AjT%oeg(uD^WMbj2 ztE@3M-VGPZlY z=h)<%OknYeLV71HJ@;-!Vtk|vs1xv_RS>nu+U`_s3yZA||6rYR%o+5xVM4fcA)CPz zsAQ&2dkdTM)Ru5)l4F&rvC!? zpP;_SgBuexjo*%BF`UTi=z>yi7??yG1jg^*UhE!+C?b9xoP=7PD;}2Dy8XzNP6Y#f zK5I_J{BHJI6UW|~ZNdl3;5VK}i$dV^61p!=5BD2Y<8)jR%8>_kdu` zkw_Ut`LkX3qrzyISRVV)BRnN$r60IN{Tp2Av15qNr9EZd@FY_`+ta6Zi&ai1_6`)v z)Gp<04vW?v51IPj>}FnNe)c{`;30Z!L#$>?raRnjL`3_oA4jSq83qK42R9G7?8XuW zMxl`pEFXC(;R3LU_(jxuRBF-i28t>bHISdw*9Sj=^;Y2MR&QAk+k|L!uhFS-{qUge z_KQBzm*~tyeRAyPj!?x7@V3CQj!W~1%%gB83jQ-W&qONWzG(zL)74QeR#6ecyMTL= z;q?8_=nll@OGpEL8vHE%z)!UWNmlhIv@&HHlCDSKnnvhBB7!$!jjbNFI!FDvZZ~n; zIa!l)RJo+l?L1$Vq2sOi)1dUERbV&eiX@ zyOs7Vj-#!mnNB(O`BJ~)HVUy(XODrHhxW>)qut!f@W7^zhNpmn>J(iB)j_aV{zNWn zN*Rh`SCW@het)&?sG$BpMoURK2iT??yBk+V8g_rRRAoBMI|z?L&%WH~B#oGCTb^2R zh3$K~P;*K8am9Nix8D!URn)2-;K9hS}KkuTT`_vQa2Pg!)g`tSiFQ9 z)n8(^!JJ;B)yg-w>Yi-R|1S6sJhPp~myjOpi}Jp3@w>wSeR(U>;{&XmN@Bh}Zw(-3&T)rYq#91+XUJ;RThGAMp*CAX{eDy9{KJ9*gXik%m&&iP z@gdHSyDe;s{_}NxKRTUm(G-pWRo%Mt<4Qzb7y9GxVcA+Qe9kEbRNm-)cx)aoD#cOo zj`O}p-rYlaYujD?oYt>L#k0lsCT6+WqqWZ6FOgyM6 z_cD?NYwf45=-%CGLNE+3TvG%XCHuJ39+2ep35lcUqwna^EmE=m^oRJ zXNWfRh7&MrxY?lk6rkK{jU19-rzUdSlCdtU4o=a9w3ZRfppn@GRi4f0dHjNu6AV-u z(KLsx9*d`fhf6(O>&O0l1@CA;8$EPce)^o-)#hx5*A(~u7aM1VB0kOsvnGfPn0fJtpInTpM3BmNO?KFA(72g49f^7tDZ|{n zFfv{ZCLou>sn>=zfuP1)tLDDQSR{_t4yQ4A*b#l3nq#9w?oO5(RN_TRS|184*;L`Q zr#;)`kdx$7I9$Vs%j%s=QySm+nHz12cr3n}=$&`khjR2XExAiaZtH!DE7C0uqosyN z@?NF8^yIlB?og=;uEip4lFHu4<%cr5>fGMVP!4M%Lh!*#bx8WoF<}+h3 z8%JNIfcr&*qv}jhQOcCXmpUt|}!7`+9e1Y5rkto#w%X zg^PgK=zv0K>A*L%nY}^pJ9GaUt0l<~kz@s~k#QbZM{almSzuKTfLKBn`zztkw-+0Z z)tx9ST|VY@s49QH{bfmCLJ%-b%3>;mvmaE?X$g+CUU1oAqgV50{_e2()7+Ei7zSo$ zG^Q|G&n-IZI}$>CIg9&(-NMdtz9zX0F3P5TlN>=Y^J>bo@2h5QG2i7dRyUa|SLvgh zKYs4=9^X5<;s&)H^5!yNfESaVPxa5|KCc%52Pe@~$GADMTMdP_E>`(d{Ct~l{k!n) z{cxkEQRNuuWYgs8KUAi}+Yb<6I99%&w|>z@$I<*u6>xlcNM%?S9jG@BQ7GkjWu;sD6a26C-<5_0Q2hNFC+UB^s!+{kJqH0=a~ z`sk%_TE7ts$H%|HULSPUX!Wdhindho89}RvLL)>kji>nuB)i51jdj2IJ?b*n(B~T6 z^_>>cD3`YR9-UguW+Mkt-iB0?GUowV!AY{BLDbXAXqifOLsHL~_ugoIS3)F>BRO)zNlKudNJrN!aNmdsdW8Cop z!YWH!z6?T1l{_HKa}GFjiA!$RF-eos^_T`RsfKc@2Js`+_gi6{&_ zz_*i>QHRp;@j8UHtiZj=)>dF@+#BRa+ z;aXj4#V9*M3Ud4R1`mRJDhH48bY$agjftfKi+vigUeZQS__i813nn@}w1a}{;4CE8 zpJL(K5A$pPgo5pD0j`sRn*##{g)wP$nYD!uK!JRndj_f(%oLDCD=aNAkeWR)IF>%& zlJo7W)>gj)W|=eI!=Phis?tc$Odo7t?!&qZ&io1)ikXkq1J}g9~ zZ&lPXY8QU?(Mu~TS=J+V2zUp)p)hTb@htnc}^tONXD+4STAt{wm`nhTo zRX_DblqOG^^!X;+HY^oD8|noDq+0lGh(wbYtS(JuqP_0*6NK z*A*>Dd=H@DSVW{Hs_(VMS@iS)t&>4v&%iF-PWf`NQaC`UdcOFm+eBR;pZTn0eup~d zriAb*@)q;;$XUDp=gmQnlkvPkcg;0KPBIEKQx%~kZ|Ohp*Yz#ja|l@!vE^4FhcSRl zRW0ENF$N&{6E{5g6=DlUij&Q>-58)~k!j-gC;5Ql4kf^*Oz}EBkrDX)mv$pc7Lz@h zPL+;bR?Kl5dNnUwRMjN7WJ$z{g5Q7lKackqPs3E7btRht<(!b$2o)mP zm0!OJ#^lizdWPck`~nzH`&s+MDp~H;Rn!A5w^A^abUMr1-akko-70WtNQBDA{NC)&EIPagtr8TRiY;Qi9s)lCM!wR)=;#ET^D9 z?iA(so%$uy{8702CmBBperM%_R*CWNAO7zZdynw@48qCV|5?oV&(9~n3yp0om4E#2 ze@^l4{cobkvp6@bKl1;7@UAu#g_1LLT=YL5)ZeH0?)yI^#R}% z1eY4h3+Vu~VH@*9@%VrR0a8x;q}M=se<<-uw#8jQXk$7lpG$wGQJ-<=b)|t#BIdPx zme!T7{r+^Hu?goAHSCKNf5|Cx4tsIz-WD6B?KE7-$5DmsaOMEET{ys2oMCrz4`{S6mv|8$S32Mh%XBq4YP({J)?cuk}&hOI8jz zW@zC6J~fs)()j0UHh8QzQWwJ|8!Zx>oK-5J90k93b+`VZ;JEahP5A#*04Y8_+_~Kx zHRZ*;6tgc<{Fc%kO`fU$zwg)zNNDEtSzZXB*4RubDdkB~?N8rRP)R3z`RQIRCQsK) z_m^k&{UNv7M?Nzckt}S0z1|m1gDXTvQ)whV^6A?#HqlEUp|mdfB)YwR zK>1-oEZk`g;7<&Ec3C+se<}U9+kTGx>`3l@=5E!+s54DgY(2>&U%6a-SH7fk6~-o>ZO$Js1$)LhNg4@VH`X zyi*8L&jX-GX2?+#sqkV+1k3BzYx@lCW>>{}g=|kAS_KLpLe20+)yG?pXearw=SV?Y^I6>0m97GSF@Fyjp(H+3+Ppq<;*cqm1hKy*rv&HJ+&|bdJE>Q z_+I!)G`tP-zNmD%PcTJjyMu6*aeodu|LI96cSfaQ&snkAXoKT!RhUq%N+#_3bi-mm z=Q9?OUZKKMTJ@~YxzdT#x{Jpg%N8SgC#By!-@PMotCqJ##lUdZEQovB8$T$<{AKA%wKy`KAiq^@$^zauSsd6gl7>M z#^Z9e+QZ$sPt@In*~pLfE2RDAf{X!rSt4#nl0K}XAm~u?v6R5+s*WnwjZ|xYL*L!;{hYW9$RwV=R)_yW{!sWjcJp z#83HM?qJ7ljV-@k#fV#0(k*6HQvPkr5|XPObTYXI&ex86!2H_rd~4*nlYQzLqyo8A zCXI|ChV!kFuK*zaJpnbEOPd&+TyL`!6P3<8$V{9D2WQA+q6!zo?EiLRd$^u7*hfJb zHGedT3Nub0C>DO&Y$O}CBjHQ3`3bp7jrx4}qyD@|Gq0*xNM<}=)?ZXNsQs$EdA^7K z5mvij(hSrprIqo-={TH4T6Anq?TPmfAw{sYooAG2s&B6DuPjYAi#hYd_~W_DMS-DT zZpBU*s38_0a#$NI;}y9f78vi~XKU$&$2OyMciPnycLIG|3mh3fYmok^fYy|BiDs_$ z44;dg3+&PUNNs#Svc5ZVs`6W3iSzJHM!AL#Z`<^e-LbBqlcv(9{I2RHfH#*JPW6$? zUp7V^E%_gtxJF=4=-EjAohngK<9?j4sierKJ`iSpYQ#pP%lB7g|89s1WYk0FngO(r z^;32?VET87M@hxz51Xv{mACNwb$-LFcSvalpka4ZRg(-i%^+-8`?miu5|x!(S+nVU z7e4*kQmdD^VDiDmkVgHHxcOLBWJ{qXQI7I=G*#n%7xdRIN7_+rmZ~yuxU&ZoB@W+# z_*_rEoQ3Rd&Nw242r_Ebo#OzV>HF(cPuw#04K8O&VUqX^j_#l47JQ$?3wUq=)FB5C z=fZLQP3giaZUnGdTrjx4tbDGzWn45U>Jz|wc;eUG#knjq{7#Vn$Y!PW5OlLzYGP=X z)HnqZpr?(YnQ2;M6_jaOjQJ}}dQake55q1T{hV&U-YqE^_vWiHFg=J?tt-_~Z~A@$N- zK9MQ?L3=crJ^0&hrq3J&0h4x3bMYsT?ioM_`TB0NKak=mqt154`$jgNCl4GyQeyi2 zIiu!#y(TlBg9TXl{y+jFq*eS!aKS9Sg{qAVqJYCCN!Y#yn$2n7YHh2BO9c(D?$(@M zR_6xE6%I3PxlR)`Q0+_}hGYKTW>0a)R%G)kv+U!sTSmw8M- z)d^EZ;|{;_66WKzv7mdBj9pq~Av{-!IZUrqUXv<-P&botD3PgxO@duTC7sw$w`r~- z8~Thz=Ft6{yk5CQk*dO(=S@aT=JeyiTclGI;SYy?Gr&Z?Uz-Wmq?;o1$tS0;i_|Nd z5zYHmE>^6dUdt5qixqWo4MklN!uh`DtYsO3L; zD{oiGo88@PV_wqnE@cUOwO~e{u1&pV|Jw(X(faPsln+`Cc+4DWSjnM`S^R{-d$yi* zI4D5Chbni&uP^wKARu;E&#F5k8Vlj5_{Je(QB@aqM)+bxYuPcg=+QfnI`|D-Tb9$~5i34m2{Q45A4YE+1M8 z@>#GvZNHSLGV>Y`%7Y-F;*K=z8|2G-s?&9zObx)v>9?qewJf#SiYwkT@G9xRXJ9X3 z81VE)z1W1V&z3Tr|Gi*2$i6?m=MKFxZ8F@nC=ae*+>0EpV{99^&1_Bd@^D6PmO2Ou zPBywoGyRt8KmD{daxZFA=Uad$ZR^8(dJ$Mo(Fqqo8h z%%^FxYaNAtgqI@=d35sf0IW3m$R8rr|~P;rb00n@Vm_yyM{aD1xrxXsJ9# z#iptg8>Lia&K)2j*LJq8ezYsg&9j~=k$VO(nWa#fR%tui znZTg?CUZ0+d)*%qQMFWY0%lE@A2@GaYMqkbFYd{t{(GaQ{-@0%14L$&w7|E-F&? zTW3p-&9VWCf%s)K)&`%Zt}Z_Kf10Qk=f3~IXt&;dd&sy9nzs%Ov)z30cG;W6 zcAHx6wmTzlWk)WRmsVy_jF0G|l6|Y|Yl?yQrpEMfdh3~ecw(~@nToBFsHM+cyh+P2l+exfR&j zwVY80zd0J7`As<;yJ;t{U9aKdK)g|X$psRc7D1Ji|lus)49OUE4`pA`<*Ysf(5hL zI-ZxS4lZZ8sN;9QnEPUU+7vDi4Ubg{T$95}Dmxul);bwCjW!(IP!k1G z{t_sV#by(Ts8unfmkLAqS=%k|HJL%$(6wWj zC&ZLvHnVNjq-|{`GT-f=@Fs`H_wz?@ud>AL8)0N-qS0&Vkj6sjitZqdvzW_!5?Fa}baI?}vZ>rN&~elm580-d%c%4v@~Cm6GvS z-=xpzR@i)L%gx_DqH0U8Tp(vXigm?7=-%sBsPV1Q^YVru%0Z{qbFT7u6e~bLzAI+x zv6|5!azm|58_JNs5$Sq4_}pTlt`-O!x)0o~^<1-CjppgeK36KeI^wwvKvr&=f)HFh z3FU7%2{!SZT`)=ctB$!khV#zzRg()&{(|hCYns!z#i#(iypZzpe4iot)(iN|`wtYt zPf?_)1vjcyAv>;ehkUKY`|HUl==#sGQWBArlirCK)v2*s*$X) z=nCu9!$xSV6KmReyRZLk@*yyBWL!HaSTng(x~3EPwK1_Lw&V1XEKq` zIBo8Hbhq?ObJYlHKkkY^S%z!)s^xy9sga+5>6vK1VEhV4z_r#=(^Xae86`Q~0()L2 zmsqDsIIRmPT|Y!`F5AdV!}&1~w>eAZ@#Ns;o)15L^5_LXQj&J#^co@T4f0CKXt9nH zUV#W|b>V!g_$apzW=`FUV&rc)H5{^Wm7H&|6Wtfv_}xJPlxi$dO)RHNYQwsSNtkE! zf!_Q?mu0LlGqjH9=r`SJZY+y~i0A|wGXZb4Lmb5ZE=`0yAQ+~p2jX201vh+exn^-; zuh)+N8~Do3j6%5_&fB!1 zlIVU-z0Jbt;06a$CD6WK)`(^tu7BtZ+bzhhZ-kt;k2nOh(F8yDRqNfw(Yw>F?S5dm!BmLjR=C1Uk-;B5+1vg~|HsBbD`p zRQ14JOLgB_!g&#rTx3M2V44cElGiBU4<#ZVuHXF$B2ccE^JOb&;&=$UQ(It{MFiFz zGxptEHwesRMkl>&%L3Hc6VFd^Ff%V_ag;MNpa;?{>cR45-AizZl67pdnWM=?q9*2gd%*mj@#1QN3ok0lC%-et{4_3q&qLN)Pn=N{{-D9)kQJMn$rBz74dXkJs?zE&NN3JJ==&0c+?u-i zV-1E#)=BdYfrDA_wB%`}#nq7zX=M)!=hQ@n5*wI&xmBms^CNNJ|89i%d(j81i{r|% zjq2;Ui+!w5|)MR&f?U(2jXZQmuuwB8qidM zPO8L$Os2fVh~BfQOYYRm-tpYij0Bgh1C3h88kg3V<*r;0$dj~?>;RLzZ#f2cuJgb+ z%NDW(fkAyCmojaeIqz))C!r?5grA8=7n^60VFzT`CU@})-W^e(|DEdvBtLm5BbH9& zDwX7|D^lhW%9JlQ3;3PV0exmA_U043gGun0Mc2sKWNdSkHo;h=On3u{wfW9BbO$eD zo^GD7QG3*tz$sz|wGw&w`~;PQkhhY`OYezdsnB(cX4Kq#&M1=%R7Kd+e`fMN#Mzy{ z52wO@JR3SP*c!b#tr{36yYMT~sO~-M__Efxx85VIilJP>Psoo5s;p;Oj_4pRv#g)d z@vlo|`zJZ==7(dIEAg|ezmLSWxrr7koq5VFAW+0FAg93P)#x~H%~N8X#mL>Y@I6u2 zVjYDj%wE8wrAYf&n*US3Zu9W__6dNy74XI(DzA?FGxaD64Fp`urQsgVog{Q$(Tnyr zdPr!Jm{o79Zhe>+>WpDKa}bmy?~jCCTcuPWMJrUUx2;@gKA-y)>!U4pW9Wc@dn=`$ z4>Yh=yLw9hxrFgh(ikK$o=)`(u;IIJ2D=1lER8N7ojm(?(8SAPQ4~W(;$}cf9@X3M zcR&7#1Zq2OKVh@h(c<@4PoIwTwRDO5iP6S!GJi%RVEWkO!28SS+1IXM?9{E6`~wZ- zpZO`bW^i_|u%?kkgNmLU!yk$@k0&zeoDDSDZ-!B+tYtl`xjdNL2Nkmff=*vZCoq&1 z40Vb2A?RacU|c3Ksk5{>MLMqy?ssGVV9~wljYWsx;4^A=8{T~0i{7m8Dki-Zg2skY zUcQQVm-8U(npAiNURyR|VAN?`v?!L`_y8tCbuBVjBR2}M1vfb})?Uj9_Q$gqv__lm zeJg)kkk9l_m_8Q#?qJ7(_CZf_sj-$M@HC6{Ce}QvSrszv>Xt^4_3c>=0G#xp9v|zg z#sgf;U0SWnjh$BsP)^q82Yvh#0B!-oy(8({VpiXAVss7RP=@Cxp9Yb4mPCZy)~kZX zOvoTTlq>3UY$iay5~WPPGfVF1)IOwW#6-dycRw zW64sz>MgwyoK;57wB4UBivz}?jCItX!l9tAHzc==?t7!?zL<*(R1*+%7#;=C$6?Ao zeBae)EXy`Jar%K~`9BOz5iKXkVYBV$sQJQR72S(V@({0+VsWy3Pr(R*q6}8qpWc(f z!CcLw5dx2%n6Z;uFti@m$~u+Oo$Eb%QkA=*s#~{DpPOeZiXup$+0ADb1iSfQ$Y~8Ax~HMxT3A60_R)AeXXXv+Y4w6R z@zcXZgIXBI`(vn_7#Jh?`4LnhidsQMCcymZ+6#6(l1elMB*;;KvA%B|O$7OdDh8P* zCA1$Rf?&*l8c;u@a*kgLty&`HMiN-;+Q2l-TB4vf+})3-SKC*|Bn;PSf=*(bf{7+B zSf)Bly*&6Ha{oW}-YP7vWs4dOl3>9?aF;-EcXtUAoZu4N-DzBcLxQ`z6Wrb1-CY~k zyY|^eso!x*?%$ApStZoxzhP}gp{0=esWUjre zT&*0%by!fXOiwwa>feM5fnn11D}>0+IqT&SLI>=GPWIc-g~_YjxQMI-t6k2W;g15E z#5_N-fV5Lu!%D*=Dw2RzGgRhg>%5HWNPL*QIpoR%*K|>`)HlV+WNq|V>SD784;d0A zDT&Qqr)a)tJGya4V9|@j`~*3@08m;QQ_OfeTU_T5<;jmTfKKQn{2IP_%uMTQ19IcF@{`iSKG5^%Qs zsZ^#U9|U)2*j28gES>6pzY|~L2`hNs-t*-nmUfioR{&LIfNm>Be<32r_JbK_9;Buw z8%p7f4a4IT;c`AG3*E3sR=nqbwFr#IEH{O*Em+qJsE==Qdd^g7LbxgF7$PX4d0#DO zD%{rCwyH50PhZThK_Q*6?IP&(=u}aM)*OJt>e2Dx_&mn0_(SfGkCbHmPv@~)9`ps6 z?l+YFd673K8rC;)grKJZeLrQ4AP|Jo~*-$~`Z{OR6r<nb>f!Pd|n+1(d>tv(XGPMgEZ^?`jJ=q9WO`T6+Qd*$y%PuxS z8W@EOTqybmsQ}Y4%{MWBFV$G%zn!?je9&3p#>ytvaL`e5`LZ{~>gnumQpvNjdI;!U z+2K)K-a$-q^msqdUfk&Wk{R4_)$Fi~`|ge+R+?JKU33u#2x^#+R3ab0 zU-lu@S?io+f`;O0;g#RqIInt56-NU7gkP*5xl~1V=ZZDNQCOx&DyqU6VWV|Mo|nzt zvN=y1dTIg#=~lhdcAggcJUh)mR)K~&D>Oi@M5F1y{a>*75U{YVUS-;dUJ!p}^N^@L zScgP(QCbC^;C7ztZ{J^^`a*vJRS8KT8I@6{B@wu)Ku}KD3>fnWhDWY)yFTQ|%XqDM zF-AA1ns*=6CWM4}pT?qw(oLtd0Y=wPscdvKc=$muDigSKI9KZJ9McBX*kH9*t z7vqr=NT^OfJ5czvGs3r;cCh-ht*m5a_N&Aw(gxX+Plm>P3?#x0HH_xUM z#Vjk{4Z`SsA2F;$E_EOVY!On=CwBwp+aj*QuStiTQdsI0Vz@PU9me>$c0{*>4HwTWS6R2=P(&9h`74 zJ06iR-F^ntjW(CJRsua!a38wkMb}2>tPx9xAK|1g)EiwgBSdxi0}D+Nc8d%B4lRrF z(G@#{@?{cpAho#i%Q90Et4t1cXOa?`^CTnZnz#svMr?UZHi=DvI!F)gcs-@@Z`)1i z>h|Yk2Cov%@0R>H?1DfjVt&%}2@)z5c?!kic$brHnV=O4%Ga-Y@n~`CVyiFq8#DiP z(uWWxCrsyKOiBf{S+6#sMyG@0L3l%(HVjamfcDW7ypu4?m@S%uUa*-}APk5d0YI36 zc-o%%Kf-AGOg?WkbkE1_+V>FBh}T(=5H&2~pVRb?s*SeY}+Rqg#YUIg1c3(6EySX^Eg9Acve_Wyn z?}C_B8A^w(KCd;YBD-p|Y%>|1;a*lmx+q%B!D=Tg-u9#{XahLcO+{_rvVZ&>jNe){ zQaU$C``vpy3*gfqdtPoE4lWHPG{ab4(6Br76P3FgBZsX4DBFroU8Rq4ZJ{DLy!Q0s z$SS7Jb8Wo%;#6pn;B#XPndFBt{Rh!K`RoSnq*~ z_Rprk6)M7#?;kC~pOIa2Y+!4r+TYcsAi^XOOl>~>Ppj&DHU;x>Qtapu^OK$WYxw{Po^f9%U& zn=oVe8()ibfSe5???U*T{o`z#9IM_I(2V}CdDCZIbA zs2^YZ|F`T@jl8Gb*P zR+i3y#7f)UVQe@hNI;Jf+p%Q4&v9aZ%ycH+-gr!McQ_Hh`VE0uuQXs zQuU>UQXX8%{BDIto9aJRWPpSS35nt!L-vD0@vRPX9y5wX(&NMs9bFe##_-M|o@D<}oPwjF%e2~PF@K7q z(+K+3A3`u&{S4o|(f)}Atbn(YfT;jZmOa2-p zl4nu*k{?%f!T5h}DFpgO*=Jg`1+izEpXd9(R7=zjzGub3VPX8qw0JMX3 zVmQqvLTFY7Z=o4slJBhlTM3wuwop(GbUjp<9K-*XMmm>Sk5f0p!HCyZZY92e{ofrF zz$F+X&}@H53{O*zzCW$7{@*fTjCm867-$Qx8`k{XZ1UeXaQO~BTpi##>++of-k|Ga zaVKol%%8g=YKGIzB~Uw_Cr7Ut1O4lD8sM))zk@mfQgU3=@PGM9|9hwZeX}9ut*>0a z`TfsY{ojxN{zi`&g|w##sDusp_yv8c8k=xvs@!LO@H(2ctn2zuEazh)?hU%lTA@XZ z2Qpt+%rFc8Kl4HI$tQyqfz)u$!`W=UAaZB5B}3WHBi@dn5d37XP$8i0^K~YB%-wEp z6rVDRKGMOxw6t26Yz>$925q7GETcOVQ?)k&@3ZZujifoqYC(?QIwqanA})oqN^%R+ z{82H$Fzd5VEx$!ncygU&+A`-(&jV}rzH;}l6(N_0bZbK0S4cp&COoZN9VP2wtwmDN zXeb~wKF2E+C+4hBbJkHbzx$N`HBR)4isX;&f$f-DWygV$Cd z9=|P>W_OWaP{iO3K1hP(n!PaaVd*3Gac4k=yYul~A?%T?5+Nq)yB`#PmZ;BXiNC^9 z8Qhs_Xo@b6qnp%+FS5t;l{*1$tE(RYIpZl&MF2*V!I?#Gep_~9A*#qbXu(};O$>PI z0vw7oc+5)4II!eK(yJ)cz_^G$N-wh)|gP{A&{;Wil zw|}t?i==Xyv`xyX=&_ok0fg);iPGEht(&l-UqkR-Sy_7Uk`Ae2AJETW9;G44&iCQY z&sMXI@zBTGZBfzBaQ=GnpL6{M6<}2U0>$q(P3wTpV!?a#{jqXJF~f;W`@X7x*d@2~ zsfxa81|S+F0_>Y~ua9fmoDSRd+rw$1m%9UnTOyeZzKsODPC_qAeby_DpS~$^2f*`v z`ilnVItk#|{YWz}1B%AeN;OR}2O;lcxSgFO<5)rf$O1+-g@+bqhNT~%2}*gP+amLH&B{6Wb$LVuwyCmYsP3Q?F zl#}{Mz0KkD?fMx6I{7t~KX!2BKc_j7^;T$PdZJ)a@cf~4$!U=PHa}wXF0PdOlW9!% z2c7HedU1Z6r^}t1V!{*ns02^+a_E zr;gM0V>F$mSw~AJZ)=&Cv$olyK;*S>QF(B&%EN(v+RQay@ANKh+X2hsKono8Ip=qw zFg~xR9HX#jfbus$kNca5vccXkp2bw*0FVQ@(^_r1%{X2HNyJcmq2iEZW8D?18V|dU zTC5~os=oEs{W0ADel0lAfO8}SWWJ`3&U(9E_?E^3*j`lH#&iSMVwKDqZOSwkJltd# z&kP?NOqKZM1__&9f>!XmkIdrxS2<|Tb(U86fXi#P%2;X zqi>t*+snO+d+xJugpbHLP4(JgO8^xOPFr&w$0q&c*~uKUP2w&nuDPE}0T&}}nbgIEb)Iy}q2$Ct=2 z<1~&a(Ov;NFS{yVMf)5~a8G{}c`6Of?(I`_wJTg}BN{WTquQ+&AdCA+tJql_?AeT| zM9?J}OFnBj%*zhit7x;IOSijOc0EB4cefav=SP-b;7hw;f4lsW&xbr=92VknZ^wV$ znt>Q*yZpt254{;gKLHa~3G>`?+4$AyPmJ~k&i{wEVhgPDOo4{WO^WqZmpQPl(N>zJ z!>jXs*%2Q1VL0EYr<{FTrbHRcan`(`98*Z&zOC z=GwB<`-7D&^rL!SM%P2*2lXmr_sI{%r$y@zI-b>Pv(NP%+DiFtcZV9TWm2xD9|MUF zG3mA964OB^Chi-~)K`4HLHTO6-xqN1tq#n@U60oRzS`9AVVx=jA@189MDoGFU9+a>ypN9k#L)#1dr#nCq&7`tL@^30o*%`{a@`->StL|3*=< z#ZS|D8o585D=dRdvYylTFPtio^yJL)*M5@iU^0^4!*b7Cjz|Tm8Ll3#EPQXWil@`b zhVFR%)^{#CQ?s@ouz0CCb2-ZxJAZzpQtlpN5HPKeQ10OJ?X@+z$_RI{((3SjQHU)0 zVnNkT#Z&&KMkM6J;F7h2EvzN-`rc-W+HEUPsdO*5pomTezgC0EX*(9>*pi(9VSEaJ*{=>hW~ z!OZ(LI<%`?)mzy`+}YwR@xY}uUPZE;Bicd{=PSgNlfVl;<5Qv9eW9p%Q~toJXFw`q zhULh7u51W~wAX#b;0ba6!OYwptbH)f;Jbg@(`;SJcAlv31#^D8eoc2$YLJQVtkQ$> zf&b~Qc-^{WC9YJh_!R@4DrzjvGIzunf(aluIi?;AG6iE)9hTMGwAv8 zaXP&rFYFnNVmRz#HJ=eTth(hmM+izRatnQ&Y!oF`Ma55-6c&@UX^BsrIRntChPM|8 zAbX(?bNCe7NPnV)D1ibv=!OaZM)j*w@1R#(w0!eS;&`_&h|w_b&a&kV_@ars`phq& zb?(v?R#n8l`_8okCJA6jDC7XRSYve|^wn{FbfIn>!Q?eMZAujG&LO^sJH-|Ll>CIt zDkg~e3pS6ZZfwZRKDC1w(cJn=5vDjUW%!oJX8%EOmzr7`1W*3xodWyYGh7Cl+-62y z^r2s~8YNb+XP9z+&R9HEy1%G@Rw98;h?O=%;i2gbij}3y&*q->orGHgg5F|nkDwjb z`rky~)?7ALFUcLTbNN$+A!t&(zjglc;ozp|2qB#I&Z$AT0J6H|8`DAM&B~B(!z!D3dwwM)lg zyUxDdSbSUlq2_v@HduNF;pME$L3j%1waf!v|MYGG6_T{|d^0J`nYgu?PW$lL9W$=d z%5gfb9o{B;8a@q>S6U@B6{E07{z$@|?5HSg|wK#>s$j9P0ZM^BN zN$<-c9uz8&%H(0U|2#%!&~~z1`tl+(D}-LZg)_W{@zcj!zieXKCw|3PXSmLv>Zdrb zJ@PnaNem{&U&?M_SiA!tcE_#V*NE)AGxG(<*@Xbv5En}vMvU9-t$}I}rb`P1kYZn0 zvY8ft8L|N)J;OP>?o&>poucb{<2Ms*Ly2_RNaq^M;U}c^l-^FRQ)%uS+D)Ft0IKuO z`OfQ2>%PvjxTVk_4IE41f=;a7H4GQgPWO4jt~K(fvaTjX#?y1>kAaU&o;N&4X^Fpy z^u4e;Te^+!d%g&!OXeZ{A9CiAfdXMyC2lBEG|7A!I><*uSdd8|R#1rc2plz6?N`MV zs-MUo)ETt)v*D(uYr_1hlwCX9p?a|L_`{xTs=OF_F>Af79jkfDzar}-oPV+zxID}K z9zwEl8HcfnvigD{kG#v^a?p-&;J%{_wE?4uKLT$ylH8IpUbP9nWTjere6$sd%mx)BRejYYOviT9r~N9q*I^0$LfxX zI7#}_2`xzduCw@U7DO|j7>GqQu=zGGN#%C^7cQ|#sU|qv3Qb37>9?7YYufr^5Qsyb zh$k?=gBj8Q^G3m7hggwuUih65?n9*Q+YN|S39EPIN#uw}`;?$v7d%Iu9~h$)OJ0w{ zUMn?PNi0Q2373IupQMw0m1zh)WEI!!H5&A>JCM6!pw-6W+2dQ~(sqXVTBA0HEF-P( z$u2y{aHQ9K&I2DIM-dNa7Ps)4|fBu|#nj)_0lCb;#|scNcL92=HpGCphZGk&uL z1*yO9(|dizd8ucK&6953eA%4K8|Y3N@>xyLs2w-tjSn2&Frr^{=9NC+yUUrSlU|n8 zzwWU+qEkX1rGhCeB~lwAa3y!c#KNZNN_FBG=|(AXB==j-14kh z`V5`0`As+0hG(oxf_qxhx1)d|F=DIB5(+z{VUVs8-}&YV@K`98WcT+&IexXHx5NlH zA8ty2kDuwgkc)ZnjM@ynKT+zZoX2m90^_OGChK_3F(R`}K?EzvC1~_oDZo3k%jg+F zZ?cqmMm*kMEYk)~K{9%h#hK))@_>W%^(G>Mh5h^DkLS}QoMr&oe_lvIP#sLg0uzcn z`>wsxt@eis^~QM10u@ko^XGzh3f_La;b};pX$jEFRC-3Ldj#wLJ+?bY)Iu2K~sk9@+}jeyWyA->EODw z-;8WeL4q7fc9N^DS`25XX)$P;3o{Y{^D*LbZTg5;%&J6moj7H=;ofPw-I4e|HIhk=CX z`!A?^n>;Ic*rH>3q`)sR2D*<~co7BPURoSNf1zTUm;Ld@$kErDj)?q3Q0iwy))Ux8qK68Xpk6rW(07UzZ+t)`v-+L9 zD2nH#cH8re_~;1KvetQsJDjof=g|S8En2!sDk%>HRrc|qJZ~ETWC%)c(G0D1^TAV4 z9|1*_d4hap;U{y1#VOSil$MMjKmDek8H~ns<0Fqm%n8eCIJuO4w+qlHICgoG*lUp| zsQ&4StqpdDbyb0+u5n+(97jVViOANam&u}RkrzMnc-a7z5lK8AvZy`R+ccBTV#(}A z0$1Yo=%A#18b-c{x zm@GL2k;Zqphbg}+!t6_YsXVvyStkBwMBp+@#L$IPw?5%EmVa$m%I5B1v{rHnBkJbI zpssbbIJ#)LL2%;t=oj);* zbQl^sgmCi0j=P(j5S^JOr3sV4HKwWQZ|}$q8XT$BA0rqMufg#A91OeVE=3fDi=b5#3J6yGX=#w;eZxY7)(*UBS=l zGt}l)@X&}JN*!%;QY2+WYIn+?b$(>`9x&n=;LG+^Rwu;95wbut(ltk8`S^A$3Li8_ zIrP(`ZiE{nZHKQAyJhV3d;~#7U(M@H((T(HXBn|~RP}`(Jj`F9seAg#&#mbCb%=QZSW(c+vZqa)7%r(oIRML8>I+T!~3$`lk8-Q4ykjC*`d>g3Gtq; zSKvdY8?KW51jp1dbz+Y``?c1b#hJz(mC}!J*wOuSmi^~Q1KcV7OMdVMYTLJ4Nsbfv z91M4jWaD#ke`+V(zWZF4jcB8fPzAqeXtNzE%1)0rPswBeg@{gGl<|BnknL6^@WQj0 zj>bZ+3xBM&wBOclKboZTHF)jhxwTcMesl_g4>YehfK#Uu26^Se=}=LH4@8b)o!5)e9u__3M(~ZVfa^s7U4-p$`&Evu5xj@XhMy}; z1VEpeFLADga=l|c*<46=5^-b-WdZl6EL>}nBG&X~xEf$C8w`)QZ*+g2-a<+e!^&@E z@n&>}A+^~gB;0O&WI8g-Xd7qGOXQqGPKMyP{rRwO%0`!r`+t<<;`ce%xxLc0GXQ66i00q zePjNnZMG+NpMHVlPGU!sI{xF~cN8b%4{xlZ7Fmk}vgs~V)-@XcO#Ncyh^(_v0Hn~PMSe)V{Uyj@u5s|>F z8+5?-^6)Vrih%TPh5H9*%nitj?P3z?hxqJHzryl=H;;fob|vFc4Xgp+A2c|S1e#k?M*3YiS0 z9;D5U+`OHpH7oUhEjGJ`jvhT+9~A*@;YHsIV~P}d?5H$;?_*LMadBe)Ip6(51#DhV z(dY<>EwNd=h|UALUX&dKrsuAY*r#E>lU%(`9JE-jnQ2C+*ptQK1|A(E(<0TvjHPCq z3%j8+Nv&i9N0D}1Ygmtn`MRdQbfek%iD;oa!=z{1-_iollHB2(rg%>+PJXrA8a|&% ze5TyEtJu&lzfX>aA@Fm%fX~RJgN2s5Uv5BKd6p4gEdmc6eW>4`=oM3|l#Bbxz~OZ; zg^~bZaK|lng{sz2H8jC`$Y;^5hL4tH(@Y&7nL|)_sz66?Ymu~#O5E@LHPW#fc`{}o zT`R*8TMDd0!2Q|yYF_8K#}wq4?PU|trRT^o@>?LGf}_?mab$t74{=pLimL*RlM1j-CfhnU{>xL;SWpAh(xHf5fgfAwT01l)o$G;*Uj&tpo#18U#IcA5o za(Carv|MkOn+CT$7M`^dX1{1R$qFhWZO;T-ix*j~=zP@yJ$r_6gXz>NBx=o`Yj7?H zzOp{vHn??Ro2ZQ?J;Utqi}0@8e7hdw$35v8s{QlmlGy;8CAF5`0^ql& zZY$IRT(MylO!P()Smios`jcy1atd4DB1UEEgiiw#A;70LABlAz%SNkF?-C%DM5g|w zQ^QMkW4pp8SQ&Puffi3}5k(Bq21ry~#Hh-F2>9BOypV zmmbW}DMScq_jc^$=UNIJ9(6P!1rfLTTtSI}Y%-&`OyW(GuM^QW&%H_)bExsVEw`z- z6D~X*Ow)MzW`Q^4{Bln+I;418usN9KbSj@U;5!(lz7s zaNrZjnLKcJ${XUWxNyp7-^}~T$_3?-&BU_i?wEA4i%XTP{=i@GK8_lZG>WltLNqz>vNYzfE8v@|BuQn>OI!5Y zVyRHu^;y`mjKiUyn~U4R`jzCGzGh1xPEVB)MMnBJ@du3Q=LZMy5-U{ zFFS-r%SL+dAMO1qiSSCQr^85a{z~-@xh;DMxwsgD+V$AbSh>3qK^apT|2g&9=%`k>UQz^JAnvc zHNkGfob*%uMBX)NO=c(OiguW%$9zsg>=w2NPu!r0?^u@0{D?iDDb=O{sF$TfbCll~ zyGhH}MvyO=&3cZo+LMTekz&4fU*8WER-*}y_9^Ult`+#1u>6$h@`Mxc|>5dOTx$`F~#<$CweR`CQiTyccC4d%;&3803sA! zj@&2eI*13Z+BM&EdwG$Hu_v>fkLPo>JuZ|_sAHJR)N`3oW2o;YFF2gbzJTG`LosMe z_@34+9}ve(dRCg8+eR_B=YYm*rJ=Y;GmxhWG}l6tbNYjk$W`c1K!)`jtEpWZ`rEOQ zy9=}#OdYS%uHLYFNkoFA8`5+-BLbD~B?pC|;S&N-Nusv2U9`IduFsBBEyVYC z2#4EbHf{(cBhxgxJYO*Av(k?4a(*43ic}IJq;m7+QYn8n*nvYNKtZNqXmQKjtB$j# zjVHBg5ZRkq{R|kY#no+ z&H0q?%v8PO?t=$xX1HtxxoKwLgc++|^f9k|hRWZHeo^GVWEF!;beM_yx zm&hYivxQ&Oib9AkEs=(YKWHkaG+w=U(eYNR&zS>%D}9Pp9%ht9`pwz;JpV(Y(=BJF z*Kz%KXUMa8`PHSF9X5@a$*uRC&ey!yC#~KTeBkT^^onIhlf9sQa2J>JX1a<5SCX96 zN{(=*`7P6f)23{o{E336f|A**HhY555}c7e+SP9Vd*-q?s&U%JEpOWtQ_8e*z1&tPZ+ody9$^JF zIeFJY?gIurJF`3XsoagI%JrFjD#?AO#N;&e1rpAh@QG zi+o20WWQM`rNybqo6AI#+q$o`3c@o|cL#4a;sN=hKAJ5>+=|ZuVo-rLP9!#v-!!?{ zM#a}<)PJTDDUN)+C08v}%ykHZllgr2+m{==Suap0V;(l%_2BZ;Au!;w209>amC|ob z@@GXsJ!P@&7AQucSO(4BPCty1q2jQzO%U)%4T3u7fBOvyfUXZ8^Jbag0?0pJ2DmPu zt2ft6hx3Gdr{hjADCpyozVxO|qeXFC#qjBvo}omP-mHM_@rdP8roxG}WnR<8BP0oW z0LYpXO-@`V342Pf0$uB``+%|0q7u7uk-O=!aRsc7wX2>q38M5qpo}AuXJy(fdQFXY z-_earuCP#LSlPdp@S*P=>CccobVuh&G2meGE94lI2AmlDHFOQIcVo`E`m$sr%^nU0Jf##^%^UhJ00HD;=QYo+=2D~K zJFVyIITHNYCTC8E@e^OC03FruX=Z3lgFP-C;*FZEpMyU4`jlY`(^K_f8?`Rk-!JjS)5gBCk^5iNnQbf+`5{CZ0pugelIkRf2-Ih}T{B9^gnUfP92v+5-Go<)wv zMUmS0l3~30_yTy&H{PVDt}hM2_p!1B>&||h^R@Nh-sMfO6*@v91iiJF_{=EeRYJRG zjgLalhLt(|SY>phuF27h536cT&(f?ycKGhmHk49Zb2+GRw|@eyMoiaL7#wj_wQ%mK z62_u}=KC>s#f3uVDLRAa5)m_cV-%dsCVWx0Td3SHx)0TWBPcT%f9RQXE3V;l=*e77)hG z0$|VVRkvc00KLnw$F}C5Crv2SJ4!jTN6`)qNL-*GI7(L~krFC>+Dkag@RyQU3kg+d zIkH_i(f>+o+O45m@%W^;x7X@jlg+%Y>V5A?SRgi_Y)%?w7AbI;pf)fxXnESG(RYIZ znN2J z-4?Ai9AeX22HK>Q^hs*+rSD~Qgh^@acZbb-29;GDE1>iN@q#Ck@e&a`l7H3$ETo>W zKz~t%AT>4uq;LD?r{ey5FdhP|i94%%hL0od-zQZ!CUhh3oNUYO#@=FgH2&Pu(YrPy z{H5110-8BCXx+4j1>*_sG?ikN0gSHOPfx9->a}8f5Q;_;K{uAQW1QBMdN=Q1RR}$= z*zQBVWM*SwqE))e` zsi|G8F)e)2JdX#i2&Rx{vtA5cCyFSBq5myq{B^PZ3rP!)Ttv zUtj3I9hB>|$>PY{PXi8s4)c4$L4`sAm&JD5@q?1O1cdPSXKk-S(c#;~f0lNohyY8m zK3oRbA6x-Ka9vhRcdm^0Fo2I<1Lf*u>Wgxz`a&bpE*YGu2=CKZ#p_dUI+%lggR6ve z-vOz~Kl3L54U`;|p`DVHzWuWyxh<^+?~M?N-wc$PgSUB3`s*#1knn$UDvA*o zN!$F{>?GP@gRylXPc==G{(Kr($V{>I#oD98U#c(#fbiBqiTPYnrnXl@WaFdN>x+sI zbCLQCl}at&oo>`@LKS3|Y?)?5)~W}Xm%3j2xpB53)xh4n>|LX93C&&Qz`boFA}?! zI!tqq5%XyBU|m0)F+UvNAf>WS(>yF|I=w_^?O2KBq`Pdonl05hOm*f1p6b#{oLG90 z_H`FB8jvT#1-eo*p|Yf(MBpzKZ82Zn@SV{!00JwFTjoOrtyTvXMd0 zs$Z(kqI!L#B8RfSf%udm7j*QSXQ?$Dpz#SeTg*MZ{W5d;%Uo$g=UseD;j9;Hs)UJn04 zpA&m1&3i}40`Bki{+;f?$RF<-^0?!2)&O5fakF`Pzs|Zeor3XwIOSqG##V7;T z>$Pb6QQGmWD#58wPmrOE5nP5*bHWh^S3p1}|<-7#-F^K%rw; zg~pXgM}0>h9wQ2X-wlr!`6Zt3&+vx46|QHP9YyVX1Ojv^H#41wy5N%3zfvt=8=@#K z(`n0`EnUlrz~e2$x7#HG(FefqklM#6@wsDO=>4#Nx|93`unXeS5~VW^QEMB^rF?XTsFK@jtKbdq)&*^GQ)Cy zLCGL!mcJTTa{eo(}X>Wic0o~AssZ-$^#t|}N~(ac+bd9|3&%E*PV#O|LxX)0k< z%=9p|Tz&IirA#~DYCM5XQvDmC-f^`b2{5~hu5sC7L6M47j3;?NSXbi@sR2D;FXT>3E$+$4Xl0Z&-fk*#T*b6Og%uFdhhB(K%QGGGeU zX&d3}b~D`kl=`}EYJF8b42RGm*^QGkT{Ux%P)}{XtE<)g<@|=2m z0!DOl7jD~#^?7)oQ-U3|S1-`#VDzw3%YXY4!OKnOGkS7!Wk>rb5&{AQ6h3H@q!Qn; z*z$fs8oZe#$m*C~nKRXJ+}TQ4h*r?X+xX%)Q3Og3QDaKg zyd3f6q3$kLT@iL9Q-;w=o5tL`l>$5FN_nMXP!Fbli`)mqEuH zFiP2vydwr4*ARnebNR)+-b3xt7jKNt>!9*)oeC9;i?YSgFjUsO_+?8U9trEn+GJBj zw~8|X4pWC=k|%@yHb?2Fu8VQdoj?Ux-br0R-{)zi;DW0}u6&f>`^;6M{Y(uuX#a5+ zv)8ik=2a2h0>Om2Ev#l}u}%qw=1^Kv^u@=`5!Bm7HW9YvKp_1}oL;9wkx zYXU}-@}!gP!KC5!YJ27(G-`03?&$tRZYuV+Ex~@X^(Z@$op{*Oz=LJ)5Mq?F5nkGs zFZLAqH}wAbEwOjpb@pzY|7Q^EPY5w*_W5bYzZv>C=ep-yel}f?=BBdx1BUpxcH-sI z89Yc|M)1y9#{=Lt-F0c_*m%i&BqBI9i0&KB7^zX)XO<6OpVY+I)FnQD(C}Mc?MyZW zl!x=cfxBP7O$NRZY(EGDJ^EqKl-o~3iJ%`M3qdbRj3>bb>CO>Or-u|dPPGV9Fe|4qOQSY;EeVjOi%AuB4mzRzaG&s!e(4jQZ zV313~8m7!XfO30t_>lYAsx^FqTtZ<|wO|gNuCc!38`G_WKS~M#J*B_^@)a z=KLzeAf+8iT>no7fesHidXx$r=6yDrc$jqRr(#Mw+O>51gO_te_yHJde!W7(~*s-pp9FQ7Gu z=G$*6!V?{8m9kpjBGqD()JHC#SF+dwp*CHsZZPCAg%Bgc;`a~eV@kxw-z5lB zx?FDoT8R^G^3G7)q-SIUjxS%2T02Zrbk&nSY0>yzfBX*eB?9}|*e4I{Nz$6cuY|ho zzE!9y01*=j{vOpJE`(~>63hB2OgG}u?#`q%bz82(C6E8!!(UYCVmZhd*}ivNFMC>f zvce%|e)RDUzlS+|O)8B&bx=^vqSqnAAo2)hupt%v^Z%;ho1g|uj8M^-@Dng;zHanp zJ=Vs%tLGUo$aE6E)E==%!;SDLR|;V}>dX$~us&7xXn4S`4)WjhLt*GabQbd82m6$K zYZ8q8@Zj5O^p($i)@nK)nSWk_cf6ipZPJKdx_LN1KrvK2S^f5IdlpnTo&4;sk%NVS zN*b(rmpp8_{uOr{OCm}D8ex{8W`}cFp_5X$?Lj-%M$JXYet_&(MQ}R90lOCHZl+LD z9hDJIn(rObz0@s!a5<*Ve%8oE)am5T&>@(oY2<#(AwT1ee)^P z2(eZSZdjVA$>~gy5X7X3hCzsooww?hT@I7z-Gj~G)haGNw0umKh}wE_quWyOSKwZx zp4&W%tizD~;CZU^fP*lNWcg}*Kf~#E<^K@8=-rducG>+(FeBe|b67s)Cnh?AGZ9hi zxdT;QNGP!c2}v6-oAca9Zcl`odHs$yxFg*&dlgiiuKlzC?&?HM`oiECAW;2Aof<5m zflP)WEj;v#)>LlS7T@C4R;)mrg2Y|-p&zo!2B=Hp#EXWi6p)7>UDCcCNt=;Rva}=s zsm&B>%{0Q;Az6y08T830jPXAHbfo`1Mq>rM~`@rp#l&IB+m!%xdf761?`h zqdnUUnf~oB{%N{ozQ;IB0Rps$2gRbMx6BL!In0DToShv?$iA|-RFGY82o@S zHhb5uTD5Aed0%thFyQDozW~R9Vmg=TE&fF(Qy|AX-(3tfc2F&-a4Plm!>m4mzb3mo zUk4fuab!E@*P zVY?Zt7w$T4QT$DmdwX8HLK4eYn=;W@SG0Etp2R0Mstcp9bwtSHFV$6wM{HBQ{Qg=Z z63U$3#+Ej@odh3s$7 zY2sq!h@L-FvNaVscp-os_>ND;HD+JAK7{fvWSo~tXT-!y(acd6>q3rGm_5YQ=bg(zRwp8oZmOQ=7kx?1B za#;R|3CV<}B<_E?$Wpv_1&ZZwCW91EuP4;evVg0x6_{^%b>xfO?F%!?C?_FQM-BrL zz)BjzHIo|IvQ}xxVSAdj6EjS_LMW%f?7&|M9&x4|%#^B&@s8$0W$l_~usBfuP0hSv zm1q2*l(O@BbT0_UL6P;#ffTSW^{L>}+W*7;$XbB5oh}yWab6p)kMf(mA(1?KDqGB zw0IYS73}sGr2cM7l<{3YX{20e!Vmf9wM>JG2shg7a9t=g@g@_h z3^8QnD8M<~uwCKagX0k%(AMS|?A*K-^!v&OAie9@b zU0f8CUOUddOII%soXfyX4`c<r>2uCkVrK3*TDIZ+T^mJ)*hB`y$dZynQE1$=_~lKP2g4ek=Q|FwQ5|lk(Tjtqr;Tv&@RhU(H}(J7|1*R$1aKaUDypBn{H3zv<>1gOsodm`ll1; zP@}+9k0_p|UA>e|-zVSX;n8I`#cNY^b``?P(9Bq!=x*D?1W6%?7vFg?%tz4;_8kV? z_&)uK4@lb_QhpWeL3hRQXZ{lCXbm@u{*I^t|P%zG`! z`PIqH;7P2`OxLOI9(5O3il=#-L=Ke2k7%R8cITaK3^7Wj9I=dp1Kg86B;N_|h`S~W zB!3Q2YVr5QmDfG)8k5r}DDt}da+q+~G=9Y&OXDyyjE}%bTp_`)#kY-+V(X&lxYDk; z=8z#vr+x`*gO?k<9yv!HW-`-m?s9ns%IUUBg07INrlyEQc6X#*-O z`aU~wlfEQcjpntiX74L|c1mlXnSrzlqtV9RBsO_`o3!2s>@ItZq@HQ9-2pZ=5Tnb1 zIg8n7CV>S1TDxNO(g6yQFN97`2B{s{MJ)R_G&@iUTTC*L94x+cpn9Rd9odU#7P8?q zM6z`}&JVxvQbmS$+1jE}U}n6e=h??@OE9HWYS0nUa>rb!9v+SMS0&+1(-MOujw$Ut z5qG;G80RA8`SWM_zM>^Q-(;}7W(W^W$@*&qTf@8w6;twzE*P`N4)@}WX@oikhwYz& z@=C0(Ojm8Mr)AXMDfoir*=8jcOkK!h1Zur?7#=bS!fmJiZ`{q>LDg#tfene}clC*% zOn2F?z=k`UX^I!VAsyi34kR%wPVam+SxJ`J9&wk&6^_9s7_^%Xs3nYurX#kBxRaBG z>9u7iO?rX<&ASj%s*8QL2%nHl9<&g7WJd9E%=|C`smKH9A7Qw4iP*HRYiWGL@8K4$ z+Y$)j$*$JdTZI4;wl7_(0(740G>iF^tORo6|He+p#O3e#ZIDwat7 z&YSB@uIXpSrlzN*(1Jzx__l^ZLnv!@eMGN~3);CCweesOjQc#<_o$fM+D0LgU!Ml) zKSUqQ6y=`@cn4bj4+9+Qq*rY zXw`taTp#LDw(%tsm^6E?XgaY-7tUnX5F~0W$ykm+&8$iXAl2N0(qA)oL;fU$5rrE! zk4Co~9)s`Y%Zs~|U4k4o$n4aH#0Q zmJ!Bjh~{gUJ+{=$#=M0?o6_Yu^xVu*=7%zDb`YrRk~paPyfzJy&osm%BMriTT505*{3s}erp>Iq{$`1`VL&F`?f1= zu@f}q-&465+aPw}-llwMJhB0A_-;|mXF@K1$5IXdG-&N|! z`SD;Q_xa*>5kXnoPYSL8a>}b?w$J^h_my#mAr%nair7{Na>=AKiq1X=4ck|`d|&q_ z;F=)f>j!qA)UUdnpab|9yZMGDY=R}bD1ND63>rKO@cw(>&y1PytS-Mq;2-%(viJ|JNgioTwOZqBo8rtGF+x)-nC zvq9O>o<^AjZgR(6X7X4~k<5A0eVj8hv3vo@-DfVnO!aZvD>nZTR*!gC+eo&!_Oy^y z;U=l{mnI%@?I8UXC+dD8qEqW-E>SJw!Yn06PO_wMI!(IsYHTPA|T5r2V`qBD)Xc@LRF zYMnJKj8c=CKbBUK&cBK9oc~#mBEMNO;dAj?DK;YjLEGB5DRbQh`kewb5Bb&bncFNm zL({Ld?zu&XLD*}FU1HIn&OK5-LxQncJP?{r#Kb>%=D``_No!q=ylbr}<4TS4yyv6YA zUK{%2sR%rxJDZ?}J(&{mdF6eafd#MHh;#`pZNMI_6LaU2v4~t$n$FPvyvw(Ru;IT8 zHtdvSHJWaB+fu8yt#-YTxLQ=+<}jjp!+Gz9fF{!H4Hn23+vUot+I@2*9}yY~vI^|1n)S!~41L`u8z=9;P!F`r*5+r!2M9 zigo_yg40*1efc+WJ-4MBWi!jk(j&(;Ml{{srwAx2UX}4wgRmjGcr%8eFS9uT`0nHU z@4aqnYhiSAhBCCoa_OSTXb3HeJ?|X?SpirKpiT$D!4y96hrb`?ixZ<(2BIvpXG@n0 z@n;ZUZ=marvK=bvf6Uu7t#9W@ABqLXaTTye1ugmgDwNfZ>d${$b$A2>}2rmMpw z8I4MzmzXd-uL`m)A1Je6sixuqdd~0*<~I-sD7ohUj=BM$ROa!A{qf)EIelv0`iVxF zlU%kwaZgVrr#ypQxUSUw5Ya$n4RAa7Ghv_K;CfusI<8#!P|SxiuVm8%_i#GGR@dQs zjF-#SRXi%f|9)+*rOCIBvift1Nvq)@qS>gbXs*ao0YPMugyk(m#Hzbd1t3%Vc=obO#gfHio2P#((vT5w3xjHc zd=GTX(mt4q&lnJBr1>P!h;phI!NA_O(l|2Zk3|CBOD68l?i=JtF)1*-+kg_SR*JnP z`waLiN}XD}@O*C8^+(5c8j`gOclnIx;&U5I$qCi=O1^2me&y2_3ToCzhtHIs4`EVz zmny0f5*|Bc1=(jdTzc8G`HnnjNX)Hc? z&#o%B_WtZouKCNaiaZx~ouQ(NP~DolMj0^kyNK@YI=YB4p_fb{#kPiD3c<>6E2T)E zO;{gJln$Wp5cStRy;Y~)xYZx%UoL45RoW~aq&2Jwt#O3CMbRD2H#yZp^lZR8e=f(; zlzDDntes&y45uy`@}i5QIG8FaMaD9jhsh=IXu0>DdEg0zwTxe?4jqLa`nmy*naRyX z+-x8A>^PlAx_Qp5)%7zxe?0$8o-}=pHsR4%$#{E{4Nw?q+!%Gu(jclXoNAM2Y~H}P z9A^|W-lA^>oS68V6KRP@(G#PB!cn@2Zq;5^YzRVbe}L*xBahgMd&5tlS_^h^IchS}^HpYpK|Jb4*Lv{z8aNt1+1>l&bMdDenORd%Y)- zi)>rPSJj|o9|?lHVv^yRud^!_)X?WY4Fu?02Q zQpJk5sH50=>2y+>I%JR?xK`BjdZ^TEYf|_g@5qmmnGC}mc--Sdc=-f&axrPOrVXN} zM%7v;-I|{8eBaE{jayD!+1BPlvIE|1T$wF8Q8EdMR-vENAV7w9QPowSP;H>Ct>>}5Q4 z_2}lbc40(2$P77S$b88}vaBna1z$YhKxAv6xkSmTfi!S;W&v1c0zpH?{UbJD*HTe z4jOpI8{I;^_WC(Is3gA|jeN1%C;1zZ22O*ixh<8>MUn`pnB3mjjX`x7*AyK3GeyaD)ZC#0(qhLVY2=dWpa z?#Iv?o?&#@dNsDT_{1U}Exp#|^?b$FGDt$E4T;~X{2~TQzE;1Ozy8JhSfYX7S%FEU zu)8ufE-Bjc_XLj>d#}ax|9#EvPv{f+s->_ys-2G@j9OA-5);(MPw_WA>jVaJG^0rh z7jk+0mF`3u#cMUV;q=}^%s!l1{;!V$NV9N$M&csSx4p5gkyMm68@s3X>ZauXS`Pjp zf?4B(dQs=UYZG<1XpB-iuQkfm`s+Zf`-r31&zIFo;8sXR)0*u2Hb4k}adAH5MBc9- z@2sM^RZ%Zg(cAX3?lhamuS2%}zTD&&CW>Npu!Two!U!#k;4#ZZ6_@j>{`v^yHspHi z^OP4^pI40NHN7_mgT*tK^{*FzV2mNNG~;nyE{y0MN%WKv<0}LGIbAx}c643F( z4LWlPG3Mjvf1MZ@Z~_=0QZx1stcLRIvTe)Jmw3kHFX;yak>>UAhA>PQ_R5u zfWj$HlhHUWA5YH8ILdA{AE8!Jet;0J`uI1E7BmgBhP0gXtp00NXspd@1as<2W(O=s znT*DYN`Nv-sVdX5ZVS~2I^>BtumjBQqFCJCFEO0*J|PL|c9gX_HvX zcy%l;LEYoV4nWfV6wp4<9+4luI}mfifm zbhB)FW~6OqoC(-I0E9@sRF~j#Q`iC6g3n}GN-GuVC9CXWYBOO0n-{2@Q=kY`q9+0$ zn45BLLVzgYM-uBC{_e%T@x@s*a4f_bAzxAEuQ9Vh3XUSFtl4VY6J9tN%N6A7>OO!f zvPn8#Su~x|0zJKY$Xe{CK=MXAocAAe285cPw)zpQ+n{)}HEARQ0d~FWRd8Iy!gH4z z^8>|id&g(!$iil38KCNgjj3tZ7l)nNu-@6@HIvo=&!Q~7rz$BhmPS$M_9w}3X$%RU z*A(h_rJ9b!U}jX)ZDvBBe7f}+8jV@?EpaEn2K>V_X$i3j!SDBhe*+%>N!_CS;O~x6 z0;GjH&>QkFm`PXXkUlQs7`rnKFjHR#VoB{tN>J6@(Dd9AeyRLqok*flZ>*0}(mh$5 zA4ULqo|g)k&XpbsDDUH*Zlxm*$zjdU2L7@mEJk4nnhB{WA2|D0>^^ z03`>Uv2-3ct)DJzF10{@&bKEaWz)CY-Nv^)ossR2ovi#dN%CvRUx?uLHkPVTmdtKZ5R--Y`cm{n}rTVX)0f*PG=E~$U z8S)6Y)5@yV!dxVQCS$*kn`%_&T_#KWqiD-0Rp`qHJ?9;xuNm%^6qbC|jB5I+O%cnI5%f*7S^mkrxG4cPqX(SZ!_nc^6s0GPhqQfnFp%%*!+q|@L>L1Eo5p#o zX|&KD>%j<>oBC^DUNmU6TXRl^QdHw<6a*jVs>nBaB~w{zdnY^zRe)!KMBPsulX&)l zhD*$8KHT4hn_JF=-($z?htjE_Ee_MWDH`YKx2kP6l|A?%uGOPE@0T}Hwn@mfbvde5 z#cbf5;~*5egqV51682Yal9%yPE7EK8?*tXdWKQV2uKILQF7VubT{QsORHG||>|xUf z*lbmLJzS~Q>hBUbtgY#`zzTGLyC|)2bBvG>>^dU?nWfng_*@c*?&bRoKwo6o_&`>M ziqMv3ucFMeu(KpO#n0NMy(m((Ynl7gMN^c9gKp@(cEAbc?yYXK^J{sj52P!eF`K1I z`c+k`)}oas6E7b*b5UdC7rWLV@I1Z(kNGmQndN*gNGW3LbSdDb_AN#8fce_w;cDOb z*FgR_olifF>u=>!4ataj&7H9~_)FXqkd{(zSxu%|0guxDxpI{-N<6IvPGr3dSkjW? z$)A#4=`ivnRiojkQHo-1-6f}^{QHYSP6-bes~;a-@i1uQ+ketUCv%MHt+cz>8xA=> zQ|oF`it4wUaHpVmqy;vdar2A;#vY}-FBstr;mHKM(Y#1qH4ga6(fWvAgxll*=bU1| z-Yo)e({J;IY#<Ofbi zKw0V5I;bQ(^>?uiTE2?Y@bAvCK|sd`7zW5te<$8MDLNgF4`N4AckXpYc>4ARH2=y1 z7(mg+Ql!>ao4Z|cQ%G94cX$Ed`QlsNCyiK_@8DP8fUVYeZans=#}4r)xYW9DPbC4%qF$EeUZ6Y}%@oII0CX5^#GkH$7F>6wd4;2|@7!7tpt z(56Yh!f*nCQscH)5e?MM3y*yryPoDq(x20Dk6hLt0Djzb8tEjJMJj_;U99KM23XT+ zwG=Zt=U&=~&NoFJ`S~gw-Iqo%_PvQ*of8%7tqH*?0^Wff!Q|E6WJLAZA=bd< z8GwmVaM$Gzr}1dN(kho}XpJFYmFF{g^tMQJHAWcBxIb9*oSIm=Ci^8aMkIuKfMzI6|5e+@(q?3);xMl2gx5tA6+kaad0jnwG4w(8917Uul zi|vuJ!C0Uxop=umFFIA+xiJ_<waU6+khE!jZu$85B4x9l5voSJeRq6@> z68=rgEaxQHZ#wGl**q~3%sx_?s%8?8M&smN=)sgp2XVyG?~zTYL0M@S7Vepmw}vAd zwPmE{p^!3609P)p=ws~Xm8+N=0xsav)%NW3Q?(DkK`3)|s}L<14&if7tLfURH=u1V zmiJ&&3c-JYWcZT=vS8n2#@g5RZ2qcCY$p_&;^ac`9TAD4kfAO;kN(|N;}OoxHSx7F7>c&HIN@;5RsrO+1xeTVrM{7&>~pH#)g zb`4154ckH=-9&lzXBG+ypY&{H_mt`rPY+_VSlJO()OFW$hd_Fw^^HTU?f&lmGjsh9 z;TV)>+m)TbV*7lQ?=H%0&hI(533<$-9}EF5{C~%$0gwF!N-+D#n@EF3p`xJWFiidh4v*fd}OaHR2mEG;^n&Z+iLjyiH2s z)$NIhsR$j(DwEKqu(Gvx>1l5DK?RLqY2XMhw4`qJsN^shhMooQX@QNCRy+$DXKreD zrL}^+k38!4%EM`bDnIy=Ab6BZXI5Z&=l^KP+O~~; zZ}4^GAih~?h5?UV&t@T{rckF7tYhP)>^5*OV82gz&*SOEEP~7@0^1eVKV(T|65L1U z{Pj<@tm>jQXn*BpO+k|YLnwbXmWL|}ejF0Q2AZ2+FMaLQ+geu6A5uP6rG|_52yTl3%`3Qz#c&>5i??>uu+2)_(;n5uMSh% zUgglm;YUB__H@?Bmdjuy!;J(1G3!MQ!klm3+-C->=)7fzt36njSGq(|^hE?9Bmj>H zgt`KVk_1$e31#wtuY@gha(B-v*X@lo`+&HkW!2215=#d3Gm%p&iMtiw{YZaZ$1!~( z{V*#uf^!fK<@}*T3PvBAq^}R?tXH?XS2EJ?n~Ym;ZcLi3opsL&R1G-mm!~2hnHQj1 z(>ZHwy6|)H`i=jn*BA$wPS(?6W@k-Qr98k+?$}KJD#b6;45NB%|V zwLx4aaB_V*H^sOt#210XC2;$^wfa5QL<6JG2;2kK_jwz$si?+8{;=44{^csviX{Ze zA{_v;??pa52|I)M!~xs&@*u?hyKvSk(qOMx>S@?L_@wD_1x+*GN(!doZvNhX#t|Cc zo#4oBABPfmGFSfKWp&>7~2`1_jQ*b6$s9N>};9d}IS7#-9s6`R`GE z+{E>m`N8~hrz$psI)SJ7+n#~vdt%w0dadcgp99%HeUjkF+d$0E&rAw@koMoVtT5mx zq5Rz@%20mxV<%%OUeg>rQZ4+Q{5qcP!sx#Q6sJoRq~pn6$&is*WPE#o`gMJz#mQl; z3xXt%nogxCLgyM6JPvKt&%TI5{lc~k^XEb!dC%&O$@L)wLzzL$m|+zBLzXVY{)+|| zTMnG=_2JC;7Sd=X43Z+heUdQxq)-|Gni1*Y1U3K(+1XBwXBu`M*5RJ! zw~ADfB?HWny&ej#_7+wsN-(mbtqYCvufFdBN?+Xkczds)HW0qo(}&l@_QX@g*VcUJ z7vzNP$4H`PT=!=`v~Q5XWZhgOW4P_iJl@Z!m~s8FJ`?rKZFk=o)j+ z=3ZW@^8t8*uXLt&>AQs;-}P!B*iMiX*ULx=`Xu>E6j7MX*2C!d&lGA+G?VWB*SH94 zUq;bzYAcT5Yd5#lBb#p_ch#NFm8-e&!`^)h`IBs3a8ff)g~TjiN+;>k7^20VY#Iim z(+X=TisZ4u8~jA6(}jb%7v_8BsiDxLYId%S#Yv;XC?xbcOL=(*-_#yWHyo9P4wZ9}hnaB+I*IyjG_BXD96lHU z&x6c=bar16URaU1f69`|NX+{Lr5pP$_H(B%zJF9~77a_W3++mUvz5J&tj zl&{)}pOw2ABrWM`234}+yDf^9rbHgjd3Xhx!UFIwE*(I9>A87OU<;-?j&9N8W&0=xz>o#E2{ZK)?Ibeh8k)etuy{J6cwXdQ;z7i$lBBUm zayMu;wP!G-ZRoq}KJnwk&X&Y7gHU<@pw3;=-%`O?6nJ)@3UH)g3Xx`Gt&;W|EEZ+T ziG_iAo>$x7o7S0D!-9IbUVqh#M8W@EJOUg+!k~@ecMu}jt;T8om=fchXE)H;Tql+7 zTZ1oR><-0>?n~#+kwObAgSNwmBZ*Uq6h*WTb(YIDmJIJzm~Rd)^U*CcH1g8d zK*4?@AUdpJ_LWgpx4wPPh$B@Nu=*hoLi$F}$M?i2!f^f`uibIIp0BB=eI^Q@{O3qf zy9ah*9aRkY^wIj~hT=@xmZ~S_n!o-wb7L31`2^T^xggcxK~n2z@FdbGYRv+6kP&Uk z!8JySfx%E@_D8q%R{Y%QAV5+FWKXbxD*FW-;@z8ZnLx2xRjSoX2PtWT_*ic|-3Est z)lZ?PoAplRlA9>I3nppKXh(MdO*dWb%Cfsm)RKoF;;cr;bszu%+nGYczS=~W68EgL zh6!^)G49R$d~qLU-ZYR~T(>NR!Xp-!NgWkI?Ce)fd=KXq7pFmdfv)|d*FB)TA zQfqqTidMcH-XroYJ?CuPA2+#F9|ejkkR_Iu}UkPfh+;;7VC&;oYXY})AVxdG;QJ^_#B`7l`P0qOWy^>hGM=T z1eaEw<`{oZCL2T=E&7}@(2UuCpUkvdy0;>ZOXZu0W;YR*PDZ6feVDCkXlkVsjfA5-NuYmaA}tVSC7D8eVEqkLaKKQ+X2xN+jH=eFGZNr1_P8&X^zi z&eKL$KbhGZiYJbm#n}7f!#jr6#45=MbM+Y!zu!bAwDSMlC-%y3Hz9$Gl;XVGkH?|d zjbe79dz3^%F&7Xi5kvA@ij;%;SHyZ!(E2n%r^og*D>;oqf%A+1GGM_kaW#|%j3pgn z=&Z*o65Z&K&zjlI1!UEH4vFmlIlrBw`EVtacH-_j7{~`K2Q?1>maj4>1T!?}P~4F9 z@#c5$RmPj|1QW4RqcUp7!BMo~EoZq0C9H0r(F>MmfWUn#6A5p7pnYy?+k)zOmgZc> zlUhdy3&x0b^XrK^T|;u@clyPZz~2gev|g>aSC@ zHjd{azX>=FF6x39P|@T9-1!Qm@sbLQ#Z$b`F5_c&2%^G4*cB4*4shWH(iZZFf-CKS zCJ=0y>S=r@>rle((bmIV1Ij=w#s@*J&~1Mmd8h7mWx{FQ)+ohv<@{ffd~VC|y;bfuXT?rd42nD-sJac@D2f<{8)f>sRKMR1clu&(AyhJJYqK6LvQfd*1$4G)@`65d6|Ok z0P+|viB6Eeu*Yd2ovKMmBx-H1%UYrX{GM5^GPUB8ObW4sWK;qI6_;*}!+}wKr^W?A z+hAqThwf9DLjNZuR>vj$=qZ1BCR(Ei^r+PF|cGWVaO8YP0? zcmWVeKUrlM7mWSvd)bh?Ocl1)k#vB;s|lJk==@oc1Rfma<1Gw!j!HQY+qXRi=`xyf zZ1NCb+dIK}HtwFp=4I2~(@6-I`ZFoSE0tkx(!3v63Hnc;x*ZIR@YL;xPMPT0j~Jh2 zI%RTS)tWnP^(=b*yoxZjJ-&IRIYHR3!^`JRJUiIS-w8g1ELwg6TMR(Fz)jxNBrZ5| z?8s?;_4YAk4#zd-Pqt08jbHZaG!ywqo1- zX7|k^GefwA7U-SJw^m?h<}hqIbKr3BKRJn<5p;dRh(7ZID84<-Q!@Ey=e;=gE5W?T z=_TQsv-W{kP&s;C`DG8219iB4n?H*>;$}PUd7lv0U6A6vbqvX0vAE{S-5z>UIF}bz zHNqT%vs~DDi-eU9|o4NAJ1>)SLz5MUPy+XxO4$kTy zI%&nIfjZ0F4vkDc7GB@#rZGyk*R#oCgwJQ{*$*Z8jbNroTx@sjj-GNZUYk*%iP5~3#G3pRsu zeD2hPg~#gF&w<|}z%Z?TRmu8^NvGUDQZqK}H%91DSh>-|+h2}OHFrVd0MJE^c*fVw zj{ua!@fkDpL?H9Soc{NLsu@eJBO-{|VWH)UpM+9zNALnApiRhp_i|0AuGAYM z9vIQtuO(k+7qo{2O}nO7cZ@rs{n7dZ$6D`3JF9?r_0q{N@t=#wCS<*a)eUA&f#yS| zkQ~{oCs}g3q=@GdLxo0&-@O5OM1*VXcq+-aCXMwODb&A6F+1#M(YJ~jFfbSzPMBW4 zK`y7!S|;5J7hBE@v8;2(tCDEf4{HRyVB!NPpS&i&5A0RvN;E&>Zay{R>k{;k>;S5u zSt-4C`@p>@aSAUsryw7daY0@B*XY~e=*=1k%P^}b-oj1HF429x^X86q2PuGOpd1Rb z8?>sPnfOU=kNQpXcTwc~pq0`Zm#M(U4`J!$t=MHD*pNEBcb-?yj6avlPN5&rB`&Qc zToaOOa37~g0ysV%?{c&!a##^f4*Th7Y=}UyZnSelY~YfmuWUJnS=}j9$+Nfp0yk5d zDb*T~gBVxmA^m&h9ndrwk*(`4L&7+NN)rGVOu_M_teLbA$0Y`(dj$*xcE@C?1cXxU z7njzs*iXTT$dW4NQmN-bFM$73*x_`NclO$3>H)}EE+J^(S7a6 z0gtd=yh zQVGV$jDN_~TW#~YPs_`A8gRZ+=CX*#eU7G-U@s+q)%<)LKgTyc)-_Ia+TDm324;y? z6RYKV%@cY5K7f1b7fS#+TqF0D!s;#;6>=TOyf-2>7g(2NoUTmhr`cu$nVd6QTWniM zBN9=3u|zVrBNhJ$Glb3E#XbFSVQg2}hc3=+EV|*cP~+0{l4Q8n(0++iDEf_Lq$}xE z3jnf){h{|)tE2V3lub*A(vo*6kKXtDM=oePn0}jSgJ_2)nc<&2G7xA5RjV{bblEg( zZsO*9-)ZU=VPAI!l>e8?8byyBtO8gVDT$oO;aXuqWR3Q&YD(p59mLW}g`FEa|4h0& zK`Jb$1R2;A<6Z^$e6+qPRZsPJFsSgNz;HZQ)ey`EdV)3swCUW?0M(ZJ?Jd^mEXVZ-em? zxms}_flK?BA@Vahf2h`l@5a>KBxmH)ZQ8O>ebdw$1ikV~jG-<-0w$%%#g_XLXMVoO zJ#IPosSoTV&%;+VsG#F~5Ym>RY1G-}*EdgxMjv_Eo{audBX&;ggF}g#Ei}9VOdam- zcp>&eMWe7lHRK(y zjIXCNkc4Qt31O0z{KIAGkHlog3ott*nO)jNy2S75xoK|wEf5AMx`L`u>O+UYUqX+_ z{PS<?N?4uY*7=2teXTR4!sA(02eZgJHYFJKi%k$QTG585M&-AsvGE z%@yy4fzqf}_r1dG{UzpG+zCs_M;MckRxua){lOU0SON%{vn$>_{%vHT|}Sn z7{3#PRFxBkll4d>w0@{Plh0PZTjqI6_$`qghT!1Ex03;M~Qb~rFjknBX!8ao|y}@DV{GaqDf&2Ku z@1w9@;0|Dqam>GZOJgz0@jPR1u3Q8au`B|c95rdiZZbM6!=!brzB5v%XHJgVF#v>u z9bxZC6I zJzRc7UP+%X!bb^@NJ82dPk)rv0z8p+bD4E7HjvJ(8!8(R@Dn{YEKPu4o zs%ClY$hLNLcMxMQqSdMUfj8-YA4#D|qU=k0zln=Omp>~R7GjRF))xx3RmJ9$N$Bp2 zGR-v4u&b!`-IR&9PLe)C)s8C8P;`;|gDr9r(kVzOxnT}KlAG`Aoo&Fn(1nJ`+24vd7HaO*30PEbSHDq?%tNI5YDIQe6`Q-R<=x8ar6>akxLp;l)uy}gX|wk+%Mfu4bJB|H+4{LCi6t-TvzWm zgs58UWPwz-zmo`SA>QmNx81^m|A{sH=U*EzK24j28WhTZ+t&X_2a12Z{EVN!pAt!& z;D3MG1{{!rv;pTr@$LWTz}q2wx(SF-t;L9~H@Ycm4VPpbw)z?3fu4Oz z*%}n_6bgY{sG;JfHCO5+*9p@~|JOUU6GEa=tJa!ava&cl;JsDwXY!_|UUb%(MgfR^ z8n9(6`8>JIY(}zNVzW(J{u>=K1$-e4020&BfHocdvrh?_d|AMDl*PH5*u7qI9`x?S z9p*HhiY(KYP)rgTGiJtHIM1g7vH(j9 zN_6Q3d89v{(f$t>Q#4(T-}dKGF-n!kb^c)uHU9)3|7&Ofj}T<&!{}wN%3se5)w>P^ zKzwN3Ab{+YUdDc~L;|0*s)IJ75c$6@-WnkJou?>a(!P|;mMs-V#QL2HhoBYZ1i%*q zcCCkit+zn^#_QA?**pM30>XYz6@c_21(W^`=PcXJUgF`Y1SkPb zYe)keOI1{zXj~5 zoy{?_oa|Cq;&7H=H6#>xISR1TZ+veo?iUo;p(6)5mTR~Rb?KpR{Oo>OB#D6&cgr~rQF93eX=JY zs@rtoN1MYvP_1CGy64UWqU=*dO^$t89n4?(*fQndF=@VCVo+D+__kv|Zwh;)^J=PH zuQ!#e+@{HhhT+|M-Hd|&`H>U8DdglgNlHQC8_W!Y3KyJ%mH7jXl7=P=ocJ^8@|Pm- z;LpBs{>*8pvJhN~b@*6Jdl_ApvT`P3_g`t@u9>W4_0!j1Xu5|Ya@pYvWJ z*4=_iqN6tOZRJ^s_+)LxD7RbhZ^SB}5^F8x#-Yt-3aL}DB5rr-?o=reoK$VH)27^0 znLXvFi&YmmF~nbCbC-ROH#_n}N3V@L*53cEB_zPH)$ma;P7KPOZiMEq=eFjr>M)78sha?WVH$L@glSm8PNDcw)I)m`LIql0Ao z7Z~~aafk!V*Wah}q;FXSaX3m;e5L_ioiJapJ}ZO1UVWsF`j*D^T35x{cy=pdBb-0= zrGU9eAp}S%Dbc^b*h0OPJS0}oDPp#n{NV5uU6q&XTNLLqzsVYhRlPG@7qRIcLoQ1O zq~%e*Jf8AZx*R&n0U=b;5^DAWz!;|e*77Ke|0ve$!%a=&UuD!JJImMRTp=!izO%my z<>Co&@L!cpg8eL1eh_)=%aaPjsj?dEU(9S2Q^O~S0ugq|Me=zcdvX~WEK;U_plo=v zm4+W-D6bWx^HzMIqCCu`R%Lt(1Tt*-t)@n)R3a}xBAo)caQCrBqz&woBJL+f>OHu@ z8T<}#3hvCX=y0r>_P@lSd|&@+Zx8f(|K|@VhQGO{l#>n} z5*Rhg1yMvEY7;-zxg;y!9p;aTN_`!KC{Pa6`@R-c`2`!YM}ExGLd3L)-&f~gO4x*< zMo9)GwN9t#^S9qM{R!8+1?GxaA0;zoGqQhYhuy8U+$5la_mN4bQ?NcgqykF~v!tDP z6&Z!@E0)jZS;OA(N&~3BN=`a{zziaY&kUkZmzR5aeK8zMWZEe-uAnqK@0BDr{1IH_ zhnZWoH6`#9GJh3;fK}`2a9K$vPm*q|Ebc)aFia;=ly%CJ$kML3UV63usETO264b9< zuF^zOS_N_*LI>jq)%%Wd*~ve9XIM?PSCMfMe}g%C9H2*cIBD+5u?uo4HK>SMs@Zm` zeciAN7p^Uqqps)#jY7G|Ji7jNZffwlCSW%RGeo%2+MOlNta&$eaqM*2^OlwJYv{B{ z!Kk}RJe0CD0Y;L8rwsM^xGVBKRjct>$_!O;%}fa|*%_Pp^zL8>XPlQ~YsVF#*CNe4 zFM)aEo<<~XR)Y}uzZ-V{`gB?9UC}8Ww|ED z=SI#Kt;E@c95@pOrPGn`bd)^^H*-Tp&MT10>JSgt$mK16gzfjoC*>815F!9EW#J2A z39+~@B&={A98g>+4a@>D8IClDn_^<9oaJpr@=JhsG}Jlse31*F|9rVvV;(5ob5ytb z`gO_X8A(}68bJhoFzB?XI2w-j?txK6w$82Dl@wD1eS?2JsK`EwW>P!(1fH_Ji|-DM z%k9LM^TBqiK=Y##jQejJk4Oc$_1nX#NVh`9r8<`>;E?c>zLkB{H3=&6tkAps282tz zY~h;|BK>2OdI7yn|MxKM4CQZDRK{nw7p_HBF0baI?*mhzjgO8PNH=)(5u&WW^9%Ts zrnmX-qjH|4$XydBumSvWcuW|F9;x)13dpkCK|Lnizk42E|EW9=Jl@MJEPNnIhkma* zsjXLhTx_HfOjab72h+p9s;ablC35)a8Jm_?=_kkF(i-Jq>$iKOdu{X7*(na6cK8@0H2Do35{`IeH6vd+obm=_M)-yAE_o8e zjQw5D$N$IPS1{GFW@`tx0Kp-_f&};A9w4~0aVJ1s&1Hl9EuCKk!#YG$Q_SFk5Ahz)j4o3OhrGDV zGZq~#z82q)gi=0=*A$JNNS2O*uPe0V_&Wiar}500d>UUNe~(!2luw&P-P+}s=qPek z-1B$GG;qBXUKZ_*gOZh|SBJ$!lNuINR=9U)gfVmvshD|wgEWFZKLPF2dW!o0pI?^7-x+KU&61F%_$u;;A3EElr$Y&I6D?i8U84M!>w%pI1 ziTOVM?FD_62D_SDi99x|PqQ|7iX@kw3X3ZX)KioLWQ!pmQFpn-DmpT3Pv4aXjfBv4 zcD?75sq0T;081ou7*aOimNfF1oNSY$(-`BRSua-VXsX4kgilxK>F|(Usj-?4739^n zlkHWX-@zg*%h~Ua>(Sz?>SQu<=YUe;boqHDy3ZGz3LDJR;Z_=Uk!)M9R~E8T$&|M?mxY3vV`_3bpc8twYF) zJGUhDZ{WHn<#vv}CT3BURBAg& zc~wJ{`nbk$q<`mHzLVv^@6L-(O#W}L3=}4!JoKV$;D;E=$L0o=!6Aw6S%8Yn9=rj# zN4ZPcM%F8czv=ed1tXkgtUq?{dOje8W0P-;w-WKXBnQsdvUtf$yYGCNW89l;ni~AB zTw?zf4yU-YdP5FCrs=3Av#oi|P9hg<&`oh`CR@!kF03k&`sm8fh|=9RvkEHidPs?68xZ{?^p|uMw`ag2y@;3vUsU`7#FQxcj3p~lC5sx z^Y8vP`FaSBCJbPgjl;uzLD^85QAW z7i93F3?mQ(Y3Q~?-R(fK?zOm`d;vyt&Gs=>H}d(Nwtts@X0m0Tbegmj z*UC?lV)5Y&U3V=AG`Ry5p6ha0aYye`KK|%H4_fePl;7*XE|oiKWpKYDA_Lmp7sn7Q zcnZzHz(vIIHP{E4gNP@Z!Xd}fx&1SZR{3q|i4hG4F~Vfr8)YAESPdtV!giVS@^FCS zdzzxD@_|IwkfGBQeGGWGz5+XCZFN$X)5F;cP@AfjE=n8%U5t-9%~eLvO~Dg0PRlSt zs%PoY7M&jTM4C^nZ^F%ic7={+z0=NP1&UVcW7t$upRJk&mDj-I-7Q$QOLC_PMefL; z_nTl(rkS(>rN|QL<;f@T+8X;)#FD_Tx*kzlk3r74*qyB{J&QiPMaXUPG>z zfdSZ_0xj2`V9OHTwIMDR+jbA>`WoF=5C_w{#uo zWlwAO^pFUnblpZW0+=FjdE~kB>2jZbfcMPAgh;gyCB$azcGmBAC-$Jhow@Q`hd=vh z1l4^|2?TVBcF$U?C8Er->GbV{ZzKH{fy&Z>I9Afen_DR#ptRu!v+#!>031k95r!w6 zF@&STjS}$q_2EQImJzAR$xj7GG55>ZOmm|cf|GD}B@;hWC?+R#1nz_vD2Eejz+r1V zBIgC`LhM974MZ*uzNVjkT$7*q<1mDC=l7#5o>UU2ExG5W`t|m2TMuZSd91#o>v_He z9)u0?$-64*z0_+@#$w*Txru)`i=y#D&F3083(>7}J1drVM-ZlvK`kG(5ZduAA0`2Z zI&jzN-&gb5E?h3f%Bb9BeBb_ELlCjpK0nty_cTl2*V8>7@|OYqzdpRGg)Jy-zM^Gg!%p^7fJ_ZPQ5F}_|K#N%HaFez+;o|?>5^% zzRx$3T*w&I!;VBJ`Zx1I==siy^q0P(V=Ip|?*H-tLMU4Bhf+!Pw|`Soe>?$E4_ZYv zz-I1j;6JYt={ll<29CXK2l9`9_~!riQ|KBqqmtHhF3nW#KM(M&I55CS3ry9ZKOMzC zMfxAcc}M(yQu0VWVxIvY4L z2B;|Zae#tVJOp%NOqauXj6_yr%m8GZS59dK449Nsj!fx1!(snH-LJCy!ahj`+nAPn zKYV0y>r(;G?u_N*kzCv;P)J5~B3IcAB%Fk)pY%)7ahZ*NGQ2KtpY-;e-G1s7vb#Pu z0QYV53z8+ZT!z$;485tdS;B2`zru-rCw_rGNDeAeF}xy~6Ycn#hKf8_yGL!k*qj+1 zl*Dck^lq4=_Oby3AL_0qWV}VdQ}5HDyN^CWfQ02`#49sIRsa7z)aA5yub3FXK>rm+ z@06=(>7VmHPYXQ2FjU9Ux14$*50}EDXV8JSaG{zXWq^a_IoRl0y4CE1HQ$^Z|8CXTVyKgUH1i#X5LM_X<=(b%G1>Dnrh#^;8Z zVT`ky)RxwA*9rJF+94&^>ToCRcMD=6*fgR5cv?H$F7Tf_@gcE|2~u2H9KiNTf(hPp zb~qbnV>zOTo0~4s0SeO}-WyYDA~d#ff4V}s`TW#kb-%%Z z@y6!XspL4P_||C3nnhnIraz>@=;JZbR0gN>JS>z8Oe8Ic7U^X{K#}EAtH?`Y)`248 zxzP{psL!)JmRL?x0>YTC3NP`wL}{q_(pb>vt6&**o41{<%UW+MsdbwZw76jYob5PL z3ZI)2{f%28Xi&SsJ{aw&w&rp$3d}yNuJ(a((^GJYCym2O z^Fj%!>Igu>2W|BGU~CPg;sGtza3_jD7y$oM&8nd&HhAiREVT{D2x(x`D5;OfD*x=` zQ<)7}vw+EV4vnYBfjqV90J$RN_2G2A_fZ!f^#dZUQ6VDs?z2~d_ta>FT-0)Dd~cO< zr5Nlt2Qqhq@d5$Xp~ejZK|QYeqmdSHTj=i&vCytcV>(fQ?jx7{?k$uCyVZ|(r@+GE z0v(Ake0uR}fLE!faa|?bq(FMLvvlEdN{=)6Mvwu!+tS-pVI`3bf&+PM)R-U&p^I*q zLxOu{d$LN|Yf(B)SJDtp$W80E6a_}bRq;XZo$wVImrNh%09~7^pOFt0K;7m)@tTx?)+*p)G>EJ z7n}k-ekzxpQjPfGd;>_(VtQH9*e!2t8P0q;W4QBTZ^|k1qRIW@^+p>p6aY-a*yxL` z35Ton#k=BmXvKr{T9`aNK0pg>5qsBvcg;H)BeR+X9B8kLi|;?#Eaw&TxLl?rFrT2rySf^T z_t8>g53J!cc&r3$iB6c;6bCY6?kSF*tbOc7e{3||s zhhzPhj)9mDa3}?P>Va?`gC!bOlSU+?7as}oD$Qr(^+F?VFI^0>_I#H{jD8`Fqd(+} z0-cTCS_3ZRS8Cr{E8d{4(9MRItY1HaG)g3(@nzn_jd7jUrys2B~GDE*84U`tJ@=|Wr{bb?7#!UNkFzaLuo=&vOgspeltHtM> zoS=Par!TxF8Ju1F2+xEcs`dYVw$g?Zcx7J_7GObM_c3wADD|Xb|$Nb z>Cl=?Sz>oM)^2whs%X{3+EHjr{X?}>o3b^XjqdXkW>PSfSS+JUnB^1IE+37zZe?3n zF9-cT^Fdp@P{UZQj`$B&F)8E14LWbf2$4j~QWLYyz(&5gb-0Sp?GY8MmF@ro2;`Qa zxO8{iiZsuMAA}v#N9eQ8Yer*o^ajXPW9rEzuV-;Z>{tvbFXYMHNi|PBc*zHr*iw_E zL!AW({t9B9>PvV7IKdSkUNEBHUOcHVI*;@p-K$B0)z#fZ^u^5U9{KH7zfL!Aa60jc zg{-fhB(Vc2#Gnfy8Q=gHwK8i!>z^LfstsE5o_iO#K}qM*1CCNkD&W{LcQJDx61dw zKJc1&IeMOHV^T@wq`8zQ=+PitUP_`YQJGJj-ZeNp-n&(p9Ju>BJX|e3iH7gbj5j5D z{oX#`fpmT}dkAZXXDBxr^^cRY%3Qm-3w0U17(KlNdKHe^Up7}ZGm_5X#|^u7k3&m4 zHOrHh;_^9M?9Ku`VQ~`TJ|n}c>9@41X%3Lvs1q46cTXYzT5eQBqcCK~V0>L^+)qpF zSzhq^GwYf7oKF35UB3tGEe?Egv;^HPkCS=w$iwU}892V5BOLgj0&1GZ_P_WCq6HK2 zk`XTC%g3v##Kqm^W!OB>BlJ-~Qv&vs!#P8(&l42)b*-#M$r4a>RD^EuidtdMavg@m zSTp4o-h|OI)>E$c>QvkQ7t|1{>ZLPM3>b1?%oKR{M;v7i_K?6BM*vwiKX%O2lRV8e zE3V+eb#U5bo5g3myEvMkOcyh$?z0)X^Bf708ULCyDQnwUAcVkY@jiY1ePw#?%*~oA zbFsoVR2Mbi=^;M&tJn^dj$ z&0H&8^VrnINv+3~*N06uqTkMU#Bj|X3~hXfNSFd>qQtr8UEDQBPVR5ILc??jd>vhB zB;y%VzvL#;=(8AY(x1$NpOl%T9>-ZKQlyaD`@WUTRl6AZ@gd_e=ICw?wgw!a5%Gqa zDN|{H@Odo1)|7DSC&F)RG2VRmWJAZb*%kU$*IE&+lU`&iGs4Xt5xnMhdyx;R^{K0> zB$P;C&dj%jMvKs&9SWmi{UF$W@fmq{rbY|>WMLoCg6=HA>y6dYM@`;o@TD4BNt$@+85#2FjT(20Ko;8A-?*+6vwtl*!9g}V^#EJ$2(~rh zJ%~1(*yXP6GG-UctR}>4Dw3l_tC0qb%W$i4exDJ~gJxmQ_3%x|?)Z|oWP;<6&CSG{ zGA+tq?ecEpo@A>a;+!)XVnk!8LUw~b+{2WUTg2H9RM^#qOzGLITn_jeU;iG4Z;*Ww zU$n*hUF#)Y*SL&dY<}lG-cOn8#u#+g;u-jP{F>X6w%yUu$5Y$=O|`qutsLfvNJ_PCG`)6p z=Yc^){M;a<6br1%h;w(jA48s|D8=y!-Xb#ihcats%>3nCS>I{D?yEfe75qn9kmrCm zSH=SYn@K|#fI!dt)U2rc8H)z&AlF10N<`x-IKnFKjd$Lnmz z-UckLB43i!UypZk(x&1Um#&)vc|E+Snmb$yWS2Ju>5E|#di9Z?DG_Q?;A0h-;csVMA~SI8;0c2mzI+R%@1B8=cIj?{v*~6=x7k`PK^8d@vycgCX>*DhvzmX?iBBJefMXy zm{$k0wQ*zH^F$B`otF|n@MhA=U0pR&Z^UZI^9AWUAIUPCBROn|j#l%V4iQRed__p3 zagF@x#=D*mhcYN)?lInDF!>n|nwjiVc;(yurlYKn?&UV|jianv{btdzVsAX%CZgq5 zQwL549ji=-SzIPA>npP9Nn|7h?K>4XjVV5ME&t{z&9O?3GaM}1Fiay9{?;|Hufe8$ z?<&j!m77V^9g)uL@~~Lz@NI@aCT$i0H)Q}a*IJSc^XPVlr*8Z?l$GdzWMe6;hVsK+fY3F`>(^(_cQ*+SUo9MIRA@+3 z3o(}mxWK%mHEY-J=BzRSZ@tIfbG>3&5?&5H{k`&&r*V#Haf+FmT}oh2@NjN!gC9{j z$<*sEH5MKv`Mukrm<7qOIA65E!bFXtJ6 zO%*#PRG8#ty?#+PZoO)oq2R0QzD}=6!ufU&=#Jy7z9-$$Ug%pmCgor}12J|XRLs?z zY`7B>FJ7&KxY*Ey44LXM-0B~t{mSx_YVEFSt{Sur?@7j8fXJ2MioC-_nfGve1}1%3 zwNRm-Kn9%5rbPdzmxxZg*QIEx{)%*D>EMSupParrw7xLTrOzf4G_Ur|Q|Mq}JdC=( zW2f>qpuMV#QB^3%4Ml?TP-Y1YFn(355UjoMaLr#H=^XgYqNDt8e*z&h1O{ zMN=OMR*%(1It}O;yi(R*rdC&)te4kKQkIDhj_Aryb65IK$o>oY=P$@pqmSnk)2Fb~UJ zh=nw+P@O82%C-W-0%z3;tQXRB_)JQsxnmw}NcARckD30>-?5g`&?L+gCd0qWm%w=ILX3?Wq0svO$D0 zRAxaEH3E9IJv59CIzJ(MeVykGGE^=mEcnU1{Q>3iy#dC{_K;F_e35uwgH?)=R-HHE zPr`r+ss>}wdT+Q>X@Y+gry$XmZvezZCJd*L1UFW5 zpgeTW;^Sj8U;lP-Z&pJ?fGDoD$o^L4p85;W3a@x2U+d9r6>Y;$v zTCCrY52q|AxU-~oU>IQXQe`LLo2{HhPHdAGyS`{EO{~X34n%>UAJ0IcG7+4ZP0vq> z+3j)Vu19lLjw(E#6fspXi~ZQW?A}3A+Fq?DT6i!2oFBCVIEs~2q23Uy;8%>0e?Z*3 zT)fMQ@zh^H?PW}rN?NVH1*(RQdSc$2k?DF5Fx$)lu}6D8%W(Hy#H}2EnVv#rMO=fvj7(`XL3IhJf{x?O8Xh zX*mm}oD2%KYa7em>j^OTx4Spf680`p(~X-ceI0>s=($rpM}>WDEP8a(Z~B6?d#3HSml};RiLCDnT}7e@{^qoKOP-wT^#bD zIS3piGs**Y0=qcYR$OY;h3|R1EN+q!n+8Zcvvq2BV2Kii3Cxkz^@Xz?w#XLis!W=e z_$;kl_9n-=43VWiH;vAy*pnIXyU8Yc5gA-;0JWu;0hY7X8Z_YX@CcvG-?{=tJwKm) zFw#SAV;t321nl+&{j{I!E0UQAt~8sdWDWVRnr)j5xgCWL=2bv8^VjJE2C3Tt&)HVlh z?N_jmnl+wUiSc_stUQ_%X60k18zPpCm$1JRcE@nBIPJ=_C@wbHi^*I^R`O z->l=1c51#Dn$^^uZk?dt=k?kLU?5)O!qBd548mA8%|Z%e1x+*HaSP`KkmfHuNxt<4f~bwH#>T%A>O{6O^N zd#!b;a9brCwNI|ndppycgNl7Z^qq!vX;t|@LjofSx$O~U&I0?95aDOaN10(KjT6Ka zAiAmQMt{OJk&xl`S^Sq*aaj~{ZB5l%?mf7Yj z%F3f_$uv~~NtaTAto?2ydr089j|peG_~UT{tg^#(6aG6K7&-83cqMeIcdk&L^AWKO z2mO*p?%S2Qp`E8V5Q)&Adg}#qxDjZ|1}xux90#ty51Ebu$UoJ`a2&bu2{=pMVx&<@ z<9baWTn!|$^=XuGZ(u8v@wXK4vhcf&E;Rlw$smRPEv^qYb(O8lSh#kZD)|_ z55uHY`=REk3}Sux8DshbEj#+*(t#qzfj#Ry;7-+pVJl6C8S{y~K9zhxhJz|rTLmxw zoMCkC66y3>I`djg(>;1R8^&qbIZdWk{1ESD!|VJMoygi)H55aWEscBmo)N)FA0d3+ zi)Po4W^=h+1d7r>8JdCo8#>5)yt*b8t^15fdFwc^OF$q>RaA}*)$v~6n_qTjIG(92 zcER_>@SxOZOG1A4f!VR6aaEznHwrw4#@!!Bl}2KON>&*O7+(%}@>wknUY@E@efwkQ zB$v}vR99eGId0`DcQOztycOUW5oMWAVzi~JU_e_ z#V<{fRm1%wP^;4X5zyJQVQUULVFkFB=_v4hvw(gpCkVdLUe^j=?2#Ye$!IVNx*BR} z)TnEoOnVi#FU9K&?4$w=KTg~)yE}N-gUT+mef^s@q`swf2JiHNEMRpjYlbm$0(^or zYxY9Ne8j$ElNVnF@^_(}Zp=o1k3+#~vjfvazmo@rzEp|z6764T+^jV1&&%s;3nYTg zUJk`&!q^5cf9}g0)XI^s5+5Y}M>rJfWz4cSvLO4bF{Fk%T3(t9c>?90mfWxsqx!2^ zgbsa%PssF{O0ZY|mKN0X4bI@6i0tEcu4kd@1RY_vDc=(g_u zT6@;QFQ6aM>AKi-@hb>u1j!y^{Z3b`k%@_2G1}g1j9r8;SGB*-Q=-4P@%%AaS(65@nzr_+5J z<(PzAi5iNsXl&OX@?{BN z(K6k@pRht{d;X&9LKHV!`^Toq*`#i$JEb-ixYt?CEnRt*x~y zLqZU&?Rq>WU3-EU|1&oR8s)s-$-52cOD1`iGmR7THMKwwEo-YaeyE{+S>cB{$ebjQ z*9GqmThg&D7ANXS2JVs%kdA257MDw<$0^yznlVs=EoWG*51`;EO=2>1Li+bQqg_{D zF92GI_~&!&9qBv7`KmtJwst8!Ti2b4X|;5b*Yt|o<>e`hp!>Hqd`!gHnY8$Y?wd1t zhRYdmR}Wk?wY;GRZj-_lR~$zl{pKV~XUD)UdMBaP<4I3r&`?Xf%rn2-)?kQLxpqdy zZt|JW!iO%|>|+I+UcSCh1U+^?2;dAZem008({J|g(mzSpFQRXKkGBKKfLjx{KYgl|HGvyo3QG87RJ>CTZCH5e{?c{^^YYi_@S1>;fZz9>pNJ zyzS9sM;tS4gPAI^eo`I1MZv{aaDQZWNy9f)MkF7GhRgccb6{+n4s6CbVq4a2)iC^R zae~={Uwfh5nxI--?lm(p-=~68t!=hw_3ytSPeOQNFz|?j9y~GRn&lydaZoUUh$TT& z2KcXNbhEzqB;G-tsCN}H-t2PuJ{o2eMyU*aZhx`T>ZRxpTdvN2bfrGQm6zYJQ+Z+` zhu;5#KRGmj8U9P_t$8ECf-!H!ik1pwDG_79lz9I2{qfI;O@j&$8XCX4w&sfsx|}qK zCV~If=Wy+t2@i)GWRUhEM#gf3UPF3NPaom7h(yRb2AJD$uJl|a0phnsimT$QPuka7 z>3m{{uBoxni&arsg@%7RV{7-J5kZLOWu)Pfq<(`5@?tVelW@O_gSUcec*4aOL3y?p zk?6RNW{;bDJcaHyZ+PgXe2tmFfacJF&tsB^r;O;2B_N?zS3#9p)#T`UGqqhEuK{28 zheUHA%~y@RV6Vm_ zSmfB0&A!ugvXA|??Q5_e;=Nl`-NU)X22G1HS02<*q_of;$k7oX>&oZw70lQ!oz3e% z*}@yD%&ce0kNx0wCAyG*FKON7E00kPhmEND8*Fo#I8725)r7427lS<4b&Bt6zO? zXCsCw2z$*>iTCD5oSyv`nL51HZ5JFO5*SOOWds#vp=#|m=5dW zQPTCDn;d*)ZR~A@ru*m(`&k3WoU#gKI= zv$!4`<}%%3_Nkj8kW4@J4G`$j>LEV(}YL89k=+Q5OZAx zEd%lP;@hnHc^o=fYGnZ##$pm(8cI4`HL_~UCqZ$bP3``$0VPwRUawy|=ZHk#`~7i^ z0`f#$^S*Y>PToL4)D4T;IZ2xG2hA}Z9EttnCm@)&3pepSn<3p1|90l<2_W6u_0O@5 zYO{l<>ZxY8GcLK3&dO7Kr@+om_COlqGG2O(@cWeu(^3oG2-LFR8>*><5wR)J zN=d2ruKcw7bzUg-whSg;=0T~g4;fcOQ$OIg&=c)fOibjY@KR5qR$g1hgI|DR-y6c8 zY`@}37}z58Yv1%-!g4YwRO;_)M5K|>ZUijQYIgs$q2U%A2iXj}1T@fG}aR_C}YgMwT;50aFk!%AWjdr_!nt#4`GDgPuk^%rJ_ zr(a>oCgoFB%|{tbM#u10ep{KiU*(f|dN=s-m3g)Lp*}QipY2^3$ltxSAh|f0&CRZf zfRcDwnl$<4b2^fD^Hhi0T*8PVOmxrrL7kS?q~`H~v2Hpw46aJTVHplc!r$eWxFzo5 zV*SVb76syf=K`@+zh{ss0GXgC<+u(ad{1f~f`iHH^~LAd+vPb=5|2mP86fg&rpg2{ zztAr@+LcpII;Q8&Zruh?T_2W=Mu`GX&{I(By<2UC7Av*<<`b9rps5QlHB6e%p~lHs zRCS9y!bcEX91j{f7=q6u#*# z3Ais~D1y7++axh0r= zLNY_T05U;EOLJ#ulY+1Ck`%{Z01Y1GaN$sJZIQ03jYwYDHmZ3@S$s?0ilqff(1<)wOeUIAugf32a zy_CYfXVGoM6;-xl8NxW=va6Tu}&G=Fioh2^`d~3EQGcBXa%aigkz0Wn*{H z?U-u7&wn>ZV0{Fy@cTKGzHMFKR$hnEm(F_Qa8b;5#`$2_u{Hkn#Aw{%_YSwegZA~9 z_DOy9F$)|r@k_sss2N6u$Q3W+(;TGHS=!uw13t)#&!Fz)%FjTZ)ftxo(|efFk-O%G zrNVYjBJ>-62DRyA7O$i>yC$I(*nY~Y`R*L~_b=_|N+A1YbG2ra0zm%ZKnLD!Wx>Oj z@WoeNO!=%i%x(jkz~*Y9n+t=oR(CZ=*SZp(a9_!$j3r^=5MS-4>($qm9y9$b0?(j_ zPF!DDd+SS|idZ+#Av}|9pI)5y@_A3oYqJmr4qJGi?NjrrTr&a?BO6c=#=Z{?UngE3Lx{nO4`e;#gyCzrC1HDVB zc1Np9W4?R0imPK8H9` z^VmhwIP^Ic0Ho*v8duLn#F774HvG-AdQ_UZ`DbSMoJ zC#ev0lWprSNZDMqOh@k1+H4Kx-@PBy;{)pldtS_fKqNkQUlI{&FPcE@&~W$Of+7vJ zjl=~a-VcNV3~J*8DcsRDkw04GSSrf;JFX<7a!&dAwVWZXiuWt8Wjwk%xmnQvs3HCM zrH|Ie(e?YY%K8r%sT@*gzu6Z%si1)WjQs%wg8;z3tiEJN`OnxIpiWf`c!u6r*Z+d8 zksuvLf?GU)doX7I zvaMh-fc{~XG8*uIKho0?)CN?GQ!zd{h-kOki*yc84KbV3r_ug%>At>z zqV^cgk$N|pBliyT-FH13bfBhR2*D8NuSZI>z46`SJPbPX(W{{^0JuUafPQ}TKtCu7 zOe#72BVD1ERJw@2)(KYp1&mgm%@dvVLL<5LVj}}kSD4M}*9H3KNE4F_vgPjfq6H*0 zja&L(0D2H4?MC{N!pDZvRVtYng&g^0ZJNUSp)VMJ?N3KkTZz^az&E3V^|TmlixX(_dePC=IPvwt$J3BZfmE7Ls!c$7a@i!H*G#+7taxP_=K9%!{;i zU_q3%o1B#=C`4{!04fp+mj>0=&@J;XtpqIp!hh4r?fYib@u{QBqDY&%Xf#jqqZlZq z4jSd3&-1;OeStQ4g68XjggB3^4YA{0mk$BZbv9(Gk$>(1DIt>fkMQvTLe#Et!RAj& z|Ff{9>d@swzj)#1i}hMw8yMqQ>e~GX?<=a$7-0REKY9Ue8|i*<%Mc4KP(=M44*SRT ziU_*AT#Cv|RromE*Ck8=7eEy@IH_~~De}JM6d5=m$@L;1^^=GXfBu&c^a$mbiLY2x z!3r=tE@9i71X??V#UTup1HeK3r_%AGuw@@bGLPz1QGpAN?EULLAtHrrlzM=Y#>R_4 z;r|(l5Of;Se2lcLNYrRi9~%YnUl3BngfHH8Z66pj>8p#8Hzo}h{`#hls26dG-)xcf z)j0&^|CCn4FyPOBAHA+nRd~;qS4o@nBJp3elwr~f)m>i-nSwf?76b?g{5jYx5g~}y z*V{Mnv7iw%<2>L{_;<36Kkfu6J}VV!Ig`J$KK!+w7}m?#(J*6CLim_m07KgJ#g>5G z>AKku4y)ISwK9@FD5dk5DcY5v<;eFpmF;~e%@2SI?uXoB8O^`0My5QeE-hGq;JwuS zolPe#_am8ikuHNG(8w6aYjiwub-fTGde9A~rSylmF{iQl7V3G?g%b$^PJpfkpHgYH zLyiZvh`q3K~NIauf zg=+Cqy#_X;iV)ve!Qnz#qzEeT;!(n*5`n|t+gxT1-}#B)p73mkdvf^DrjJBU>6y`1 znhe@c4V$)SZ5R&C2NV&LINC7TZ}vD(?R6qQlRo`g z0OWC802>UK`#EeZ2sd__^YTEDLM~N+9H4b_9;O4A>Oc;&3i^_%^6MS8^a>FfcCnTL zm^lc-YqwfGT>mJV0MSpZ_g?!(ePt{8gI5jBC#q93MUV60T{s~x&8Z}@F)=NM0{2Po zQ|E>kIS(I<9|-|>X+KI{xP-&$I*r}-F%U-zD5^vT7iEM;p5Zb63aZ>)&dfIKx8W5n zilGrrot;ow%)C8<_za#s@#-6cEYHRGlB<^+tzEj3wVBz+?E$T#V&Z8M-(ucCu+ zWjy3M6$`T9w8Z75Ob(R7K~dMhnVMe}akCp_EA0XOAn}9Gt%nYr z)z83jyUG@}+lzC783M3=hT0oTi`9k5{E?IX^9@IMW+N&3SkpcK`yR;8T<|%>+Zxt;1yry_#xcx@wZAB-W7heU zog(0LH)(aEu(L`AoH(QTBa$cGrTnX#00}I>T-7(M3Lq&4Kjr;Auc|gvZklkabA0rY zlXDtK<;@?U>JSV}MBDfSv5t(pYSNn`3D8YUcirsIIBa#5>UebNjV8JRbY`KWxuJzW zyy`kEp^o)xY?j{IZSHgb(9nFy2nXt&%e(9sS||zbYpwE6DV?{5h}dPb6*BJ`FGgq{ z8|*=r(RR@kl395~y3LLn_pM&>Dyqadxl(-8QgO%pl=53nJEMYzbC-hS-0uKFJy{JJ zoyr7>IQqiyEbo=Imy@U-mqZG83aTYKb&^*3((xa$nQ&Lo2PqZ50B+vF$&{X{SQc>N zUKU3+RI5lOy$_a_+s}Ob{J}w}uolE9_=PY{lamd{(xx zm%BXhx9>YJ;I?|_R{4qb(JSjyFM=4E+o2}&!cM)V%{fMonZ^~ng%4;h0C^ML961K^ zAbth&7BeU$A{Jzv^fd5zxWmJ8N3<#emB?hnxDhln&_Bg-e-6M-KsAzvh0Jw#6m0z` zlMtAV9r-tIV2_Lq-;wl6$lC?V83Z_*n~%>+aF|1?B-LWmwxGZ>9t;E z)NPHEA}L@Rg6-W#+%?;7d@K(55Tq`=r#4n3YYR%`yTA+tL=hu!?XtlSY?8F4Lj`=#PrOyTV{5=<_7Lbkp zR9es4OX&M6+X=r;iMSKG=-}u)x_}=olm9}emplJVrx%5s+hX0ljIai9ZCyXuqlh;@ zlboIQ(#MKnQF$>OzcrG2-Qcd+G-*@s`7&vTr`DriQ3Ec2;`(>tl_;vThH+j$sUT~& zOad*yc4CH0rE7g#U7%gqjL+g8HGR^Z&e$F06|K@dw<&uge8hKgAV_d0y(8+mkrc+b zljn2hU!YSJ;tFtS#oM-28EvH-o?jPQEAwW)GFO1aC0+ViG2G=0SJ0f^XLJH3HktTW zH)^+$67O(qA8Emx59d|S{2sBYHW~EQ`^G-%0G@eWx{mQ(cq1` zHr+gpKeVR}jj>1UIF&(eo2e;dE96#bhz^CVv|0~AE1!VkEBU3zpW|M{2TLu|Z_di+ zIAU1zhVK`VyU$&`y!h3tGd?ff%I+o9nPQb{$!LAc;!!wpJe0eV^jK6Y)y5$4Vbx}6 zGa&xSJ$+>1d)@^)B{&=0C($|d@A0|35~K-WOA9rDoH&-LGal_giWf=SPNz?l4L9o? z2F;5O%t5pTcR6!V@PD16z=uv+UusmLVqC1GL?(?|DVm~VGCfW29Bfwl?A9l{AC;u# zQ-mZCO+~o9nO~MI%XkxL`(TGa~n%Q#X!9Yi_SPX&3?(gZF6^#Asp@P_;>mc6=sFVl` zjb_JO05r?fV)5Wdu$$!-xpLo*O%kC1ok4V5u}&euCQSS0t$J8lMm6h~Tt7kRjp)LH zK60WMa&*K#A|@Pt+kz4}Cpv*67+0AY7BY*d})Ibhz-8N`4y;@-)Q7y7$7wQ!L zFr-f>l6fJ!)k&i3Q-Bo_qf%o&1{5{Xvo$u?DV9V?`YQ?QvB{WZxqSl>vcEJ`IwwSM zxv$gYI+k)qP$mea;K0iS$Bqm_jYbLLDvX~@`OvfFqRuKkImp3Hn8Ymljt;XS@Me(f z3vV6sYRyk4?;4X=N!iv;r|LB!CSy@g%sOhWKS`-MN^Ip=W~&y8+vJ|iBk+7KOYc>i z-5-BA!+WNwO85f%fOr<^%*&?nbk*;|_q(mP(>PRk;47RWaQb^UdV#r6I<%erdE}EU z9C%x4Lw9$>tH%ctQlYU#a{>2KvKN<8zE^!YGEC*I9=1iSt|Ihb(CXxNKv z`K{EQ!Q){1Om$Nmf{(jI!MlVP9y(Mg)Ip03``&?~7R5g7`JK60+*_8^Q!s~UfI$_a;7==<-gaQH){ z5vi${UtIk9ktj)?^Z0kjA#M^to(Qa$!5Dfp&NfH%Sp~h`_X#HD5VGTEG~HI88~atq zm1;ExdtgxtgoH(FQ*ykI``#Zw72Mije-xR1{>lCF@!iH8WfBi=WfkW>+oa#vs59p|?}(Ml3$oK(T!*5y^F?1p>^L&O!zgJV;`U>DkBQL;z5;A7+bq&Pl@UJ*`>Z+eqHuSns)`4W0VzI|h{2um#!P1ZgjXCp zjgqWEUNnhSY=aYg?)y<$-1e96oz;l0!RG*O8qk%XK!qcd#Icm{%$~bv` zom3kM(g5liG7(Qn`()_DVlpkjb?SA*;0mSao9;!_P^PuNh^jYTi~*S3PY+hIGPz5?hmQU-QGS z)hWiY$zP7W_34-^1~M|2$V-h{BBq~+%MzYjoIRY%quqXJAmuRx6`Ln=2IU2I3!+1* zkqIT2z=pJh@Y|^L6%>@uqQxh#fvJnF^lE-a*#cM%zzKT#=;F1=J}%UXvP>~#99iB&MN9Wh zRL28M8vhPejkE#3Kl~V(ha~`Qm5VIO0E5bdr2c}R35`iSw$uVu#KGHMkJhgwOnIF9 zpO0;ZCUDy}hA}#pLca!xpcErtLx|#Xnvy@R9EM!X zwlRMFg+lt4!Ytk7oj}$sr9_8y?D@E>ju<8<_Faf*$fU?Q+7-L%&#^H0kI4UlLHRCR zTt{E4t4~^8N)4r0)kh=xc3Ift`ZMOn`td5k@%uA*_DJTBUB(2(A`&&<_>BS{#bK-u zB3M^pNc>~S#;NsDL_#1+d=|I`k@C0knPxavr&f*P_*-pd$_Y{BL;X%iZ2vdc41#tJ zb9@NwO+;9vGZy6wHvM+ntiEElxhh&6U0g!!OC5iJg54jXmZ{xAOyZaPA(cxt((u`P z(;a0hL|w-9?A;|#MLqS}DG-36FBv(Uaqt>`ItAz`mi9~BRlL1YKml>rR zK>#Y+MWwG|dnY`p;0)8RyySD7XPFB?8tsUn(Bqyk=4<4{k`}SADrpJgYCJ!q9$PTS zn0o>Tl<1;J#jr12a{XN(#;sablAreWL%Yd9E?#5&(-r5Y~;)4tg z1n+xg%M5dCm<@L%2>fop$=#SX;`a3iVPqoQ_dgQf3d%QOjy%MPc#$odxNnnRkBj+4 zhaffBCa1;|;*S?|`zS6Xj9Rp1%+gtKk2Y5Htqe-WfUJlT8rrfQ79=A#bqt9GR~=Z; zW46LKdWUbHc4+*l>kMXIg{B~QNkux{SrR3woq|SDb1KRc=dV&F!H4Uo*xY^h zASLI2UQ|w z9aC_e+L&TIjJob5e*$upTFLjkCF07_L``hgW4GL5NBryb)-l*4xeVD4=33#)L&A7n zBKt1|rt^t##%%I9zfqIwjbw`e`26qk_g|`-rnGyz>PqzI{AMAe#q&b$_8~F(jzvr+ z$Dl%ym-rGloudnm_L&=3z%eJ0kXdhR?Rghrs;Hr${+n;?S{1?H8o=e;HuFhRu4+7V z#b@HL!vpW#%sZFNUNf3gseZK{OBPF&nXFz76P#WrqI`#kz<*rNogc&C_H8Ng_fs?@ z!FUb*!Q^<5DKOE%TjlH=iJxG4`MwWv*E_9-Ik)K+y$K7R7vU#d3vxLr9-wgfN-Qsb zE8&95BFRX;+=oU9lwOn9J={I7IW;bVFC**osr-Qvoqc>nP)$H&y=tnx zV^r@W-Dm$UKTybm4C+vL^OW^nLt_!hNGDAhleM^kS~6@V&?Ea0->RD3CU((*Z^^)B(F6Igh)Xl^}&TFK%^n&t@M;YgF1OKNO z${|>u)DtbIHyJcxH}dZVRTwQz9U5R?CHhd%-Gy(jeGfVg5>%0V;xp^2Tl)6+67#ZG zW_w;5daZt6`Zi)1SZ;+;xz!+DDpDo!TZ?ml4> zA(-!zu>|veyrY+XYRcM?N?jPy&NbBpq?WpROdoAt2Mr)_GmC4k-$k|G1O0l_T_czx zE|yZHt4`4ltT`&NB02%d2j~@Nu{S4e3q6?ThLx2p}Vefle$Tf zbyL_J6zd)rfDM+$p?F{3EaKNjHvcuU-LvaT>P1OX^!e%^QGb71zrz3Ygw%svpVNE; z`FPe8YrL1w=2fy)346yc-0kx(*Ei=>0YBq7%fLgKrM!mPw2?VB z!tcrzJrPs+BtVZ@n_8dDnZR|hec1_9UeYb@feNujq83-Ni5}^l$r!5a;9S#vo)*cY zW*67VTCer;kYX4$mqX25&^e{?`upP-A8oR0?gWU+8jh1<;r!PtZR1b4$UIkj0;jxA zUQ-CbD^o&7K75PuSrfQ<&DaoSMfw-@){eo9-rxW+b_0W;} z`jzF2)Jtd*{eq@`A`*^hKGl)=X(p0drq%zT@-VC{$a&;802ai&6rIuZy`_$M^e#c8 z8o6wJMfxf(d&I+*CDC5_t$3Y8;O#2dUpY*3n$Cv!DkoDW6a{IhOq^e*&5{5pIrk_( z#314Tz8I3YJ~JAZ6BWBY5LdjE(Xz8tyC7p}-0k1{!)Nm_uC&bB`SXFo@+oOB6L70Q zd4QCW-f^M5icsJA7;U`*%b!dhn~^b(4Cr~*x^zJ@&(csdf-oK%5`G=^mXdJS)uWSq zTOzmanFV4d4_c)-jxmtNQ@8EH#i*b#oa4V98NWh3wqwr0kz4PClz*%_6vW@|Df`e4 zy^K$7RXIq8<|f3-3Ntj+F$b=J2DG!B{a}kBTHGZB1V?)XuVcR=GOPABaFj$9#6jH= zv6bJXaFZg9iWQaq@u5iIy?OpRy+#pZR|93KFGWEuFJx zwb*lL<^5z;DxK@hdE1NB>L!~Ln4`4W)y2M$SA{dgD(cQUI9~Fm<|Y18tB3w+%1DIl z028GUH6)`iWZ@4eVpjrT24fxlCdXziv5}*>RI!xj5x8z4sRB8plsTAVMqgD>DYp+J2orbFHAq%^UZFMQy!= z{aVVV#M-o1mZ~gl)JTaBbFe8n0%1Bw1&~hA5mr-lWu$=pdlqnJ6R*WAYxBfm3E$OV zxHDtcts3k0UBuAFu<=SV!!w<%)-z3q2fIC6@v5uCOR-Yd@+Z&M z`}I>8GOGGsaPL$1*4sGNJ(s5yLKp|46 z9=m+oJ-67wUlFl*-&g?cO!O*uZ$*i;N;_#nsRhGX(?0&=V+y86K)pTK%Jy&Vh{Ue= z{G@tK{1`3bMjr$ZH-(y?Z6Jr2kP6Hbg{sXXWcozIN=<{L#hQ6s+Wpi zaQsF53vaT#xv7;?PaE~{eqk5_7SDBgQi?EOC6WU6Js^F=fZ{sri z3CH{o{AOKZWT0AwrG*_c^{upo=|hW0iBgD@J|X_Pyb~TH!=SJRb^qI2g<|cUIYuzK zL&OPG3ym^Ru=_gKZ9oYS+UR=bvmUi^dsVtVnKx8P0vsSIezH=a7x=B))_PGzEQ;W* zZdgY~5r@}uH=TONgu$H@L>5BrZ9IVX(hDs?#^|Za1zJLO-MpPoW!B15rk03&ag_zE zj89P4p`p?G!#zgL!`lO5YzD>E9Bv-1^l@N%#Yf0Om6u^R{Q?61E(h&&e;_|I-^&gp z6K>P^OMA1aQhYAEG0zE*O2T(fM59})Be3fgF5~`avQrOSq3CWu>qTI3Q%62!yhT89 zKYQ)_)ocDjOi&06wYl6?d=NPW4vl$QrRVkK;<8j)HPehM@t?)G68fOzA?jD+NK96B zv6DIAz}r%Vi`Xh$Dy|GZBRl3E{4|M|9nn5tBQfiY0#27zFj-&RB`0&Zh|uMK$vmi& z)l-X35sl6)RKIC9D3+s>i`Q}Yxo{ZM4?td_!5k31m3SBwE6?_%BNKsiJ$5&o$yBh# zI{0al@VTiEPBDg!G%?24K6hwL)qL>>={9G5R83|PW(4=hmqf!QwNkrAULG&9S&)bd*q^}OcU~H4Uzd{G6&=&s(az0hOicRB#Q>-q6$r1Ry4LGC$A4Q^RE1U>+p$>Yk7*Oc zua^thaoS@j94?_(i{C5oK=2iAV)K0qc(ArzTxxQ<5^S2O( zd2FPM&lX8qICP45Z6zNqb~1Qvtd)A|Su|V!z|>|{phWueEy93!x6VXvTrc4B_Ip7QRvf%)*&z@90QEuK zCOkIe9#132)*gm@?_ZwSk;rI%2s;*?ZYuv{%(YW zjz*)?e)aS2mrhTI{NHL##(&4&jkxx(E%&b%eg0u1mFs-R5f!=(jA z{*DxM`xL3U2Ld37emiJy@^wZA|A&Na<@>>02)ASI~C@QDDm z8q#1h-fu@=Q1tCA8PIHYExbH0kg<28dfZ#QEGWJdiiC18j72Jh z`WXKJoi<4PG3euB+e?#>hjY*E?VWo;7xXjnRU1x=BEz;Y_jr}@tcH>s@JWAjvxVn* zv#-^DMVgmX6h#8PI{0M{n&INCU&+h^t*ur~Z>Lz0;8H^Rpu<(9W{j5|KI><9#&;Ib zeAKI{R5XjVw8ATgxu(dLk#aIOLqMCQAr+rV#+2?$yMYX+4hCG|OAg>2uJ&My3j=@G zmRv$dqP`;tGP9|~XT-peV;~w%T92ys66rD3hX+WHCnCRwMgvCz1RJ9EC;(a{1aU?C9n425TZf*_|m4$EpqWFPzb}fX@{FEKH^9gtGENhu}F>(@Eta zS}20vfBwf)&UIQ^{AtOB`xh0wQn#QrL8ph?(aXj8XrsuQjmU?{-a(0q4VK` z6Y^uNs|}4&y#Frs;9;<)!J}Va{!``wQpXc2o%IZL38~*J`Bm{_Ef?&+sQ+H(@ukcM zlO!i)6l{rKU;b8_N~f3Sw`ToaaG4a*hzN1=qJ<+O>WeT!}L6huY9C_W*9j-{fOjNe{$|DYY`BKh%V z6OQ)`&wo}00S1}pbl$7=3^0qq2-2@k?xS(a_xX@%s z_#D4VHWuIogBMyv?O{NnDCwt$hE3`ImR5WzBU!TwV0f!$Yc$26E2MaxMJs|Lup*hw zyni`hd=zW7p{R9p$`R&2=|7a#CO+jfO)Fse#u`GeJ92d}+EL}jV>K(dI~w*A4TpE1 zCB9cdt3No0NWoGj)O~lh?5$K)VNJ~&%X@z4%u==pN`8=x2+?o`4_mWXR_#}V9{r)l zN@zkWz^Hzt!7IjBvUNhr)bunWjl27ai z9UR%1`~(s$IWJ!9L)5D~cHVlWWsNQm7zV;q5)+X(*T+6|Pj-U7p|ehV>{p<=9NgT{ z)H(RZWUY5ly#GoiaIvRes8$eq(Ef&@d_1|AzB5szY1AYMbv*n1HQiOfMyy0zz)4o@ z+GVY*Ko^fvb`?DVN=3HchDF*PyfSzHY?z6Ge0SnWYi>)64#gdqE5GaWX#S5S4$vi$ zgal#R2y{s?Wz*OP(&1rj{QgSmYS@Bk;Oj&GOz%m7W=D(c2*A*tk?)EBSZ-W%snUgG z)_UuGo5A?_0K5NgW_tRL0p^9sp*7G>y18vw{-OvBJYqw$$C>tY8 zYaLJv>@fCS^-nupPt3i#-Y9}D#=Vp!XSb61@*18|w)De`%QEAuZn^MnY#VJamNYgS zEby{S5$;*crFfS6Q%aKPgyLUpEyxx#u@ke(Q?s$&w5`cu?EysLwQs?6J`4frw;lJi zdj9@vVCHfNDjE0VFOw~C^|QrF`fIfe_u7sL3vCzSj%JTE5o;fmOEz+q?T-hD+|IamIXV+h)L z5?n;2VA1;?U-!~27y7w5z9ief7hA)^yijOyR!S-A9IN$aG0Z$=8)c1+gugC`89QK0 zpzdaTIq!ONZO@&zYy^0ApimNcYtzwgf@H?4TC}MM&N|Q^9JgRu7o)FvR&Bj};&lIh z2&gsT_60rPMwy$fbzv}@s%#;kNKWG()|c(^NRv&U+so(O%zBQ}wVo)2*pCjji*gm; z2?}+e=rc}GrTtacY3WUyO%!VR#H=rTTact?au=#^gp01R4XmAN&CWgnGx)DA(C(c+ z38UN_(vAhv;}Pkdw)^wZ;L(a4eh8{|*z#$Y{g*h(6-2UFP$;kWAh(Cu-5(T}sCr%b ztw4{tVs_t@g*oPaWWc%1t|$KTXB1Xb`28hxrxxVf=Ur;iT{RD^m~ zxq(;FP&hr~rO8Ew3dw}{&`+AsxnXLbDqH<-&2ouyu;Q{rl;{|21rzM)i?Ln9w@>_x zgd3wu_R*7BuFX9z6;Iz0>*e(W-~08->qRvDwQD;+41Ub@i<@V-kM@vhnd zdXQSvbBE_Ysh@$ETEg#`E5dcynWM6NP`3E z<51gCg%BB*A@vjPH{1JaDR!Zucl(q<;-#o?N8r(gWxt_Y3Gc$e43SY1vDi>t^AaV; z{#er z!9Tu@Q)v3I*}-1&jOIH4;nirFc6z=_(G)!0X~tt>f`#g_vVa$cOXq&|yLMnfP(TBj z%ljD*la!Olf8@}gPvT0*@pg4C?e@NR0wus8W`}wlyVk>YuK5iB*Hyaxn2~5yGOO33 zkw>qzL=bb9z;$WQJcWU=V)jm@c2XdMoki|s-7VmbP-B}LxOeF~=E_^y#?~7f`#DNy zw4Qj(xd{g`8|DH_V)*x{tjMu_yranVihE|;5&z1DkXmJX=GR{%#C>q+Xw@r`>IN(x z@96fPAKcoL(SeuI^L{^}`%YVdy|;v<3Ug#{J&;^0-|uYWbcgk)d5$slUEJFxXL z@AoD$kG02qszoR6Z%S~06LkSSGMk!+nr8hk=2tSqDgwRA5cOF2V9AmZZm|^sf4D!k zv5-D#A@}SL8ey(iQsEiyTW4ER%sObFq=+O~4?n2}bN{V5{x#5=Pu_%^B++BzvXMNi z5h!W0UpQc&qTSOwMK#8+31dOSrQ8`>Y_V}`=}j7`kk@A}e-jpML+tS_FFre4%aLbT z9g*ERSzkx6zzaa3{D8sT8_J%Qr8`^SM@UWxq+x)13)Q)Yry5!sf%EGQS;ILshW4v*NEYcueEd7Fqm^+ZO+oja`|7rZ zELhgGS$0jaG<3|=o)-}3l@(cBF`n$~u_fxT{Ki*OQ)9N#fel#HmFT={&WDSojcJ&i zM*LiGBMXLoe0z2m=u7k|Ybx88w`4l4Bzu{b5}Mn`0_fbO6@`rhLw)bpWnvr*H`CRU@??zFx6 zvXCszgGMtOWe#k-ZFRULegoeRj6jFk$X7#<$5ILwT_XO7AjC2*^P_PKM99346~F1$ zd0e{_;!av_*X#X=vqoYJ{vlxv@u&@Cbu_lH=*OoWu8W7b|6W#VPZ7&XQ?s(Yr=1(G z5m3kFrJWS4c~kCWJO~oWCgEeJ)Ty1bvP{Nxowm^QUudbVMAN0E2LgjWZnx%i(dWT= zI&T{B3&$B=*yc+4hHit~=sBid7RU~4mrzhPE^3Mm>n;n|a-KftnJ#=N>>$+;i5l9_ zP)c=JGMJ7?PR^k`wrG&o#oWe90&waSG7`<^EHltdH|UVlGGnmP1b#{y8qHQGF_|FK zoNZGElmSZoHtq9%%;}ql;l9^;OBeOeA>%(!(9}_Lyp`YCIRzhmnVfH) zDe9S@Cw`ed+Ww{~1&lDRXTGrh=~*V#nXf6BxbbHE`k>`()?XB^^oV9LUuuE73v}*0 zj{GxyR}MeRZrQebGlA|0a=fzGIzNnlGJT3d6Zd|T*h-&=#BJi|H~@B69Hy#YD^kpU zvmZTKE;KKbUSpi|w8hiE_hAio_Pp_|I+K>Pukeg`5{}d4&#-hAc{g1fa3{ha(hmv@ zY>;mgdw+??pi@LToO+e8ZOAh!rwo9}rmfvR`*ke%B|G1}5U*i=-6(reJwmqMRd|)$ zF4rZcRaM_C_*vPPUMcMyghl=;*FcDyv9PB(U(-FX>DjYqf;~NZH|6cTcC)W>1f9cA z+(vX)hZ4{zcEF6l6SDY?hQa@hm2)Az*m7xT^zr5|9ii z_OlBvlp84~c`Q7N^|SEbhgCexpfw#0Jr<92hGd&DQybneW$yF2MmyOOlB$!{qxCS2 z?d}vK|Gv|=-mgV-EHnKjJz+OP#I`;tgKl==KVewEeUmDuNI4Blwr#zq-ilfeG?inB z<~1^}z9@;<81cJe6~79bF{*(lMJkfMupac|5#OBfbNcakGuM~?hhtX9y=!I4vFSid zetpA4{8+JBYzmRnEy(diJ%^CWta_Nxfsc4VdK=ZwuwW0ZJTzY-DvZ+q`_a@z`XM|_ z*KIAr$|(8FvVYF}WTNx5K|KEaw1L~|P0_iv&)yHB%-om-%kM*!7Aq<0r&Wl?&v`fA zYh28qZ!bW!UT|c5c&DsP<&}&I5{2KgcyGd0=GVtkM@!guRK?jiI*yz0YsT$rT3V)& zpAhwnQ3Q0O7uu>6uEvlPL>p@9gwa<*^F@%(bk5qX)ovKF1LOj#X=gg>-=W-}!j|U9 z&&xKo2cHx@WQEatdtV*j>^dgs!&;29eSh-6sEeQc%gRz3FMg<#$Lyfj%;E)`n%T}O z2nxRty`8uswsx!pv>vaJ@a@gJDf9d&_2QSvi4{#_yiSOVJ3xKljz%&uezucuoxhuL z=bSVjaPepag2i*6a5p{PyS89E%dOC`so4;T!zX<6IbnU0Z_&-bvE_<_AzV#E<5@$L z0=r<}{tRS5SWWqaLMh`egw?oK&+%mtR)*7{V z#}Ns0j?TkIVlUot%_k?6t1JH&6}x47wI9a4MWB?{T|u2BNrST{4DMSi!{I4 zWByPT@Z!Yh&nx9$N%bCFLK^c`=;|%xYyzlmI$8mG7fC6Gj5M-XUP!olvHNBD5Gk|^ zzMGDb{e{94+cs;Hjm5S*#!k>}iffBa2|bMBU3R|wx6>s7P-XK8*D_z@0&6?JsajIU|*E5JbIjcFxFZ zt;&q}$dU_m8|I+@n|GEoe=$Fv*{j--3D7rwB&Xl(kg_qH9(gVpY01RO8jnbc)E`3* zj8B?;qt|Hvgx6Sw`+_*S_3pYkP)Ke%44a%W#n9D=yWD+uRKfCOqbkPR#|II0oVJh3 zJf9>|`9YQG&-TmCm!)EbH0Q5M#Q3v+?m*x+ z;Og&15*5z7oPU}lsa^%~?0WLEG}&Yk%%E2OX`=dLHN0l5F*U2Mw+!=CqM6S>*N&Pq z{b0mQo)1N!h`=gKm3Gx98=&!bmqpf88~a|Op25^sIo_^7OPlIq0v0I$$<4*SrlYr# zl{J`QzpPL!E;%vrnGK}1=mdY8x7El4T!mx2YwzPbX=!8cqr0tQNW>FmDR}{1y8bFa zi_M8ZUoG$Mf$Rp0Ykm@D{nrK#SIsp-uAmm4@~NM_RH6Uz5zHXeSMNQjC9ps%1{EKT z$uvl_aOF|D+*7zfk4UG=Ku|&9?wdZ}7c&2jD<%kPaKFJE5doD?)`3A*HweQT{f&R~uZdl~+NEPvMU8Rf0wNO%0o(*(*oopH#O2ix~~ zjoAMjdb%HP!3g!8s_K^TQJq~%%tl0v)cRM`5cn9UUJ!yJZMe9MUgTPaiKeM(1iBVozl2-r zU!nug?ICIQZ1w}BE~GiHwG6a00)b9rZriP;&hRmS@MU}NdYcCR zlPcCD4+Dy4(K3JP98^Iluav&X=b*LB-M-Q_Xop@Hp5b3=xNQBWVc()Z+$raE>Tf*| zha2{Q>1M#u1rEQ&oj5f{VKKcBsQfhLhl9}Z)3^_2{~`Jyh!JxO_UyV%I%J6ZB#bjK zEaY~fl`-dexzsO3XGJsW^e4%^l9zPSytE9+9Ud_83V`|m{CfC zL4Gs(PcO~1wN@2v;*->xf@4b%rvA&dQSQH6gmUaflyp}|{C?iuRX*Olutd&BFA~2c zGOY89EB#+Wj;aZ%x0mj%jt(^n{hP*;Tvd5RNe?!;A3=+=X~ah-eeA)SwIHoh@?R&N z@W>B$NUaO*t4p9ZPUbT;^uJX5iNGMm1VxZ;g5nZ`2CfJ`a)qSy`~R$0#Oz<{n5YEZ zH8#A#7*dHtH%UsZhBx_k#%3r%{?eqWz9Xfxi~H)Aq%**aN{R$k2uH#p6AzIq-ucz4 ziu_f_--GnW7FVDJ7!)p=h`142V|1Sj*tf2Xw(yw9qt$fgPOuWj- zmz{7n_%|i zFG_z`_n5X6!9S2ij>vfbd$ljiB3zaM!YmEw|4s9aD&ifw&x(NkfA{cfEE00X?Rzb2 z{Qsu;B^UAT>FGDO>Hi+t50oefzJ63WO8?(9sU#5Z=(Zv@YyN#?LG)7$1F!OURX0!R zziEmGBi_l6;)6~97e*KG4B`FltC!69jiLTEY(dBbR60qy4B0(@3Dn<*;FeLm=Km(} zzX$vOCh-4r2a1c}UE1IA0#8ELUs>%}GgA)Fdw9F1%y@RUJJfkOCGo`$Vjp8FtOY`N z`$KZ7M!)Zp!-zjvj^B8)EuWOrUiP(Z zm7;EqVUyz=O|LZFc(sER@kC<;*(i^KGTF4`+xUcm$-rTjDD&R$epYt^VNFT(p^fa# zF_e~D!!uX0!!8-F5IIAO_s#SNH#&+R4^9raE)qKSNxr|+_As*As+|gT%A#~nBsmP^ zfU{Qc!aQw-wm)dO9))|3oHDpI53qsR+%I1JJU5MBU>x>0p82+s_37352y8*~&Q;Ax zpxDXn!rwouWxcBC7@>yS{BVqWf$vDBS0ojk0;^{x*VrEDo99z+a-3Oh7drUD>k2Jf zs8CmubSeny@amY`dhYI+#B_h#dxyT?)%DN5zSO6hTtdJn?VbYmIh1t_9ea7~PF@F! zrq>x|r#HxC-+36@%dbs|(R#*}f^s#1k$#*7E#`nq1H&mHX{QZ4y@t(! zHZf#J1Cqj$ejZsbjPh6RIddnr9pGH@%Ppm?TA3cGY#0%wc*}~BkHOi2b!T*X0eb6U z0j(G@Xc0BW*?w|)yMDl)9LBI?#f!&qf7XvPK5#N)P)mn*TbBf{dByM|SQAr3WR#mz zU8h}p{4J8ixxc^BB(qTmc+WffR8uTxZkY0g{gutSvXJg7w6DW}M&Apiddcj%>5J(` zEa_R}kbeCjJkVVB*&S{fK$(o_Z%+F`40*-p`7%ri5aS$Ft~%RD&&hRgM`=EizO}IS z@qlJ=F|gL80uhNSxVgCw9zeTU*?(^h=tLe7YpH6mfqb=@P=&DUe4(%qd8lpwgAcl( zcX2(JN=o)vBv&CBYnO6MZgSxC0kE{8Ik_{2^STOn-ZC|ol z)k!(%9fv?i)M~ycG;P~DsYJ|Z+bC=a&>x}II1%!@7~**rK9{Wt08cZjTRNZ%`hmL0 z7dZ4=&8-HWLezJNAq4|C& zA?9fcvvnec7nEg@;O>uU&@YM3&*&nqNDeka9_JjaAII&l*Y(VqtwK(u>VISR;Z5QO zs1@!y{Yy&*B<4pYZoUI8U2F^9>5%QrM|1r};GvWm`jjMuFYm;>7X95mlzDWu$8d%# zPwFfEeY@dCnyI45kPpqd9eq|km#|%8&cqABCH93Ec0;O3>};>rN8#yOt+@I%ZIXAJ|9F*py^NEz*e`gzb8C*7~O3pM>+28 zc6fERNq%tq6kqkdw-1%}2p%MKk$y>AS~}!|F-cA|y^2nbXY^XX(QuIs-Tvff>jWZ) z6ri2gKysG!t6Vdhv5ey>-ED&X{P|mIE;9L$geEdOqe^ib&R0B^p7nK0>d zyv6Ot9t_znIVI-st<6kkLSqBwS;eXczTgaVnVW+xpU!U3oa1%^TAgdKFO2NcY<{@z ze9`iZJGnkvA+H72Z}n}63qGnZR4@zp3hL)&pMZ5@$k*@Zh{Ly3l{XizE`hEE?!RBE7&l4HrrDiRoe1m`#XG6sUpUR^Qe7`t4GviV zk6lBs<84EYcFsuj%8x~04lonQl#v6;fY+ewnz~u3l#mnIT(4iC17F1HqaIRyUgeaR zrpI+`ocC66lX-iAUIQ!ap)<>gnRwft}lwYNPTY zf=&Ld-VoHXnFc0)jtPIu_md$+XK{~u*7n9|ca}b$ zzVjR1gC!g9m9*jL7qibiaUVg*+i~A?m7BSsqJT(XtwM& zx8`V9wU;khIfh8mbHBWxfpK_=IW6`IvceABE{L!hXIDfnu0={Errn0U!Y+jLq5Gp9 zdBOp5ls5Ph%hv%X3-%|N6e8`7q?O0%XZtf6+#kHbgHaCZnNZo`PFaf4w+BM?C3Ax? z7>on+dSrsYD*Zw7owb0!wcUlAMi$K5$rmip8ZvLbGf5Vycj_~0GC2sDYP*44(cHV% zps%5LmEA*Gtzw6qtO!#ttG7dxJV$eUWKfMI_q}IM`~GK2w-Dl!*e zpMyGV4`p&jHZmO=$d%t^;qIy<&B@}JZ5z~|vR|U_e#*|kEHVYV^GdcWh@;&ZJI)=$r^sr$FsrXvK*OQjTle~Hf zDm({%1m5Rm4jWRk;*yru%_-lwR2ZDN>f(JHx#%#uUjeTpX|@`GA+VfGhS`t-{Zl<` zY>k<9l4sMJXc6hV-CY#*{^St8Ib}hBaSH0Q(`cic#a$y`*bt}4xc!x*cTASMQS3fZ z2a)-xyr!S}B&)IL>PpaCABFp2*|itE5aoYp0EL~&~^tvcE1Z<<%htD;>e?Mn;4 zi40H%(C<$cNISg^yjTKCNTBT(9iFG`?pfyVckU!QDpL(RmS7U4QfY4~Rkf*Yf22<} zZq47LKT4#T{Q)?S`+UvGWxsiVd66Q8nzUfQYrN-?eV554e2F@6pziMul0s{CZkn{w zKjpu_S=YOnLVurW<(SC*rAXRW`*sR>i~>}bb5ag#+~baD1am#CDkmHkE0*@f&3vz; zFYtZ-TW>jktnWo8AgCB^W+`A&mZGI07HdnB@Y3&mRWo0z?OfBHaf~uYf4RCihKOQL zty%!oc6xYf`!-{9IDyr{drGlW2)Ng3GbL;o;Qx+jb5;#apS#BW{&LxFk{trwpF`R_ zI?Q8R{}3)dhm!$AH9GqCV454z z(?)s(V__MG)sMn=e6ihyO@ot~tU1bAln{;8Se)bjZDE`|``0yzz#BN0Yv~4&5cXlyi5}tiA zW!DKH;gwHX#j!C9m=%nYO?xqM-ll5h$7xsZ#pO0^kGtdT?P=qzaR1;K(p^WT)rS+k zw&8Z}){)SjV*|&z8p0C#-QSHDi{v7J<(%<@++4vfp@dd%J#X+v@G7yPL+HCl0?nQ9 z!v2fe0JDxZQgR=wu&6CX@{OY~Gr_jO*qsEnzIixkIwMhANwjn+7H2-Ua}QW%mzhLQ zInOyUOyGVLe(pL!4p}gI>@+e>E2zO{@W=7(xN2L@EDGmg+u6tJbB9lWS%~6^uFXMN zS^Z!WfUz&B>dhm9gahGzhh64`oZX4ek%p*sD@a8bAvtvspL~<>?QX|`%hr*#W3_@J zr$Q8}3O7U4u!LE2*`B7;p;5af0$#q0xq>0yV)3M?=Omjqh*{r#3vxyOyo_&NFPjUb z%8U^@>tJ;6C^d?`bze=)dXpl`0~2=R@@d-sRA=s8U4Q-F?G7Nu>ud$VD5$AoXx0UUN2o79(D$Dd$+njE^_p}yc-@|e^y>P(&}7V$`+QE zY3<1ftL(J^6}1PkwCz6|&d~NzF7^r54N*EkXIvA)Q7gD!@}ks4h!@ zK>`q`GEp!+;{z$z^FT0&!ym-9EX-3Mb7B(KT< z2Sb#7W?rd7`yYq28I339h@%lI_fa$3b;_Aq$Y4DEZuAJnNr6QZaq&7^y6kJ;7t=XU z-TGXt@9n(^KxL5phyoBWm|d(Xf%JB4-N)?u1A3@+3@coqd%i!E1*~lJ|Frj=QB5^b zyNHM&0xC_aC`~{>=^Yf2UIap~(rb_oAs`k|lp>K9I?`L{2%)J+@5Rs&q(g`hAky#Q zEsDOb`{Vw)-&!BP!b;A}?3vkRo_Y2@+!$ZK5xa=Qz@#zm@x1#9BI67Zl|3=)?6h7T zF|^x-1$=7kg#xuwAIiO-us^62-W{EoeX+59Ey3Zj5s1?^Vp)?rD6bdm*`4P6I9x7b z&I2aX5@Vfwx7EvZafMu^!)2MA|I|Ie53Ec;wgc97WcG@^x>tL>jD7FW0GvE-QQ`!t zx+o7j-Nj;6qoem(G;rVuSbMt0O03Rw)eVgJw3FX~A34h11n~Q6vg0K!Dh!s})6y*& zzV5toboJ6+ifm1h4B{ShzZX)wgbN|#E~|c7ZVL6Yzo87O@8KG4nEz5Uzo@=Z-1cO< z`-s%zGzdPp$1Sk>Ff3a^r-(z8(Qs-cOXM}sY3YDmR4hU`2xAwGB|EoiSh|&T$t|pC zd$YY_MQ77+0<+V*E2vLH2Z)lWJ!+&pwQ_U|{?r7};S}NQ8y6(~?6^^yPn<-qOo4!R zLgyWsO!n+ZIz~Dc(gx&_rx(8$DYQKZUmS6O{~`DCtdfO1W4scVil-bK|T2j<;7y~8C(@Xq9B_cP*pkW7kG= znDxi9;AbOiU;2}oCJ&n3YIqsAB$&{j(xrx+T)%J&I63AJAC53Zi_aED)AEX1t_f$u z!CeDO{T`<^#Kb!n+RNUJ!azIHV<>e&J8YiP?7`cmXW6KyQj<_;+t?KtOP&1b3A(0% zFR!Q}UB0N??s7sMPoJ!+rvWY0z709G+`;ihYdC*9(1Z%)?XzwaPFUZ`JrQ~nQijze zgDz)n^=;MO=7A#Spk+nd0g1N3kH;)lgV^?8{zMsM_eq+pixVBY8!zGai4uDtSqw>PD!K*c5J>S)Tt$0ve}1*nE-#APa?+!#9$=f)P1IqG9=sn@-vxM{}1&jPAuJ>p6E; zm%h*A>-^TBs==%cVYf-RYF~04nD%_9BUEjvVm+KTQ6;x@>m!e2By|#|+SL=Xkoj!M z+NjU_oi`HTW$sB`F*LJ25?QKgiJy!ILoZ&Q-$;pKK9C^51sK>y#f({}jjgWK&EG7M5)};m z%su7&#+|HS!oq;6q9nnWwGN#-Fgb2zVzDu)qq|!U))|>`;NiZ0orG@q+~)-yXGPV; zoto(arhd`4GeE}EK%E+PT1Q9l`5gLw`bV#8oM|61F>rs&G%3ZHV1mzt!N!iSS6h(k z6cma>}OLUlZW9lt%)+&3RYDy*OBo9~A0C_=vNqBt!i zgF48JRu$3+a{5voKo-$=p%Gdk)<5U2+H(h-s*DKv*m<*G4k8>~kEZVkk#UKMl{P`^ z#2TxdnpGTQ3fs!tlQJ8Era;ZS}F@`J?ZQfaYI6*O|tV4XW^5S5z#Ig?@K>+QEZpqNoIOCu$k4L=1VFWh@R@1 zb=V-I_v_i;GR8*ubU^2oT&CU-&IHFN+)=y4VA#zx@!r>AY6;in0cnu@K7UDUm{n@c zA^7!>C#^$p)rfiYwjW8Xhrf~OYr$JKd&{1h(U$g$VrRt*?typx{QL;iu;(>iWtdKU zhSVDhsmI=*w|_L5{_zk1zOSvZp(YZ+^yA-~B&+#6TBm#j3KshM@g5Nhn;xhIF7@%h zV>F9nl4Q9P92#IBoa)>0rTaZ9+S?#wAW7{)A3Ma%&dqs=hzqRx03N@MNK>0^iPAKFtVLv2H5 zC;Bm@M&fZY=W8Kb%zK}GCk%VKReImA1CZsv*Z&p-k12AR+AJIq_}B8;i(}Bpkqoycc>e{PMv{Mb-Fb^8*3O5~psPGK?i=$CRvL&WJW( zK&N^Oo!NbdF{;0B;N>C|Yb6V%OW@fh!l5{dqAON55>tBd;!F@JxpLTg<3Jm7i4I~z zll8&c=h%h}zbIq-0%pUA2oxbHZgytWJXwEB2J0aX?o&WSRDYEaTg3eA&FL>z+L04U$Y6TJ|bP-EdlWZ&Pc@yvb2@C;`nX;amzyc z;#juC!cNm!+mL*AyGcz7&2sBHv8f)Pgo(56>M-RaQO&~(2$wXriP`2Fop=rx{_{aW3@V5)@n;3ymT)qFbRO8Jm< zH9?L1SGcIz7xe)vO0N(tARsnaj!SgYG(hNMv`s5)9Tu-xRQg=%_ueA->e(6T9ZuH@u<6#%nkChV|CBMf*MZ_7|M& zCu-8hUA1Uv?fpWBGR_OUSlS|fR;kx5GiAtyiO<#{?cJAVZ!I-{L93-}7^=owa=$Je zdsh%`FwcYSr07r=xZN3AbQV)XrDg{4r>Wju_w9aA_c^?753=wOo6Bz2=K*`t3OOL1 zYS?J{ZrH{)PaKFz_>x41>)UuvoK?SrzGpuv3QC+*nTHo!j~AC>1HZm7nH4#Tccf|K zJGy05t|xEkPJ>TYlfmU>*>u}@WK^yh7F7Tn7YPyIq5ZV+p zp{8*Q?7RPjYNIIEyEB#At-^QL*g6vkKpZ1@C7S%}WkiTu>#WOnc#ExH#RnYaZ*3Q1 zN2erv-&1f98&a>S-H<^{Z)an7;|bQck@5(XnY5TjxL{CK5`rY1#|K=NSJjApa`u_gXN+oltKKUjS+PS~ zxi76BEtOOc?^`tP$zCv#k?skV(lY&dJAKLEI51oy9++(g$4!$n%K3>c@sr{M_&TxQ zW_y5Je`?I+h!DKqOO-?Hu(#5Mk*>U#{#kBWRSl3IBig+GX`iSEMko}CMMYXIir=^) zsOdSxIdP4yAJ-_h_fk4*GKRd~6eFoSS&vxUV#cxxfFmvl4J1pO!SPhpUQW(Rfj1u8 zBk+s_L)wur)Qiaremy;13#&AO<*h|#NqT4lp;1yGM=9e(6w zB68ev2>LD~ZC*|54F^VwERKi?J&-A~s`E`le1aFaZ`z5wb6|S#c1Vr=#zJ1qd8{wP z?2>QBm?L>@k7}>C8L+6^L+s$2qeT+!vqNR_5ITp(L8I_(g5u-&D|~LDA|0A+StN6? zRxU4t9M#_1(a@EcdN_$8N2=G>CC}}Up(Sv+ZSuX{_aa`a-msDd%Kh-5z#hsS| zgIQ+ozFRf#(?zP&d`yhn zFO7v|H+7dGoI5SMF-IV9b*noC6&%$j!J6~%Ua6$deOKA(;00<$i-&W8+9-}|(v}-n zpG`9J+(1u&Mx>pMxFJK2np7HSnL|kJ@2}l6sakOfjtymQ7uHSIz7NtZ zErl)NS=jS|S{}QmcZ_59`)wMe-j2@fVg&Ud&0@KBJ9`*ADSn31be&AFwXf4yz ztDh9ODUBaGiD5ShG)vJlX+f$1_bugQ;6A+efqC&Q!X(dXLQaj#eY!j*+H#u1E0rEIw^8|C=UzVn5bkd%yd3^avEI+5hSK> zMI|Gul28JR5tO6)2W0QoSDD;2Q%Ys+>Fd*i3A@qc(qPRCK^f%DV5v>U9%Rf>u&WY%O{lzu#m0Z z)e4kfvE$9YN1JN_{?0XOV}=#F5^uP4YsuHe(q&O7R?AamZS8@9y^~jb8t6dPl#5jP zuGXsOTa+mWOeEJCwy7wb-zN@N40z#t$pS@9+Nph!#c`qDOW%rN>wATVAj-5Ro1>?` za%j$T`hpVjLA81TVERT~lFGU$c#u0^u98iU>cf0@8@?D~4>D2|4#>{O@*P7VLiR@T zV-AqdahP|W^VuP7CZG|RB6r@CofDzucTlS7nf(1wRpg0_(;xJrYZTAbanD<~KN+5Q z`!CmtXfO*PAB_&$nEmO=Xn?qr_a@{I3@kkS3QnL=;fj%;*WM=% zdzk>kh-=Q`>IrZCZ~L;&$Bn8vZ~vfj{~Rc@W-c%qWnca0Kim58NizXx)ImD|`UlDu z(f@BEW_+IScCa-2^a;Wi=$%GMNv(AM{);znRtv)>O3D9?LyswB&rb)<&ax`0sJ-6Y zq-^U{guQu_A!_IOK=?j*&TE!NOa|Fim*gJUf;M?zdY-}aF8=&VmwyABO7M>TDD%)x zpYOVU)HeK51Sz>Qc1=pY6si!g==58{MD#g9$I(WDqUDLHb#5se8NM6B_wz`r&y=!N zX)b^2%1bG+H>#uV6$QO45VVIy^Kw3YwiLWUil6DXE; zV!eP3#g&R9f0h-iZedY^Ez3@A4eHwqju{Di7nt+4gVo!+q+mFTZ!|yjF&=*3#ol7Y z1!~d^B^zZAu7YQVGvfLH2Ux$-xyL|Dks{pRc?v;7$*ar!V+I>AL z!X;f&WVMid2_IB)FDrmR`HXHt>bGvGPxZLtJDH-|J7;wP%qfuTB$jnSHZ==h+)xg4 zt9&+k(tWU8y>hCm(Pkh&3=p$|C3WeGFy?%dj)QH)(3lEv+Vol*{mM`{pOgOUs`Opx zWztnE!^d6TPG+Io(o*<@zdPHI2!H~6p{NG{jry*(tc<(7bYST1@iY0Mz{^R>Gh(H; z_~0C`YED`h=8`l01E)2p_0B*ruZORdNbRBt@*a?=qKR~vi$5-;)V(9ookNp(=&7iL zA+z*cU)=nK{J76*oP28eJOn|xkd&2fTPtVG-{CY2!n9pQi1;S_;COXPAw z%=dR!cXp6-)eB}((cF3xg_2)iJ>>UjWxi4smvzD*9D7Grcw?Za$^5n9e|p1f?X2)PCmazs@-GWmSbd<-fgr1))20T;geHS=>0YwMu1 zR>f*kMk;|!$OLzA zUvi~$ew%6X<+pBj%Wj{R6YucMCVgqMHAuw)i2fllCxwRRwIiZVN!Vp{l~-yr@`+XZ zAb0}zPvNIy!TvT6UpRv3`9yU>RkmXs`$ML9qet9Ud|Duo_M+44XB!Ifp^P%f9=D$ zx?(RH>W+d;dh4Yc#!~+SvbHYn$e1oV#cW+Qt5>v>G&Kh=Ve=i`6VLb^ZyVGv?x+a2 z1FPM4)o!97Ezz$vz_aary(7Tz23=mQheFyUzzsLhV-=)j<@*JlovO4)ZP}&HV_vq? z4=r01zcz=pBkbl?6E~x4_uE&xOSuv61Xsm7f6ov}Eg}dFRhgYcWp!v^KNAXB6#0rw zugG`T!ortX7GACL#%8OV`{xj&b9;2 zBQdudBF#gSbIxRa#M`#L{F!xK&YjqhdJQ4m#G9szskWdu-CM&YZ|6o94i^T6Ej}!` zjSs)=7^<~Fi^mIi*9#myN_&z|d4XGxR^WH_d|uk9V%F)M(wq&7kbH-HwTil5Q|WTe zu?ih!D-+M666@@?E$%z2MfR0D zN32F_vML?i^KF4=apy8xs&Oa<`0Tf98qf_`=l;X!GJnkmGPjZSCy7hJv`_gMo)heZ zB!ljY_e~%@qfP7l#4Dfz)^G{dUZaByCD=q2N0LXPL>IxY^Mr@*$x|F)lw51;r#j*` zlOw$`MnTx|sECIx-=b!7xz~8De(~ngWMQ<4e6xnxy!uL0Ri*~x;L_LdRW?gP@7FQ& zoKHymwdW!y(X*pv!7rs>7wh#1^V>BDYw0lf_@&Jk_iynJwEJmVXJm_FJ*3}rQK==~ zDdq}Y*qw!`yE^Wm(~x%0_I$8ZI#=pbk_Dp>TYp;8pL3tZ79RNfdVYJ-m@X6)5^Qe- z7q({>h;bs?D}yd1r_Q^KRgXcZIwW~|V>}(bTQhOc>plMB3IT$0XR#z7&Q`4M)?PC| zb9vLAE&UrjX~Os(oJkv|hRCB8ux)DwxMZIBjb}Y)no6%evg{hb=lo^!0(<$pl6gmX zY4&=dX`R|yVCDYvG*|SHIVglWEJ@xrVwG*c1`$%tWA2`(4V^XKJ#%0 zxRj8Rx0XAA3$;g{Eu!Q3Na%SEIkG`zH2aC%rfuseiIu)G_Dsu%qR!5zRLAPVA zp1LlI!)}J^z`H`*YjKR_rj<%D0SB~a+tp!-&`R3FD%UOD_=X2%1;*7wCK@>RF#*_I z1L~FM!mXf3Kt_c%Tlyya^=n=Z1{qz`z^~5nB7tcIh86du_x4Guwk;!Q|6n8@nGqNY zJ6@NG6=vrVGT4P|Xn?ZA!ss*Fp4X$3u1o}=V@x(SpQg-a`#L5qSAx4>bM)>Rjw|d# zxyjDUC3q(O4_w_^!lgzFw+4FH+xD*buhwG5vSU5aqa!BG|53DniSosyd8Hdb_;^cnN=ZqH z-({S5V=*xKd>(Z3z5+qr0_m?SkB1*{u~qPWri_V4-1y;aY`Vs}QI%JS@m>!x|GPj1 z`-BP27(~zOGcpKrhc)}Rd&M9Gm2L09Ul?e$B8|9g$<|8(=#b)$4Kssa-VrD_I5%M6oxJ2S8tc5Qu%0O zfRIbxM;R!kbIn+w7*2MYLD2OPvWu95VdYPLj3^_M(C;>=5)+VQ)QXN}yb6U>kRaiN zumDK+?F}R>SW($ct5+PYN?x#sVBk?;92KyuhYob!Kv@SO{oi=F#9P}U5liePduF_1 zcgPq&579obqp@quOU-GjpQ}hFVT~{H2~xFt=$5)7LU^gR!rA07eS7R`BIJ;*)Ms7G zez-WuUwB$dNsUKl+3oxLw~00yDY~OQec-dVAt%hGqHK^)4d(g60QT#0=~at$e}=7U zbTvEqP*$Qphk>7sUtYP(s4V88M+fJ zie*3HZx-YYXSJ)i?0!W9FF|yuQi#@d-GA7kSXE?_XG}wXGS>Jf^mqZ}ZgDBwO7ZcT zv8%%wbc_IrFyv(7J>4}Uuo)?*AzTpBZwt(Nzdx)}1` zF*M2m`MEMZVQ;Y4?y~_@^=+Hyg&8_ zr0==&+WnjGe4^XPZ-9rsba)Bx-?;chx8EXw^V3lxiQyj(WURG-lusK7@P5FCKOfv@ z0kY}-pTui&F3tn$5(HA(|1#7;`cn|Z>%>GIaCjTy?Bimyp7ZED3~&Yyc!@Lz0WWxR z@1mrpLg5@kzW&ID2Zj?QexVpgmz(?08>w^FP-u*;SqN+CNRY=J37^4eJ{*Vc*<@sk zFQgY2XVfe_MgJ7w_}O!3R_pHh8Gf03`+>?p(=v$A{-L*u;aAHneBewIxr8*L6h1Mnno|V8<2Flm3Q!4Y8Agq-uNqrvWh!1&GDi~O8^j+T0_Z_8 zSE1l;6+|PAk_J}Aby4~*1IvjmSHYHwyX=QZdv?{KM6ahk182AzJ#rdSrDD8Ypf#w=Lnl#*+&IU|;T^F$(Ge-zVCGc*qY5yvPh`$5 z)-{DEPLy)d0!_uEAe|mk7036R?H{eCjybwB2E`cqektm3Vf;cwe9#6xnq z%kstTOoQcY5?1XqXnu%6c)?gA$599Hh_ZM#J6AK>@P5=DF=^e5_&mb zWAPJ)J%I>*Y7W6io>R8UroCO}sPTf5GFG$=v-EK8BZWN7pCwf)M@t*VHj$y=5SQ%a zr5fd3wP3v2rgG5%``*N^KPO!hFN|A2!nzXmLop9jC#ufXVE_KtZH za0Z|DXfqB@ti(TREGu;hfEyyz+5dz`H6ejUePo`LU->V*l%oKc)aAamS9mY0t`)8Kp53{kQ2!I7r;_(b8c?tgv>;eX8bmriE{~sX2R3>8J5=4OmCU+uN z=wEw2V*)ffM?s4p{Sgha0Q0@IXytbO58~Gz*OR~rDDM5d0smK?p#NRO|H~qN bIXYGOGAl5cR+JGB_*0Tszgu+2{PF(*KE1Yx literal 0 HcmV?d00001 diff --git a/docs/reference/ml/images/ml-health-check-config.jpg b/docs/reference/ml/images/ml-health-check-config.jpg deleted file mode 100644 index c235d79984525528844bba88ef2df5d7ffa8f04b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72237 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%0ABZrB;UN;ctQ2 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

(M*s7Nd;Lua0AQqkpp6XtrKt2fy7#OGc>PNf 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 */ -public class Metadata extends AbstractCollection implements Diffable, ToXContentFragment { +public class Metadata extends AbstractCollection implements Diffable, ChunkedToXContent { private static final Logger logger = LogManager.getLogger(Metadata.class); @@ -1318,9 +1322,60 @@ public static Metadata fromXContent(XContentParser parser) throws IOException { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - Builder.toXContent(this, builder, params); - return builder; + public Iterator toXContentChunked(ToXContent.Params p) { + XContentContext context = XContentContext.valueOf(p.param(CONTEXT_MODE_PARAM, CONTEXT_MODE_API)); + final Iterator start = context == XContentContext.API + ? ChunkedToXContentHelper.startObject("metadata") + : Iterators.single((builder, params) -> builder.startObject("meta-data").field("version", version())); + + final Iterator persistentSettings = context != XContentContext.API && persistentSettings().isEmpty() == false + ? Iterators.single((builder, params) -> { + builder.startObject("settings"); + persistentSettings().toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("flat_settings", "true"))); + return builder.endObject(); + }) + : Collections.emptyIterator(); + + final Iterator indices = context == XContentContext.API + ? ChunkedToXContentHelper.wrapWithObject("indices", indices().values().iterator()) + : Collections.emptyIterator(); + + return Iterators.concat(start, Iterators.single((builder, params) -> { + builder.field("cluster_uuid", clusterUUID); + builder.field("cluster_uuid_committed", clusterUUIDCommitted); + builder.startObject("cluster_coordination"); + coordinationMetadata().toXContent(builder, params); + return builder.endObject(); + }), + persistentSettings, + ChunkedToXContentHelper.wrapWithObject( + "templates", + templates().values() + .stream() + .map( + template -> (ToXContent) (builder, params) -> IndexTemplateMetadata.Builder.toXContentWithTypes( + template, + builder, + params + ) + ) + .iterator() + ), + indices, + customs().entrySet() + .stream() + .filter(cursor -> cursor.getValue().context().contains(context)) + .map( + cursor -> (ChunkedToXContent) params -> ChunkedToXContentHelper.wrapWithObject( + cursor.getKey(), + cursor.getValue().toXContentChunked(params) + ) + ) + .flatMap(s -> StreamSupport.stream(Spliterators.spliteratorUnknownSize(s.toXContentChunked(p), Spliterator.ORDERED), false)) + .iterator(), + ChunkedToXContentHelper.wrapWithObject("reserved_state", reservedStateMetadata().values().iterator()), + ChunkedToXContentHelper.endObject() + ); } public Map getMappingsByHash() { @@ -2469,60 +2524,6 @@ static boolean assertDataStreams(Map indices, DataStreamM return true; } - public static void toXContent(Metadata metadata, XContentBuilder builder, ToXContent.Params params) throws IOException { - XContentContext context = XContentContext.valueOf(params.param(CONTEXT_MODE_PARAM, CONTEXT_MODE_API)); - - if (context == XContentContext.API) { - builder.startObject("metadata"); - } else { - builder.startObject("meta-data"); - builder.field("version", metadata.version()); - } - - builder.field("cluster_uuid", metadata.clusterUUID); - builder.field("cluster_uuid_committed", metadata.clusterUUIDCommitted); - - builder.startObject("cluster_coordination"); - metadata.coordinationMetadata().toXContent(builder, params); - builder.endObject(); - - if (context != XContentContext.API && metadata.persistentSettings().isEmpty() == false) { - builder.startObject("settings"); - metadata.persistentSettings().toXContent(builder, new MapParams(Collections.singletonMap("flat_settings", "true"))); - builder.endObject(); - } - - builder.startObject("templates"); - for (IndexTemplateMetadata template : metadata.templates().values()) { - IndexTemplateMetadata.Builder.toXContentWithTypes(template, builder, params); - } - builder.endObject(); - - if (context == XContentContext.API) { - builder.startObject("indices"); - for (IndexMetadata indexMetadata : metadata) { - IndexMetadata.Builder.toXContent(indexMetadata, builder, params); - } - builder.endObject(); - } - - for (Map.Entry cursor : metadata.customs().entrySet()) { - if (cursor.getValue().context().contains(context)) { - builder.startObject(cursor.getKey()); - ChunkedToXContent.wrapAsXContentObject(cursor.getValue()).toXContent(builder, params); - builder.endObject(); - } - } - - builder.startObject("reserved_state"); - for (ReservedStateMetadata ReservedStateMetadata : metadata.reservedStateMetadata().values()) { - ReservedStateMetadata.toXContent(builder, params); - } - builder.endObject(); - - builder.endObject(); - } - public static Metadata fromXContent(XContentParser parser) throws IOException { Builder builder = new Builder(); @@ -2640,7 +2641,7 @@ private void dedupeMapping(IndexMetadata.Builder indexMetadataBuilder) { Map params = Maps.newMapWithExpectedSize(2); params.put("binary", "true"); params.put(Metadata.CONTEXT_MODE_PARAM, Metadata.CONTEXT_MODE_GATEWAY); - FORMAT_PARAMS = new MapParams(params); + FORMAT_PARAMS = new ToXContent.MapParams(params); } /** @@ -2650,7 +2651,7 @@ private void dedupeMapping(IndexMetadata.Builder indexMetadataBuilder) { @Override public void toXContent(XContentBuilder builder, Metadata state) throws IOException { - Builder.toXContent(state, builder, FORMAT_PARAMS); + ChunkedToXContent.wrapAsXContentObject(state).toXContent(builder, FORMAT_PARAMS); } @Override diff --git a/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContentHelper.java b/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContentHelper.java index bfae6c2a082d..282fadac236b 100644 --- a/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContentHelper.java +++ b/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContentHelper.java @@ -66,7 +66,11 @@ public static Iterator array(String name, Iterable Iterator wrapWithObject(String name, Iterator iterator) { + return Iterators.concat(startObject(name), iterator, endObject()); + } + private static Iterator map(String name, Map map, Function, ToXContent> toXContent) { - return Iterators.concat(startObject(name), map.entrySet().stream().map(toXContent).iterator(), endObject()); + return wrapWithObject(name, map.entrySet().stream().map(toXContent).iterator()); } } diff --git a/server/src/main/java/org/elasticsearch/gateway/PersistedClusterStateService.java b/server/src/main/java/org/elasticsearch/gateway/PersistedClusterStateService.java index 752ef4345229..03d931e38b19 100644 --- a/server/src/main/java/org/elasticsearch/gateway/PersistedClusterStateService.java +++ b/server/src/main/java/org/elasticsearch/gateway/PersistedClusterStateService.java @@ -60,6 +60,7 @@ import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.util.Maps; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.core.CheckedFunction; @@ -71,7 +72,6 @@ import org.elasticsearch.env.NodeMetadata; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.ToXContent; -import org.elasticsearch.xcontent.ToXContentFragment; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; import org.elasticsearch.xcontent.XContentParser; @@ -1082,7 +1082,7 @@ private void addIndexMetadataDocuments(IndexMetadata indexMetadata) throws IOExc private void addGlobalMetadataDocuments(Metadata metadata) throws IOException { logger.trace("updating global metadata doc"); - writePages(metadata, (bytesRef, pageIndex, isLastPage) -> { + writePages(ChunkedToXContent.wrapAsXContentObject(metadata), (bytesRef, pageIndex, isLastPage) -> { final Document document = new Document(); document.add(new StringField(TYPE_FIELD_NAME, GLOBAL_TYPE_NAME, Field.Store.NO)); document.add(new StoredField(PAGE_FIELD_NAME, pageIndex)); @@ -1094,7 +1094,7 @@ private void addGlobalMetadataDocuments(Metadata metadata) throws IOException { }); } - private void writePages(ToXContentFragment metadata, PageWriter pageWriter) throws IOException { + private void writePages(ToXContent metadata, PageWriter pageWriter) throws IOException { try ( PageWriterOutputStream paginatedStream = new PageWriterOutputStream(documentBuffer, pageWriter); OutputStream compressedStream = CompressorFactory.COMPRESSOR.threadLocalOutputStream(paginatedStream); diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java index 0dc2422f9c53..d2e20b2182fc 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -67,6 +67,7 @@ import org.elasticsearch.common.util.concurrent.AbstractRunnable; import org.elasticsearch.common.util.concurrent.ConcurrentCollections; import org.elasticsearch.common.util.concurrent.FutureUtils; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.core.Nullable; @@ -272,14 +273,16 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp public static final ChecksumBlobStoreFormat GLOBAL_METADATA_FORMAT = new ChecksumBlobStoreFormat<>( "metadata", METADATA_NAME_FORMAT, - (repoName, parser) -> Metadata.fromXContent(parser) + (repoName, parser) -> Metadata.fromXContent(parser), + ChunkedToXContent::wrapAsXContentObject ); public static final ChecksumBlobStoreFormat INDEX_METADATA_FORMAT = new ChecksumBlobStoreFormat<>( "index-metadata", METADATA_NAME_FORMAT, (repoName, parser) -> IndexMetadata.Builder.legacyFromXContent(parser), - (repoName, parser) -> IndexMetadata.fromXContent(parser) + (repoName, parser) -> IndexMetadata.fromXContent(parser), + Function.identity() ); private static final String SNAPSHOT_CODEC = "snapshot"; @@ -287,19 +290,22 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp public static final ChecksumBlobStoreFormat SNAPSHOT_FORMAT = new ChecksumBlobStoreFormat<>( SNAPSHOT_CODEC, SNAPSHOT_NAME_FORMAT, - SnapshotInfo::fromXContentInternal + SnapshotInfo::fromXContentInternal, + Function.identity() ); public static final ChecksumBlobStoreFormat INDEX_SHARD_SNAPSHOT_FORMAT = new ChecksumBlobStoreFormat<>( SNAPSHOT_CODEC, SNAPSHOT_NAME_FORMAT, - (repoName, parser) -> BlobStoreIndexShardSnapshot.fromXContent(parser) + (repoName, parser) -> BlobStoreIndexShardSnapshot.fromXContent(parser), + Function.identity() ); public static final ChecksumBlobStoreFormat INDEX_SHARD_SNAPSHOTS_FORMAT = new ChecksumBlobStoreFormat<>( "snapshots", SNAPSHOT_INDEX_NAME_FORMAT, - (repoName, parser) -> BlobStoreIndexShardSnapshots.fromXContent(parser) + (repoName, parser) -> BlobStoreIndexShardSnapshots.fromXContent(parser), + Function.identity() ); public static final Setting MAX_SNAPSHOT_BYTES_PER_SEC = Setting.byteSizeSetting( diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/ChecksumBlobStoreFormat.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/ChecksumBlobStoreFormat.java index 53765f452657..7a08079dbe1f 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/ChecksumBlobStoreFormat.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/ChecksumBlobStoreFormat.java @@ -41,12 +41,13 @@ import java.util.Collections; import java.util.Locale; import java.util.Map; +import java.util.function.Function; import java.util.zip.CRC32; /** * Snapshot metadata file format used in v2.0 and above */ -public final class ChecksumBlobStoreFormat { +public final class ChecksumBlobStoreFormat { // Serialization parameters to specify correct context for metadata serialization. // When metadata is serialized certain elements of the metadata shouldn't be included into snapshot @@ -68,31 +69,42 @@ public final class ChecksumBlobStoreFormat { private final CheckedBiFunction fallbackReader; + private final Function writer; + /** * @param codec codec name * @param blobNameFormat format of the blobname in {@link String#format} format * @param reader prototype object that can deserialize T from XContent * @param fallbackReader fallback prototype object that can deserialize T from XContent in case reader fails + * @param writer function that maps the given type to a {@link ToXContent} for serialization */ public ChecksumBlobStoreFormat( String codec, String blobNameFormat, CheckedBiFunction reader, - @Nullable CheckedBiFunction fallbackReader + @Nullable CheckedBiFunction fallbackReader, + Function writer ) { this.reader = reader; this.blobNameFormat = blobNameFormat; this.codec = codec; this.fallbackReader = fallbackReader; + this.writer = writer; } /** * @param codec codec name * @param blobNameFormat format of the blobname in {@link String#format} format * @param reader prototype object that can deserialize T from XContent + * @param writer function that maps the given type to a {@link ToXContent} for serialization */ - public ChecksumBlobStoreFormat(String codec, String blobNameFormat, CheckedBiFunction reader) { - this(codec, blobNameFormat, reader, null); + public ChecksumBlobStoreFormat( + String codec, + String blobNameFormat, + CheckedBiFunction reader, + Function writer + ) { + this(codec, blobNameFormat, reader, null, writer); } /** @@ -371,7 +383,7 @@ public void close() { : new ToXContent.DelegatingMapParams(extraParams, SNAPSHOT_ONLY_FORMAT_PARAMS); builder.startObject(); - obj.toXContent(builder, params); + writer.apply(obj).toXContent(builder, params); builder.endObject(); } CodecUtil.writeFooter(indexOutput); diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataTests.java index 49fb4000763e..107828e06b25 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataTests.java @@ -19,6 +19,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.Streams; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; @@ -39,6 +40,7 @@ import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xcontent.json.JsonXContent; import java.io.IOException; @@ -2271,6 +2273,45 @@ public void testEmptyDiffReturnsSameInstance() throws IOException { assertSame(instance, deserializedDiff.apply(instance)); } + public void testChunkedToXContent() throws IOException { + final int datastreams = randomInt(10); + final Metadata instance = randomMetadata(datastreams); + int chunksSeen = 0; + try (XContentBuilder builder = new XContentBuilder(randomFrom(XContentType.values()), Streams.NULL_OUTPUT_STREAM, Set.of())) { + final var iterator = instance.toXContentChunked(ToXContent.EMPTY_PARAMS); + builder.startObject(); + while (iterator.hasNext()) { + iterator.next().toXContent(builder, ToXContent.EMPTY_PARAMS); + chunksSeen++; + } + builder.endObject(); + } + // 2 chunks at the beginning + // 1 chunk for each index + 2 to wrap the indices field + final int indicesChunks = instance.indices().size() + 2; + // 2 chunks for wrapping reserved state + 1 chunk for each item + final int reservedStateChunks = instance.reservedStateMetadata().size() + 2; + // 2 chunks wrapping templates and one chunk per template + final int templatesChunks = instance.templates().size() + 2; + // 2 chunks to wrap each custom + final int customChunks = 2 * instance.customs().size(); + // 1 chunk per datastream, 4 chunks to wrap ds and ds-aliases, or 0 if there are no datastreams + final int dsChunks = datastreams == 0 ? 0 : (datastreams + 4); + // 2 chunks to wrap index graveyard and one per tombstone + final int graveYardChunks = instance.indexGraveyard().getTombstones().size() + 2; + // 2 chunks to wrap component templates and one per component template + final int componentTemplateChunks = instance.componentTemplates().size() + 2; + // 2 chunks to wrap v2 templates and one per v2 template + final int v2TemplateChunks = instance.templatesV2().size() + 2; + // 1 chunk to close metadata + + assertEquals( + 2 + indicesChunks + reservedStateChunks + templatesChunks + customChunks + dsChunks + graveYardChunks + componentTemplateChunks + + v2TemplateChunks + 1, + chunksSeen + ); + } + public static Metadata randomMetadata() { return randomMetadata(1); } diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java index 9bd25e82cdc5..831ae1195a65 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.TestCustomMetadata; import org.elasticsearch.xcontent.ToXContent; @@ -226,7 +227,7 @@ public void testToXContentGateway_FlatSettingTrue_ReduceMappingFalse() throws IO Metadata metadata = buildMetadata(); XContentBuilder builder = JsonXContent.contentBuilder().prettyPrint(); builder.startObject(); - metadata.toXContent(builder, new ToXContent.MapParams(mapParams)); + ChunkedToXContent.wrapAsXContentObject(metadata).toXContent(builder, new ToXContent.MapParams(mapParams)); builder.endObject(); assertEquals(formatted(""" @@ -314,7 +315,7 @@ public void testToXContentAPI_SameTypeName() throws IOException { .build(); XContentBuilder builder = JsonXContent.contentBuilder().prettyPrint(); builder.startObject(); - metadata.toXContent(builder, new ToXContent.MapParams(mapParams)); + ChunkedToXContent.wrapAsXContentObject(metadata).toXContent(builder, new ToXContent.MapParams(mapParams)); builder.endObject(); assertEquals(formatted(""" @@ -385,7 +386,7 @@ public void testToXContentGateway_FlatSettingFalse_ReduceMappingTrue() throws IO Metadata metadata = buildMetadata(); XContentBuilder builder = JsonXContent.contentBuilder().prettyPrint(); builder.startObject(); - metadata.toXContent(builder, new ToXContent.MapParams(mapParams)); + ChunkedToXContent.wrapAsXContentObject(metadata).toXContent(builder, new ToXContent.MapParams(mapParams)); builder.endObject(); assertEquals(formatted(""" @@ -451,7 +452,7 @@ public void testToXContentAPI_FlatSettingTrue_ReduceMappingFalse() throws IOExce XContentBuilder builder = JsonXContent.contentBuilder().prettyPrint(); builder.startObject(); - metadata.toXContent(builder, new ToXContent.MapParams(mapParams)); + ChunkedToXContent.wrapAsXContentObject(metadata).toXContent(builder, new ToXContent.MapParams(mapParams)); builder.endObject(); assertEquals(formatted(""" @@ -554,7 +555,7 @@ public void testToXContentAPI_FlatSettingFalse_ReduceMappingTrue() throws IOExce XContentBuilder builder = JsonXContent.contentBuilder().prettyPrint(); builder.startObject(); - metadata.toXContent(builder, new ToXContent.MapParams(mapParams)); + ChunkedToXContent.wrapAsXContentObject(metadata).toXContent(builder, new ToXContent.MapParams(mapParams)); builder.endObject(); assertEquals(formatted(""" @@ -689,7 +690,7 @@ public void testToXContentAPIReservedMetadata() throws IOException { XContentBuilder builder = JsonXContent.contentBuilder().prettyPrint(); builder.startObject(); - metadata.toXContent(builder, new ToXContent.MapParams(mapParams)); + ChunkedToXContent.wrapAsXContentObject(metadata).toXContent(builder, new ToXContent.MapParams(mapParams)); builder.endObject(); assertEquals(formatted(""" diff --git a/server/src/test/java/org/elasticsearch/snapshots/BlobStoreFormatTests.java b/server/src/test/java/org/elasticsearch/snapshots/BlobStoreFormatTests.java index 46416922a5df..cc5d10f847f4 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/BlobStoreFormatTests.java +++ b/server/src/test/java/org/elasticsearch/snapshots/BlobStoreFormatTests.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.Map; +import java.util.function.Function; import static org.hamcrest.Matchers.greaterThan; @@ -80,7 +81,8 @@ public void testBlobStoreOperations() throws IOException { ChecksumBlobStoreFormat checksumSMILE = new ChecksumBlobStoreFormat<>( BLOB_CODEC, "%s", - (repoName, parser) -> BlobObj.fromXContent(parser) + (repoName, parser) -> BlobObj.fromXContent(parser), + Function.identity() ); // Write blobs in different formats @@ -105,7 +107,8 @@ public void testCompressionIsApplied() throws IOException { ChecksumBlobStoreFormat checksumFormat = new ChecksumBlobStoreFormat<>( BLOB_CODEC, "%s", - (repo, parser) -> BlobObj.fromXContent(parser) + (repo, parser) -> BlobObj.fromXContent(parser), + Function.identity() ); BlobObj blobObj = new BlobObj(veryRedundantText.toString()); checksumFormat.write(blobObj, blobContainer, "blob-comp", true); @@ -124,7 +127,8 @@ public void testBlobCorruption() throws IOException { ChecksumBlobStoreFormat checksumFormat = new ChecksumBlobStoreFormat<>( BLOB_CODEC, "%s", - (repo, parser) -> BlobObj.fromXContent(parser) + (repo, parser) -> BlobObj.fromXContent(parser), + Function.identity() ); checksumFormat.write(blobObj, blobContainer, "test-path", randomBoolean()); assertEquals(checksumFormat.read("repo", blobContainer, "test-path", xContentRegistry()).getText(), testString); diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java index 68c0274d1dbc..7eb2187b27ed 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java @@ -91,6 +91,7 @@ import org.elasticsearch.common.util.Maps; import org.elasticsearch.common.util.concurrent.EsRejectedExecutionException; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.core.IOUtils; import org.elasticsearch.core.Nullable; @@ -1161,13 +1162,13 @@ protected void ensureClusterStateCanBeReadByNodeTool() throws IOException { XContentBuilder builder = SmileXContent.contentBuilder(); builder.startObject(); - metadataWithoutIndices.toXContent(builder, serializationFormatParams); + ChunkedToXContent.wrapAsXContentObject(metadataWithoutIndices).toXContent(builder, serializationFormatParams); builder.endObject(); final BytesReference originalBytes = BytesReference.bytes(builder); XContentBuilder compareBuilder = SmileXContent.contentBuilder(); compareBuilder.startObject(); - metadataWithoutIndices.toXContent(compareBuilder, compareFormatParams); + ChunkedToXContent.wrapAsXContentObject(metadataWithoutIndices).toXContent(compareBuilder, compareFormatParams); compareBuilder.endObject(); final BytesReference compareOriginalBytes = BytesReference.bytes(compareBuilder); @@ -1183,7 +1184,7 @@ protected void ensureClusterStateCanBeReadByNodeTool() throws IOException { } builder = SmileXContent.contentBuilder(); builder.startObject(); - loadedMetadata.toXContent(builder, compareFormatParams); + ChunkedToXContent.wrapAsXContentObject(loadedMetadata).toXContent(builder, compareFormatParams); builder.endObject(); final BytesReference parsedBytes = BytesReference.bytes(builder); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesMetadataSerializationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesMetadataSerializationTests.java index 49cc784e0d5e..8692a0246329 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesMetadataSerializationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesMetadataSerializationTests.java @@ -79,7 +79,7 @@ public void testLicenseMetadataParsingDoesNotSwallowOtherMetadata() throws Excep XContentBuilder builder = XContentFactory.jsonBuilder(); Params params = new ToXContent.MapParams(Collections.singletonMap(Metadata.CONTEXT_MODE_PARAM, Metadata.CONTEXT_MODE_GATEWAY)); builder.startObject(); - builder = metadataBuilder.build().toXContent(builder, params); + builder = ChunkedToXContent.wrapAsXContentObject(metadataBuilder.build()).toXContent(builder, params); builder.endObject(); // deserialize metadata again Metadata metadata = Metadata.Builder.fromXContent(createParser(builder)); diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/PolicyStepsRegistryTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/PolicyStepsRegistryTests.java index e6a4c73d9b58..3835787c1c41 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/PolicyStepsRegistryTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/PolicyStepsRegistryTests.java @@ -18,6 +18,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.core.TimeValue; import org.elasticsearch.index.Index; import org.elasticsearch.test.ESTestCase; @@ -237,7 +238,7 @@ public void testUpdateFromNothingToSomethingToNothing() throws Exception { .build(); try (XContentBuilder builder = JsonXContent.contentBuilder()) { builder.startObject(); - metadata.toXContent(builder, ToXContent.EMPTY_PARAMS); + ChunkedToXContent.wrapAsXContentObject(metadata).toXContent(builder, ToXContent.EMPTY_PARAMS); builder.endObject(); logger.info("--> metadata: {}", Strings.toString(builder)); } @@ -406,7 +407,7 @@ public void testUpdatePolicyButNoPhaseChangeIndexStepsDontChange() throws Except .build(); try (XContentBuilder builder = JsonXContent.contentBuilder()) { builder.startObject(); - metadata.toXContent(builder, ToXContent.EMPTY_PARAMS); + ChunkedToXContent.wrapAsXContentObject(metadata).toXContent(builder, ToXContent.EMPTY_PARAMS); builder.endObject(); logger.info("--> metadata: {}", Strings.toString(builder)); } @@ -447,7 +448,7 @@ public void testUpdatePolicyButNoPhaseChangeIndexStepsDontChange() throws Except metadata = Metadata.builder(metadata).putCustom(IndexLifecycleMetadata.TYPE, lifecycleMetadata).build(); try (XContentBuilder builder = JsonXContent.contentBuilder()) { builder.startObject(); - metadata.toXContent(builder, ToXContent.EMPTY_PARAMS); + ChunkedToXContent.wrapAsXContentObject(metadata).toXContent(builder, ToXContent.EMPTY_PARAMS); builder.endObject(); logger.info("--> metadata: {}", Strings.toString(builder)); } diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherMetadataSerializationTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherMetadataSerializationTests.java index 9aec0fe5c9a5..cfd9624c3e73 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherMetadataSerializationTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherMetadataSerializationTests.java @@ -64,7 +64,7 @@ public void testWatcherMetadataParsingDoesNotSwallowOtherMetadata() throws Excep Collections.singletonMap(Metadata.CONTEXT_MODE_PARAM, Metadata.CONTEXT_MODE_GATEWAY) ); builder.startObject(); - builder = metadataBuilder.build().toXContent(builder, params); + builder = ChunkedToXContent.wrapAsXContentObject(metadataBuilder.build()).toXContent(builder, params); builder.endObject(); // deserialize metadata again Metadata metadata = Metadata.Builder.fromXContent(createParser(builder)); From 9478bd306a19a0a5bbbc5424fe80436a56b35725 Mon Sep 17 00:00:00 2001 From: Craig Taverner Date: Wed, 7 Dec 2022 10:35:03 +0100 Subject: [PATCH 180/919] Avoid odd edge case for azimuth when lat=-90 (#92167) Azimuth is not well defined here anyway. --- .../org/elasticsearch/h3/AzimuthTests.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/libs/h3/src/test/java/org/elasticsearch/h3/AzimuthTests.java b/libs/h3/src/test/java/org/elasticsearch/h3/AzimuthTests.java index 034ab39b0b28..99b526452c1b 100644 --- a/libs/h3/src/test/java/org/elasticsearch/h3/AzimuthTests.java +++ b/libs/h3/src/test/java/org/elasticsearch/h3/AzimuthTests.java @@ -25,15 +25,26 @@ public class AzimuthTests extends ESTestCase { - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/92136") public void testLatLonVec3d() { - final double lat = Math.toRadians(GeoTestUtil.nextLatitude()); - final double lon = Math.toRadians(GeoTestUtil.nextLongitude()); - final GeoPoint point = new GeoPoint(PlanetModel.SPHERE, lat, lon); + final GeoPoint point = safePoint(); for (int i = 0; i < Vec3d.faceCenterPoint.length; i++) { final double azVec3d = Vec3d.faceCenterPoint[i].geoAzimuthRads(point.x, point.y, point.z); final double azVec2d = Vec2d.faceCenterGeo[i].geoAzimuthRads(point.getLatitude(), point.getLongitude()); - assertEquals(azVec2d, azVec3d, 1e-14); + assertEquals("Face " + i, azVec2d, azVec3d, 1e-14); } } + + /** + * Face 19 gives -180 vs 180 for azimuth when Latitude=-90 and Longitude is between 98 and 102. + * So we just exclude lat=-90 from the test to avoid this odd edge case. + */ + private GeoPoint safePoint() { + GeoPoint point; + do { + final double lat = Math.toRadians(GeoTestUtil.nextLatitude()); + final double lon = Math.toRadians(GeoTestUtil.nextLongitude()); + point = new GeoPoint(PlanetModel.SPHERE, lat, lon); + } while (point.getLatitude() == -Math.PI / 2); + return point; + } } From 3a2ad7db3bdfd09232d36cd0db8506810400fed4 Mon Sep 17 00:00:00 2001 From: Rene Groeschke Date: Wed, 7 Dec 2022 10:45:13 +0100 Subject: [PATCH 181/919] Use separate gradle user homes per bwc project build (#92159) * Use separate gradle user homes per bwc project build * Keep respecting init files from root build in BWC build * Remove BWC Throttling --- .../gradle/internal/BwcSetupExtension.java | 9 ++++----- .../InternalDistributionBwcSetupPlugin.java | 17 +++-------------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/BwcSetupExtension.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/BwcSetupExtension.java index 28b6c1e66992..b9566a8624a3 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/BwcSetupExtension.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/BwcSetupExtension.java @@ -37,19 +37,16 @@ public class BwcSetupExtension { private static final Version BUILD_TOOL_MINIMUM_VERSION = Version.fromString("7.14.0"); private final Project project; private final Provider unreleasedVersionInfo; - private final Provider bwcTaskThrottleProvider; private Provider checkoutDir; public BwcSetupExtension( Project project, Provider unreleasedVersionInfo, - Provider bwcTaskThrottleProvider, Provider checkoutDir ) { this.project = project; this.unreleasedVersionInfo = unreleasedVersionInfo; - this.bwcTaskThrottleProvider = bwcTaskThrottleProvider; this.checkoutDir = checkoutDir; } @@ -60,7 +57,6 @@ TaskProvider bwcTask(String name, Action configuration) private TaskProvider createRunBwcGradleTask(Project project, String name, Action configAction) { return project.getTasks().register(name, LoggedExec.class, loggedExec -> { loggedExec.dependsOn("checkoutBwcBranch"); - loggedExec.usesService(bwcTaskThrottleProvider); loggedExec.getWorkingDir().set(checkoutDir.get()); loggedExec.getEnvironment().put("JAVA_HOME", unreleasedVersionInfo.zip(checkoutDir, (version, checkoutDir) -> { @@ -75,7 +71,7 @@ private TaskProvider createRunBwcGradleTask(Project project, String loggedExec.getExecutable().set(new File(checkoutDir.get(), "gradlew").toString()); } - loggedExec.args("-g", project.getGradle().getGradleUserHomeDir().toString()); + loggedExec.args("-g", project.getGradle().getGradleUserHomeDir().getAbsolutePath() + "-" + project.getName()); if (project.getGradle().getStartParameter().isOffline()) { loggedExec.args("--offline"); } @@ -101,6 +97,9 @@ private TaskProvider createRunBwcGradleTask(Project project, String if (project.getGradle().getStartParameter().isParallelProjectExecutionEnabled()) { loggedExec.args("--parallel"); } + for (File initScript : project.getGradle().getStartParameter().getInitScripts()) { + loggedExec.args("-I", initScript.getAbsolutePath()); + } loggedExec.getIndentingConsoleOutput().set(unreleasedVersionInfo.map(v -> v.version().toString())); configAction.execute(loggedExec); }); diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionBwcSetupPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionBwcSetupPlugin.java index f974b02a1c5b..373e5ab6bbae 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionBwcSetupPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionBwcSetupPlugin.java @@ -18,8 +18,6 @@ import org.gradle.api.Task; import org.gradle.api.provider.Provider; import org.gradle.api.provider.ProviderFactory; -import org.gradle.api.services.BuildService; -import org.gradle.api.services.BuildServiceParameters; import org.gradle.api.tasks.TaskProvider; import org.gradle.language.base.plugins.LifecycleBasePlugin; @@ -44,7 +42,6 @@ */ public class InternalDistributionBwcSetupPlugin implements Plugin { - private static final String BWC_TASK_THROTTLE_SERVICE = "bwcTaskThrottle"; private ProviderFactory providerFactory; @Inject @@ -55,26 +52,19 @@ public InternalDistributionBwcSetupPlugin(ProviderFactory providerFactory) { @Override public void apply(Project project) { project.getRootProject().getPluginManager().apply(GlobalBuildInfoPlugin.class); - Provider bwcTaskThrottleProvider = project.getGradle() - .getSharedServices() - .registerIfAbsent(BWC_TASK_THROTTLE_SERVICE, BwcTaskThrottle.class, spec -> spec.getMaxParallelUsages().set(1)); BuildParams.getBwcVersions() .forPreviousUnreleased( (BwcVersions.UnreleasedVersionInfo unreleasedVersion) -> { - configureBwcProject(project.project(unreleasedVersion.gradleProjectPath()), unreleasedVersion, bwcTaskThrottleProvider); + configureBwcProject(project.project(unreleasedVersion.gradleProjectPath()), unreleasedVersion); } ); } - private void configureBwcProject( - Project project, - BwcVersions.UnreleasedVersionInfo versionInfo, - Provider bwcTaskThrottleProvider - ) { + private void configureBwcProject(Project project, BwcVersions.UnreleasedVersionInfo versionInfo) { Provider versionInfoProvider = providerFactory.provider(() -> versionInfo); Provider checkoutDir = versionInfoProvider.map(info -> new File(project.getBuildDir(), "bwc/checkout-" + info.branch())); BwcSetupExtension bwcSetupExtension = project.getExtensions() - .create("bwcSetup", BwcSetupExtension.class, project, versionInfoProvider, bwcTaskThrottleProvider, checkoutDir); + .create("bwcSetup", BwcSetupExtension.class, project, versionInfoProvider, checkoutDir); BwcGitExtension gitExtension = project.getPlugins().apply(InternalBwcGitPlugin.class).getGitExtension(); Provider bwcVersion = versionInfoProvider.map(info -> info.version()); gitExtension.setBwcVersion(versionInfoProvider.map(info -> info.version())); @@ -323,5 +313,4 @@ private static class DistributionProjectArtifact { } } - public abstract class BwcTaskThrottle implements BuildService {} } From c8af735776623d3899fe5652a50be5a9de8202be Mon Sep 17 00:00:00 2001 From: David Roberts Date: Wed, 7 Dec 2022 10:10:33 +0000 Subject: [PATCH 182/919] Make adding auth info to REST responses more robust (#92168) If decoding auth information stored with a background task config fails, this will no longer prevent the entire config serialization from working. For example, if an ML datafeed config document somehow contains corrupt authorization headers then it is now possible to list datafeeds. --- docs/changelog/92168.yaml | 5 +++++ .../AuthenticationContextSerializer.java | 19 ++++++++++++++----- .../core/security/xcontent/XContentUtils.java | 10 ++++++++-- .../security/xcontent/XContentUtilsTests.java | 5 +++++ 4 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 docs/changelog/92168.yaml diff --git a/docs/changelog/92168.yaml b/docs/changelog/92168.yaml new file mode 100644 index 000000000000..fb758d44cd95 --- /dev/null +++ b/docs/changelog/92168.yaml @@ -0,0 +1,5 @@ +pr: 92168 +summary: Make adding auth info to REST responses more robust +area: Authorization +type: bug +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/AuthenticationContextSerializer.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/AuthenticationContextSerializer.java index b18b6091adde..c44376c45d65 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/AuthenticationContextSerializer.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/AuthenticationContextSerializer.java @@ -7,6 +7,8 @@ package org.elasticsearch.xpack.core.security.authc.support; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.Version; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.util.concurrent.ThreadContext; @@ -23,6 +25,8 @@ */ public class AuthenticationContextSerializer { + private static final Logger logger = LogManager.getLogger(AuthenticationContextSerializer.class); + private final String contextKey; public AuthenticationContextSerializer() { @@ -57,11 +61,16 @@ Authentication deserializeHeaderAndPutInContext(String headerValue, ThreadContex } public static Authentication decode(String header) throws IOException { - byte[] bytes = Base64.getDecoder().decode(header); - StreamInput input = StreamInput.wrap(bytes); - Version version = Version.readVersion(input); - input.setVersion(version); - return new Authentication(input); + try { + byte[] bytes = Base64.getDecoder().decode(header); + StreamInput input = StreamInput.wrap(bytes); + Version version = Version.readVersion(input); + input.setVersion(version); + return new Authentication(input); + } catch (IOException | RuntimeException e) { + logger.warn("Failed to decode authentication [" + header + "]", e); + throw e; + } } public Authentication getAuthentication(ThreadContext context) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/xcontent/XContentUtils.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/xcontent/XContentUtils.java index ed92541f7c8a..a81cc3b406b3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/xcontent/XContentUtils.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/xcontent/XContentUtils.java @@ -94,8 +94,14 @@ public static void addAuthorizationInfo(final XContentBuilder builder, final Map if (authKey == null) { return; } + Subject authenticationSubject; + try { + authenticationSubject = AuthenticationContextSerializer.decode(authKey).getEffectiveSubject(); + } catch (Exception e) { + // The exception will have been logged by AuthenticationContextSerializer.decode() so don't log it again here. + return; + } builder.startObject("authorization"); - Subject authenticationSubject = AuthenticationContextSerializer.decode(authKey).getEffectiveSubject(); switch (authenticationSubject.getType()) { case USER -> builder.array(User.Fields.ROLES.getPreferredName(), authenticationSubject.getUser().roles()); case API_KEY -> { @@ -103,7 +109,7 @@ public static void addAuthorizationInfo(final XContentBuilder builder, final Map Map metadata = authenticationSubject.getMetadata(); builder.field("id", metadata.get(AuthenticationField.API_KEY_ID_KEY)); Object name = metadata.get(AuthenticationField.API_KEY_NAME_KEY); - if (name != null) { + if (name instanceof String) { builder.field("name", name); } builder.endObject(); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/xcontent/XContentUtilsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/xcontent/XContentUtilsTests.java index 3134e4e1748e..03cb4e109a85 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/xcontent/XContentUtilsTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/xcontent/XContentUtilsTests.java @@ -67,6 +67,11 @@ public void testAddAuthorizationInfoWithServiceAccount() throws IOException { assertThat(json, equalTo("{\"authorization\":{\"service_account\":\"" + account + "\"}}")); } + public void testAddAuthorizationInfoWithCorruptData() throws IOException { + String json = generateJson(Map.of(AuthenticationField.AUTHENTICATION_KEY, "corrupt")); + assertThat(json, equalTo("{}")); + } + private String generateJson(Map headers) throws IOException { try (XContentBuilder builder = JsonXContent.contentBuilder()) { builder.startObject(); From 6818cc8dacddda40330be4c00517c9f562d5614d Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Wed, 7 Dec 2022 21:28:59 +1100 Subject: [PATCH 183/919] [Test] Do not use registered claim names in tests (#92187) JWT library has certain expectation for registered JWT claims. Therefore they are not suitable for random tests. This PR fixes it by avoid using them. Resolves: #92184 --- .../security/authc/jwt/JwtStringClaimValidatorTests.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidatorTests.java index 671f0073d14b..33383cf6f6b1 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidatorTests.java @@ -36,7 +36,7 @@ public void testClaimIsNotString() throws ParseException { } public void testClaimIsNotSingleValued() throws ParseException { - final String claimName = randomAlphaOfLengthBetween(3, 8); + final String claimName = randomAlphaOfLengthBetween(10, 18); final JwtStringClaimValidator validator = new JwtStringClaimValidator(claimName, List.of(), true); final JWTClaimsSet jwtClaimsSet = JWTClaimsSet.parse(Map.of(claimName, List.of("foo", "bar"))); @@ -49,7 +49,7 @@ public void testClaimIsNotSingleValued() throws ParseException { } public void testClaimDoesNotExist() throws ParseException { - final String claimName = randomAlphaOfLengthBetween(3, 8); + final String claimName = randomAlphaOfLengthBetween(10, 18); final JwtStringClaimValidator validator = new JwtStringClaimValidator(claimName, List.of(), randomBoolean()); final JWTClaimsSet jwtClaimsSet = JWTClaimsSet.parse(Map.of()); @@ -61,7 +61,7 @@ public void testClaimDoesNotExist() throws ParseException { } public void testMatchingClaimValues() throws ParseException { - final String claimName = randomFrom(randomAlphaOfLengthBetween(3, 8)); + final String claimName = randomAlphaOfLengthBetween(10, 18); final String claimValue = randomAlphaOfLength(10); final boolean singleValuedClaim = randomBoolean(); final JwtStringClaimValidator validator = new JwtStringClaimValidator( @@ -88,7 +88,7 @@ public void testMatchingClaimValues() throws ParseException { } public void testDoesNotSupportWildcardOrRegex() throws ParseException { - final String claimName = randomFrom(randomAlphaOfLengthBetween(3, 8)); + final String claimName = randomAlphaOfLengthBetween(10, 18); final String claimValue = randomFrom("*", "/.*/"); final JwtStringClaimValidator validator = new JwtStringClaimValidator(claimName, List.of(claimValue), randomBoolean()); From a6351aae3b10762de3242fd790bc7435e7838114 Mon Sep 17 00:00:00 2001 From: Dimitrios Liappis Date: Wed, 7 Dec 2022 13:08:53 +0200 Subject: [PATCH 184/919] Add docs about multi-arch Docker builds (#92185) Enhance docker distribution project docs with prerequisites for building multi-architecture images. --- distribution/docker/README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/distribution/docker/README.md b/distribution/docker/README.md index 2e22fe099f4f..4c8052cfc26b 100644 --- a/distribution/docker/README.md +++ b/distribution/docker/README.md @@ -92,6 +92,33 @@ images, and combining them with a Docker manifest. The Elasticsearch Delivery team aren't responsible for this - rather, it happens during our unified release process. +To build multi-architecture images on `x86_64` hosts using Docker[^1], you'll +need [buildx](https://docs.docker.com/build/buildx/install/) and ensure that it +supports both `linux/amd64` **and** `linux/arm64` targets. + +You can verify the supported targets using `docker buildx ls`. For example, the +following output indicates that support for `linux/arm64` is missing: + +```shell +$ docker buildx ls +NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS +default * docker + default default running 20.10.21 linux/amd64, linux/386 +``` + +On Linux `x86_64` hosts, to enable `linux-arm64` you need to install +[qemu-user-static-binfmt](https://github.com/multiarch/qemu-user-static). +Installation details depend on the Linux distribution but, as described in the +[getting started docs](https://github.com/multiarch/qemu-user-static#getting-started), +running `docker run --rm --privileged multiarch/qemu-user-static --reset -p yes` +will add the necessary support (but will not persist across reboots): + +```shell +$ docker buildx ls +NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS +default * docker + default default running 20.10.21 linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6 +``` ## Testing @@ -130,3 +157,5 @@ Ideally this import / export stuff should be completely removed. [DockerTests]: ../../qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java [multi-arch]: https://www.docker.com/blog/multi-arch-build-and-images-the-simple-way/ [ubi]: https://developers.redhat.com/products/rhel/ubi + +[^1]: `podman/buildah` also [supports building multi-platform images](https://github.com/containers/buildah/issues/1590). From bd30534452a9694e84f3c6c7ee81838b8c182718 Mon Sep 17 00:00:00 2001 From: Egor Krylovich Date: Wed, 7 Dec 2022 12:50:22 +0100 Subject: [PATCH 185/919] 90630: Fix links to Apache HTTP Client doc (#92174) --- docs/java-rest/low-level/configuration.asciidoc | 6 +++--- docs/java-rest/low-level/usage.asciidoc | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/java-rest/low-level/configuration.asciidoc b/docs/java-rest/low-level/configuration.asciidoc index 18f96858c761..e1102305db1d 100644 --- a/docs/java-rest/low-level/configuration.asciidoc +++ b/docs/java-rest/low-level/configuration.asciidoc @@ -14,7 +14,7 @@ additional configuration for the low-level Java REST Client. Configuring requests timeouts can be done by providing an instance of `RequestConfigCallback` while building the `RestClient` through its builder. The interface has one method that receives an instance of -https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/client/config/RequestConfig.Builder.html[`org.apache.http.client.config.RequestConfig.Builder`] +https://hc.apache.org/httpcomponents-client-4.5.x/current/httpclient/apidocs/org/apache/http/client/config/RequestConfig.Builder.html[`org.apache.http.client.config.RequestConfig.Builder`] as an argument and has the same return type. The request config builder can be modified and then returned. In the following example we increase the connect timeout (defaults to 1 second) and the socket timeout (defaults to 30 @@ -50,7 +50,7 @@ include-tagged::{doc-tests}/RestClientDocumentation.java[rest-client-config-thre Configuring basic authentication can be done by providing an `HttpClientConfigCallback` while building the `RestClient` through its builder. The interface has one method that receives an instance of -https://hc.apache.org/httpcomponents-asyncclient-dev/httpasyncclient/apidocs/org/apache/http/impl/nio/client/HttpAsyncClientBuilder.html[`org.apache.http.impl.nio.client.HttpAsyncClientBuilder`] +https://hc.apache.org/httpcomponents-asyncclient-4.1.x/current/httpasyncclient/apidocs/org/apache/http/impl/nio/client/HttpAsyncClientBuilder.html[`org.apache.http.impl.nio.client.HttpAsyncClientBuilder`] as an argument and has the same return type. The http client builder can be modified and then returned. In the following example we set a default credentials provider that requires basic authentication. @@ -99,7 +99,7 @@ include-tagged::{doc-tests}/RestClientDocumentation.java[rest-client-auth-api-ke Encrypted communication using TLS can also be configured through the `HttpClientConfigCallback`. The -https://hc.apache.org/httpcomponents-asyncclient-dev/httpasyncclient/apidocs/org/apache/http/impl/nio/client/HttpAsyncClientBuilder.html[`org.apache.http.impl.nio.client.HttpAsyncClientBuilder`] +https://hc.apache.org/httpcomponents-asyncclient-4.1.x/current/httpasyncclient/apidocs/org/apache/http/impl/nio/client/HttpAsyncClientBuilder.html[`org.apache.http.impl.nio.client.HttpAsyncClientBuilder`] received as an argument exposes multiple methods to configure encrypted communication: `setSSLContext`, `setSSLSessionStrategy` and `setConnectionManager`, in order of precedence from the least important. diff --git a/docs/java-rest/low-level/usage.asciidoc b/docs/java-rest/low-level/usage.asciidoc index 342388f1370c..439606281e04 100644 --- a/docs/java-rest/low-level/usage.asciidoc +++ b/docs/java-rest/low-level/usage.asciidoc @@ -57,7 +57,7 @@ dependencies { === Dependencies The low-level Java REST client internally uses the -https://hc.apache.org/httpcomponents-asyncclient-dev/[Apache Http Async Client] +https://hc.apache.org/httpcomponents-asyncclient-4.1.x/[Apache Http Async Client] to send http requests. It depends on the following artifacts, namely the async http client and its own transitive dependencies: @@ -152,7 +152,7 @@ A `RestClient` instance can be built through the corresponding `RestClientBuilder` class, created via `RestClient#builder(HttpHost...)` static method. The only required argument is one or more hosts that the client will communicate with, provided as instances of -https://hc.apache.org/httpcomponents-core-5.2.x/current/httpcore5/apidocs/org/apache/hc/core5/http/HttpHost.html[HttpHost] +https://hc.apache.org/httpcomponents-core-4.4.x/current/httpcore/apidocs/org/apache/http/HttpHost.html[HttpHost] as follows: ["source","java",subs="attributes,callouts,macros"] @@ -203,7 +203,7 @@ include-tagged::{doc-tests}/RestClientDocumentation.java[rest-client-init-reques -------------------------------------------------- <1> Set a callback that allows to modify the default request configuration (e.g. request timeouts, authentication, or anything that the -https://hc.apache.org/httpcomponents-client-5.2.x/current/httpclient5/apidocs/org/apache/hc/client5/http/config/RequestConfig.Builder.html[`org.apache.hc.client5.http.config.RequestConfig.Builder`] +https://hc.apache.org/httpcomponents-client-4.5.x/current/httpclient/apidocs/org/apache/http/client/config/RequestConfig.Builder.html[`org.apache.http.client.config.RequestConfig.Builder`] allows to set) ["source","java",subs="attributes,callouts,macros"] @@ -366,7 +366,7 @@ include-tagged::{doc-tests}/RestClientDocumentation.java[rest-client-response2] <2> The host that returned the response <3> The response status line, from which you can for instance retrieve the status code <4> The response headers, which can also be retrieved by name though `getHeader(String)` -<5> The response body enclosed in an https://hc.apache.org/httpcomponents-core-ga/httpcore/apidocs/org/apache/http/HttpEntity.html[`org.apache.http.HttpEntity`] +<5> The response body enclosed in an https://hc.apache.org/httpcomponents-core-4.4.x/current/httpcore/apidocs/org/apache/http/HttpEntity.html[`org.apache.http.HttpEntity`] object When performing a request, an exception is thrown (or received as an argument @@ -395,13 +395,13 @@ and un-marshalling. Users are free to use the library that they prefer for that purpose. The underlying Apache Async Http Client ships with different -https://hc.apache.org/httpcomponents-core-ga/httpcore/apidocs/org/apache/http/HttpEntity.html[`org.apache.http.HttpEntity`] +https://hc.apache.org/httpcomponents-core-4.4.x/current/httpcore/apidocs/org/apache/http/HttpEntity.html[`org.apache.http.HttpEntity`] implementations that allow to provide the request body in different formats (stream, byte array, string etc.). As for reading the response body, the `HttpEntity#getContent` method comes handy which returns an `InputStream` reading from the previously buffered response body. As an alternative, it is possible to provide a custom -https://hc.apache.org/httpcomponents-core-ga/httpcore-nio/apidocs/org/apache/http/nio/protocol/HttpAsyncResponseConsumer.html[`org.apache.http.nio.protocol.HttpAsyncResponseConsumer`] +https://hc.apache.org/httpcomponents-core-4.4.x/current/httpcore-nio/apidocs/org/apache/http/nio/protocol/HttpAsyncResponseConsumer.html[`org.apache.http.nio.protocol.HttpAsyncResponseConsumer`] that controls how bytes are read and buffered. [[java-rest-low-usage-logging]] From ee38b30b75ee8b4af008d7765d3dc59230af64f3 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 7 Dec 2022 11:51:03 +0000 Subject: [PATCH 186/919] Remove unused CountDown#originalCount (#92170) This field is only used in assertions, but they are not useful assertions since they hold by construction. --- .../org/elasticsearch/common/util/concurrent/CountDown.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/util/concurrent/CountDown.java b/server/src/main/java/org/elasticsearch/common/util/concurrent/CountDown.java index bbbea0f31d8d..3c4f76d6fd2f 100644 --- a/server/src/main/java/org/elasticsearch/common/util/concurrent/CountDown.java +++ b/server/src/main/java/org/elasticsearch/common/util/concurrent/CountDown.java @@ -19,13 +19,11 @@ public final class CountDown { private final AtomicInteger countDown; - private final int originalCount; public CountDown(int count) { if (count < 0) { throw new IllegalArgumentException("count must be greater or equal to 0 but was: " + count); } - this.originalCount = count; this.countDown = new AtomicInteger(count); } @@ -34,7 +32,6 @@ public CountDown(int count) { * reached zero otherwise false */ public boolean countDown() { - assert originalCount > 0; return countDown.getAndUpdate(current -> { assert current >= 0; return current == 0 ? 0 : current - 1; @@ -47,7 +44,6 @@ public boolean countDown() { * false */ public boolean fastForward() { - assert originalCount > 0; assert countDown.get() >= 0; return countDown.getAndSet(0) > 0; } From 01d476e11b6803ebf2dc37914148c92bc722e1a9 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 7 Dec 2022 12:13:28 +0000 Subject: [PATCH 187/919] Revert "Remove unused CountDown#originalCount (#92170)" This reverts commit ee38b30b75ee8b4af008d7765d3dc59230af64f3. --- .../elasticsearch/common/util/concurrent/CountDown.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/common/util/concurrent/CountDown.java b/server/src/main/java/org/elasticsearch/common/util/concurrent/CountDown.java index 3c4f76d6fd2f..17611f4372bb 100644 --- a/server/src/main/java/org/elasticsearch/common/util/concurrent/CountDown.java +++ b/server/src/main/java/org/elasticsearch/common/util/concurrent/CountDown.java @@ -20,10 +20,15 @@ public final class CountDown { private final AtomicInteger countDown; + // Track originalCount because new CountDown(0) never reports completion which can be a leak if not handled specially + // TODO fix the places that create an empty CountDown and then drop this, see #92196 + private final int originalCount; + public CountDown(int count) { if (count < 0) { throw new IllegalArgumentException("count must be greater or equal to 0 but was: " + count); } + this.originalCount = count; this.countDown = new AtomicInteger(count); } @@ -32,6 +37,7 @@ public CountDown(int count) { * reached zero otherwise false */ public boolean countDown() { + assert originalCount > 0; return countDown.getAndUpdate(current -> { assert current >= 0; return current == 0 ? 0 : current - 1; @@ -44,6 +50,7 @@ public boolean countDown() { * false */ public boolean fastForward() { + assert originalCount > 0; assert countDown.get() >= 0; return countDown.getAndSet(0) > 0; } From a653c7071a5744c47e22a1b5f132f11e47b7cf18 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Wed, 7 Dec 2022 12:44:59 +0000 Subject: [PATCH 188/919] [ML] Adjust more BWC constants (#92195) When semantic search was backed out in #92105 the `tookMillis` field was removed from 8.6. But the 8.6 BWC constants for `tookMillis` were not changed in #92157. This change corrects that discrepancy. --- .../core/ml/action/InferTrainedModelDeploymentAction.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/InferTrainedModelDeploymentAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/InferTrainedModelDeploymentAction.java index 2217403c75b5..7b1d40167f3c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/InferTrainedModelDeploymentAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/InferTrainedModelDeploymentAction.java @@ -312,7 +312,7 @@ public Response(InferenceResults result, long tookMillis) { public Response(StreamInput in) throws IOException { super(in); results = in.readNamedWriteable(InferenceResults.class); - if (in.getVersion().onOrAfter(Version.V_8_6_0)) { + if (in.getVersion().onOrAfter(Version.V_8_7_0)) { tookMillis = in.readVLong(); } } @@ -329,7 +329,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); out.writeNamedWriteable(results); - if (out.getVersion().onOrAfter(Version.V_8_6_0)) { + if (out.getVersion().onOrAfter(Version.V_8_7_0)) { out.writeVLong(tookMillis); } } From 52db0519679e396e8921cf7f27d4bf8b3e053326 Mon Sep 17 00:00:00 2001 From: Rene Groeschke Date: Wed, 7 Dec 2022 15:37:24 +0100 Subject: [PATCH 189/919] Ensure rerunning querying-cluster tests in basic license tests supported (#92194) --- .../StandaloneRestIntegTestTask.java | 16 ++++++++++------ .../legacy-with-basic-license/build.gradle | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/build-tools/src/main/java/org/elasticsearch/gradle/testclusters/StandaloneRestIntegTestTask.java b/build-tools/src/main/java/org/elasticsearch/gradle/testclusters/StandaloneRestIntegTestTask.java index 11ad0a29f5b8..13d2c8b948d5 100644 --- a/build-tools/src/main/java/org/elasticsearch/gradle/testclusters/StandaloneRestIntegTestTask.java +++ b/build-tools/src/main/java/org/elasticsearch/gradle/testclusters/StandaloneRestIntegTestTask.java @@ -9,8 +9,11 @@ import org.elasticsearch.gradle.FileSystemOperationsAware; import org.elasticsearch.gradle.util.GradleUtils; +import org.gradle.api.Task; import org.gradle.api.provider.Provider; import org.gradle.api.services.internal.BuildServiceRegistryInternal; +import org.gradle.api.specs.NotSpec; +import org.gradle.api.specs.Spec; import org.gradle.api.tasks.CacheableTask; import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Nested; @@ -40,6 +43,11 @@ public class StandaloneRestIntegTestTask extends Test implements TestClustersAwa private boolean debugServer = false; public StandaloneRestIntegTestTask() { + Spec taskSpec = t -> getProject().getTasks() + .withType(StandaloneRestIntegTestTask.class) + .stream() + .filter(task -> task != this) + .anyMatch(task -> Collections.disjoint(task.getClusters(), getClusters()) == false); this.getOutputs() .doNotCacheIf( "Caching disabled for this task since it uses a cluster shared by other tasks", @@ -49,13 +57,9 @@ public StandaloneRestIntegTestTask() { * avoid any undesired behavior we simply disable the cache if we detect that this task uses a cluster shared between * multiple tasks. */ - t -> getProject().getTasks() - .withType(StandaloneRestIntegTestTask.class) - .stream() - .filter(task -> task != this) - .anyMatch(task -> Collections.disjoint(task.getClusters(), getClusters()) == false) + taskSpec ); - + this.getOutputs().upToDateWhen(new NotSpec(taskSpec)); this.getOutputs() .doNotCacheIf( "Caching disabled for this task since it is configured to preserve data directory", diff --git a/x-pack/qa/multi-cluster-search-security/legacy-with-basic-license/build.gradle b/x-pack/qa/multi-cluster-search-security/legacy-with-basic-license/build.gradle index 77c6f91f75cd..f9d60181fc81 100644 --- a/x-pack/qa/multi-cluster-search-security/legacy-with-basic-license/build.gradle +++ b/x-pack/qa/multi-cluster-search-security/legacy-with-basic-license/build.gradle @@ -55,8 +55,8 @@ tasks.register('fulfilling-cluster', RestIntegTestTask) { tasks.register('querying-cluster', RestIntegTestTask) { dependsOn 'fulfilling-cluster' - useCluster queryingCluster useCluster fulfillingCluster + useCluster queryingCluster systemProperty 'tests.rest.suite', 'querying_cluster' if (proxyMode) { systemProperty 'tests.rest.blacklist', [ From c13c23be9072b022f1826e234973b57fea6f5f39 Mon Sep 17 00:00:00 2001 From: Joe Gallo Date: Wed, 7 Dec 2022 09:38:33 -0500 Subject: [PATCH 190/919] Optimize IngestDocument TemplateScript and ValueSource execution (#91723) --- .../elasticsearch/common/util/LazyMap.java | 174 ------------------ .../elasticsearch/ingest/IngestDocument.java | 139 +++++++++++--- 2 files changed, 110 insertions(+), 203 deletions(-) delete mode 100644 server/src/main/java/org/elasticsearch/common/util/LazyMap.java diff --git a/server/src/main/java/org/elasticsearch/common/util/LazyMap.java b/server/src/main/java/org/elasticsearch/common/util/LazyMap.java deleted file mode 100644 index 7b6be425985e..000000000000 --- a/server/src/main/java/org/elasticsearch/common/util/LazyMap.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.common.util; - -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.function.Supplier; - -public class LazyMap implements Map { - - private final Supplier> mapSupplier; - private volatile Map map; - - public LazyMap(Supplier> mapSupplier) { - this.mapSupplier = mapSupplier; - } - - private Map get() { - if (map == null) { - synchronized (this) { - if (map == null) { - map = Objects.requireNonNullElse(mapSupplier.get(), Collections.emptyMap()); - } - } - } - return map; - } - - @Override - public int size() { - return get().size(); - } - - @Override - public boolean isEmpty() { - return get().isEmpty(); - } - - @Override - public boolean containsKey(Object key) { - return get().containsKey(key); - } - - @Override - public boolean containsValue(Object value) { - return get().containsValue(value); - } - - @Override - public V get(Object key) { - return get().get(key); - } - - @Override - public V put(K key, V value) { - return get().put(key, value); - } - - @Override - public V remove(Object key) { - return get().remove(key); - } - - @Override - public void putAll(Map m) { - get().putAll(m); - } - - @Override - public void clear() { - get().clear(); - } - - @Override - public Set keySet() { - return get().keySet(); - } - - @Override - public Collection values() { - return get().values(); - } - - @Override - public Set> entrySet() { - return get().entrySet(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - return get().equals(o); - } - - @Override - public int hashCode() { - return get().hashCode(); - } - - @Override - public String toString() { - return get().toString(); - } - - // Override default methods in Map - @Override - public V getOrDefault(Object k, V defaultValue) { - return get().getOrDefault(k, defaultValue); - } - - @Override - public void forEach(BiConsumer action) { - get().forEach(action); - } - - @Override - public void replaceAll(BiFunction function) { - get().replaceAll(function); - } - - @Override - public V putIfAbsent(K key, V value) { - return get().putIfAbsent(key, value); - } - - @Override - public boolean remove(Object key, Object value) { - return get().remove(key, value); - } - - @Override - public boolean replace(K key, V oldValue, V newValue) { - return get().replace(key, oldValue, newValue); - } - - @Override - public V replace(K key, V value) { - return get().replace(key, value); - } - - @Override - public V computeIfAbsent(K key, Function mappingFunction) { - return get().computeIfAbsent(key, mappingFunction); - } - - @Override - public V computeIfPresent(K key, BiFunction remappingFunction) { - return get().computeIfPresent(key, remappingFunction); - } - - @Override - public V compute(K key, BiFunction remappingFunction) { - return get().compute(key, remappingFunction); - } - - @Override - public V merge(K key, V value, BiFunction remappingFunction) { - return get().merge(key, value, remappingFunction); - } -} diff --git a/server/src/main/java/org/elasticsearch/ingest/IngestDocument.java b/server/src/main/java/org/elasticsearch/ingest/IngestDocument.java index dd5a5607c7f6..76c42b7fc8a4 100644 --- a/server/src/main/java/org/elasticsearch/ingest/IngestDocument.java +++ b/server/src/main/java/org/elasticsearch/ingest/IngestDocument.java @@ -9,7 +9,6 @@ package org.elasticsearch.ingest; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.util.LazyMap; import org.elasticsearch.common.util.Maps; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.index.VersionType; @@ -26,6 +25,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -43,15 +43,23 @@ public final class IngestDocument { public static final String INGEST_KEY = "_ingest"; - public static final String PIPELINE_CYCLE_ERROR_MESSAGE = "Cycle detected for pipeline: "; + public static final String SOURCE_KEY = SourceFieldMapper.NAME; // "_source" private static final String INGEST_KEY_PREFIX = INGEST_KEY + "."; - private static final String SOURCE_PREFIX = SourceFieldMapper.NAME + "."; + private static final String SOURCE_PREFIX = SOURCE_KEY + "."; + public static final String PIPELINE_CYCLE_ERROR_MESSAGE = "Cycle detected for pipeline: "; static final String TIMESTAMP = "timestamp"; private final IngestCtxMap ctxMap; private final Map ingestMetadata; + /** + * Shallowly read-only, very limited, map-like view of the ctxMap and ingestMetadata, + * for providing as a model to TemplateScript and ValueSource instances. This avoids the cost of + * constructing a purpose-built map on each template evaluation. + */ + private final DelegatingMapView templateModel; + // Contains all pipelines that have been executed for this document private final Set executedPipelines = new LinkedHashSet<>(); @@ -61,6 +69,7 @@ public IngestDocument(String index, String id, long version, String routing, Ver this.ctxMap = new IngestCtxMap(index, id, version, routing, versionType, ZonedDateTime.now(ZoneOffset.UTC), source); this.ingestMetadata = new HashMap<>(); this.ingestMetadata.put(TIMESTAMP, ctxMap.getMetadata().getNow()); + this.templateModel = initializeTemplateModel(); } /** @@ -91,16 +100,22 @@ public IngestDocument(Map sourceAndMetadata, Map } } } - this.ingestMetadata = new HashMap<>(ingestMetadata); this.ctxMap = new IngestCtxMap(source, new IngestDocMetadata(metadata, IngestCtxMap.getTimestamp(ingestMetadata))); + this.ingestMetadata = new HashMap<>(ingestMetadata); + this.templateModel = initializeTemplateModel(); } /** * Constructor to create an IngestDocument from its constituent maps */ IngestDocument(IngestCtxMap ctxMap, Map ingestMetadata) { - this.ctxMap = ctxMap; - this.ingestMetadata = ingestMetadata; + this.ctxMap = Objects.requireNonNull(ctxMap); + this.ingestMetadata = Objects.requireNonNull(ingestMetadata); + this.templateModel = initializeTemplateModel(); + } + + private DelegatingMapView initializeTemplateModel() { + return new DelegatingMapView(ctxMap, Map.of(SOURCE_KEY, ctxMap, INGEST_KEY, ingestMetadata)); } /** @@ -425,8 +440,7 @@ public void appendFieldValue(String path, Object value, boolean allowDuplicates) * @throws IllegalArgumentException if the path is null, empty or invalid. */ public void appendFieldValue(TemplateScript.Factory fieldPathTemplate, ValueSource valueSource) { - Map model = createTemplateModel(); - appendFieldValue(fieldPathTemplate.newInstance(model).execute(), valueSource.copyAndResolve(model)); + appendFieldValue(fieldPathTemplate.newInstance(templateModel).execute(), valueSource.copyAndResolve(templateModel)); } /** @@ -443,8 +457,11 @@ public void appendFieldValue(TemplateScript.Factory fieldPathTemplate, ValueSour * @throws IllegalArgumentException if the path is null, empty or invalid. */ public void appendFieldValue(TemplateScript.Factory fieldPathTemplate, ValueSource valueSource, boolean allowDuplicates) { - Map model = createTemplateModel(); - appendFieldValue(fieldPathTemplate.newInstance(model).execute(), valueSource.copyAndResolve(model), allowDuplicates); + appendFieldValue( + fieldPathTemplate.newInstance(templateModel).execute(), + valueSource.copyAndResolve(templateModel), + allowDuplicates + ); } /** @@ -471,8 +488,7 @@ public void setFieldValue(String path, Object value) { * item identified by the provided path. */ public void setFieldValue(TemplateScript.Factory fieldPathTemplate, ValueSource valueSource) { - Map model = createTemplateModel(); - setFieldValue(fieldPathTemplate.newInstance(model).execute(), valueSource.copyAndResolve(model), false); + setFieldValue(fieldPathTemplate.newInstance(templateModel).execute(), valueSource.copyAndResolve(templateModel), false); } /** @@ -486,8 +502,7 @@ public void setFieldValue(TemplateScript.Factory fieldPathTemplate, ValueSource * item identified by the provided path. */ public void setFieldValue(TemplateScript.Factory fieldPathTemplate, ValueSource valueSource, boolean ignoreEmptyValue) { - Map model = createTemplateModel(); - Object value = valueSource.copyAndResolve(model); + Object value = valueSource.copyAndResolve(templateModel); if (ignoreEmptyValue && valueSource instanceof ValueSource.TemplatedValue) { if (value == null) { return; @@ -498,7 +513,7 @@ public void setFieldValue(TemplateScript.Factory fieldPathTemplate, ValueSource } } - setFieldValue(fieldPathTemplate.newInstance(model).execute(), value, false); + setFieldValue(fieldPathTemplate.newInstance(templateModel).execute(), value, false); } /** @@ -512,7 +527,6 @@ public void setFieldValue(TemplateScript.Factory fieldPathTemplate, ValueSource * item identified by the provided path. */ public void setFieldValue(TemplateScript.Factory fieldPathTemplate, Object value, boolean ignoreEmptyValue) { - Map model = createTemplateModel(); if (ignoreEmptyValue) { if (value == null) { return; @@ -524,7 +538,7 @@ public void setFieldValue(TemplateScript.Factory fieldPathTemplate, Object value } } - setFieldValue(fieldPathTemplate.newInstance(model).execute(), value, false); + setFieldValue(fieldPathTemplate.newInstance(templateModel).execute(), value, false); } private void setFieldValue(String path, Object value, boolean append) { @@ -698,18 +712,7 @@ private static T cast(String path, Object object, Class clazz) { } public String renderTemplate(TemplateScript.Factory template) { - return template.newInstance(createTemplateModel()).execute(); - } - - private Map createTemplateModel() { - return new LazyMap<>(() -> { - Map model = new HashMap<>(ctxMap); - model.put(SourceFieldMapper.NAME, ctxMap); - // If there is a field in the source with the name '_ingest' it gets overwritten here, - // if access to that field is required then it get accessed via '_source._ingest' - model.put(INGEST_KEY, ingestMetadata); - return model; - }); + return template.newInstance(templateModel).execute(); } /** @@ -973,4 +976,82 @@ static ResolveResult error(String errorMessage) { } } + + /** + * Provides a shallowly read-only, very limited, map-like view of two maps. The only methods that are implemented are + * {@link Map#get(Object)} and {@link Map#containsKey(Object)}, everything else throws UnsupportedOperationException. + * + * The overrides map has higher priority than the primary map -- values in that map under some key will take priority over values + * in the primary map under the same key. + * + * @param primary the primary map + * @param overrides the overrides map + */ + private record DelegatingMapView(Map primary, Map overrides) implements Map { + + @Override + public boolean containsKey(Object key) { + // most normal uses of this in practice will end up passing in keys that match the primary, rather than the overrides, + // in which case we can shortcut by checking the primary first + return primary.containsKey(key) || overrides.containsKey(key); + } + + @Override + public Object get(Object key) { + // null values in the overrides map are treated as *key not present*, so we don't have to do a containsKey check here -- + // if the overrides map returns null we can simply delegate to the primary + Object result = overrides.get(key); + return result != null ? result : primary.get(key); + } + + @Override + public int size() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isEmpty() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsValue(Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public Object put(String key, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public Object remove(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(Map m) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public Set keySet() { + throw new UnsupportedOperationException(); + } + + @Override + public Collection values() { + throw new UnsupportedOperationException(); + } + + @Override + public Set> entrySet() { + throw new UnsupportedOperationException(); + } + } } From 18801a83903844a611ad3f7fc10bbb32fa331721 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 7 Dec 2022 15:48:44 +0000 Subject: [PATCH 191/919] Introduce Iterators#flatMap (#92202) Various places that implement response chunking involving some kind of nested structure (e.g. a list of lists) need to call something like `flatMap`, and today this means they must must convert between iterators and streams to make use of `Stream#flatMap`. This is noisy and awkward, so with this commit we introduce a way to `flatMap` directly on iterators. Relates #89838 --- .../node/tasks/list/ListTasksResponse.java | 14 ++- .../status/SnapshotsStatusResponse.java | 13 +-- .../cluster/metadata/IndexGraveyard.java | 2 +- .../cluster/metadata/Metadata.java | 22 ++--- .../common/collect/Iterators.java | 91 +++++++++++++++---- .../xcontent/ChunkedToXContentHelper.java | 4 +- .../org/elasticsearch/health/Diagnosis.java | 27 ++---- .../health/HealthIndicatorResult.java | 18 +--- .../elasticsearch/ingest/IngestMetadata.java | 2 +- .../PersistentTasksCustomMetadata.java | 2 +- .../common/collect/IteratorsTests.java | 30 ++++++ 11 files changed, 137 insertions(+), 88 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/list/ListTasksResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/list/ListTasksResponse.java index 2d65e128a657..1937effdaeda 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/list/ListTasksResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/list/ListTasksResponse.java @@ -32,10 +32,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; -import java.util.stream.Stream; import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; @@ -166,9 +164,9 @@ public ChunkedToXContent groupedByNode(Supplier nodesInCluster) toXContentCommon(builder, params); builder.startObject("nodes"); return builder; - }), getPerNodeTasks().entrySet().stream().flatMap(entry -> { + }), Iterators.flatMap(getPerNodeTasks().entrySet().iterator(), entry -> { DiscoveryNode node = discoveryNodes.get(entry.getKey()); - return Stream.>of(Stream.of((builder, params) -> { + return Iterators.concat(Iterators.single((builder, params) -> { builder.startObject(entry.getKey()); if (node != null) { // If the node is no longer part of the cluster, oh well, we'll just skip its useful information. @@ -193,17 +191,17 @@ public ChunkedToXContent groupedByNode(Supplier nodesInCluster) } builder.startObject(TASKS); return builder; - }), entry.getValue().stream().map(task -> (builder, params) -> { + }), Iterators.flatMap(entry.getValue().iterator(), task -> Iterators.single((builder, params) -> { builder.startObject(task.taskId().toString()); task.toXContent(builder, params); builder.endObject(); return builder; - }), Stream.of((builder, params) -> { + })), Iterators.single((builder, params) -> { builder.endObject(); builder.endObject(); return builder; - })).flatMap(Function.identity()); - }).iterator(), Iterators.single((builder, params) -> { + })); + }), Iterators.single((builder, params) -> { builder.endObject(); builder.endObject(); return builder; diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponse.java index 3ac90f882ab2..abd1c0b9068e 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/SnapshotsStatusResponse.java @@ -22,9 +22,6 @@ import java.util.Iterator; import java.util.List; import java.util.Objects; -import java.util.Spliterator; -import java.util.Spliterators; -import java.util.stream.StreamSupport; import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; @@ -90,13 +87,9 @@ public int hashCode() { @Override public Iterator toXContentChunked(ToXContent.Params params) { - return Iterators.concat( - Iterators.single((ToXContent) (b, p) -> b.startObject().startArray("snapshots")), - snapshots.stream() - .flatMap( - s -> StreamSupport.stream(Spliterators.spliteratorUnknownSize(s.toXContentChunked(params), Spliterator.ORDERED), false) - ) - .iterator(), + return Iterators.concat( + Iterators.single((b, p) -> b.startObject().startArray("snapshots")), + Iterators.flatMap(snapshots.iterator(), s -> s.toXContentChunked(params)), Iterators.single((b, p) -> b.endArray().endObject()) ); } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexGraveyard.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexGraveyard.java index 900c4ff80f00..bf628c7f5ce8 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexGraveyard.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexGraveyard.java @@ -127,7 +127,7 @@ public boolean containsIndex(final Index index) { @Override public Iterator toXContentChunked(ToXContent.Params ignored) { - return ChunkedToXContentHelper.array(TOMBSTONES_FIELD.getPreferredName(), tombstones); + return ChunkedToXContentHelper.array(TOMBSTONES_FIELD.getPreferredName(), tombstones.iterator()); } public static IndexGraveyard fromXContent(final XContentParser parser) throws IOException { diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java index 46670b84d42c..4eb9ced4f435 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java @@ -75,8 +75,6 @@ import java.util.Optional; import java.util.Set; import java.util.SortedMap; -import java.util.Spliterator; -import java.util.Spliterators; import java.util.TreeMap; import java.util.function.BiPredicate; import java.util.function.Consumer; @@ -84,7 +82,6 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; -import java.util.stream.StreamSupport; import static org.elasticsearch.cluster.metadata.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; import static org.elasticsearch.common.settings.Settings.readSettingsFromStream; @@ -1340,7 +1337,7 @@ public Iterator toXContentChunked(ToXContent.Params p) { ? ChunkedToXContentHelper.wrapWithObject("indices", indices().values().iterator()) : Collections.emptyIterator(); - return Iterators.concat(start, Iterators.single((builder, params) -> { + return Iterators.concat(start, Iterators.single((builder, params) -> { builder.field("cluster_uuid", clusterUUID); builder.field("cluster_uuid_committed", clusterUUIDCommitted); builder.startObject("cluster_coordination"); @@ -1362,17 +1359,12 @@ public Iterator toXContentChunked(ToXContent.Params p) { .iterator() ), indices, - customs().entrySet() - .stream() - .filter(cursor -> cursor.getValue().context().contains(context)) - .map( - cursor -> (ChunkedToXContent) params -> ChunkedToXContentHelper.wrapWithObject( - cursor.getKey(), - cursor.getValue().toXContentChunked(params) - ) - ) - .flatMap(s -> StreamSupport.stream(Spliterators.spliteratorUnknownSize(s.toXContentChunked(p), Spliterator.ORDERED), false)) - .iterator(), + Iterators.flatMap( + customs.entrySet().iterator(), + entry -> entry.getValue().context().contains(context) + ? ChunkedToXContentHelper.wrapWithObject(entry.getKey(), entry.getValue().toXContentChunked(p)) + : Collections.emptyIterator() + ), ChunkedToXContentHelper.wrapWithObject("reserved_state", reservedStateMetadata().values().iterator()), ChunkedToXContentHelper.endObject() ); diff --git a/server/src/main/java/org/elasticsearch/common/collect/Iterators.java b/server/src/main/java/org/elasticsearch/common/collect/Iterators.java index a2629ffd0556..2f78ba70529c 100644 --- a/server/src/main/java/org/elasticsearch/common/collect/Iterators.java +++ b/server/src/main/java/org/elasticsearch/common/collect/Iterators.java @@ -8,9 +8,13 @@ package org.elasticsearch.common.collect; +import org.elasticsearch.core.Nullable; + +import java.util.Collections; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Objects; +import java.util.function.Function; public class Iterators { @@ -43,36 +47,33 @@ public static Iterator concat(Iterator... iterators) { throw new NullPointerException("iterators"); } - // explicit generic type argument needed for type inference - return new ConcatenatedIterator(iterators); + for (int i = 0; i < iterators.length; i++) { + if (iterators[i].hasNext()) { + // explicit generic type argument needed for type inference + return new ConcatenatedIterator(iterators, i); + } + } + + return Collections.emptyIterator(); } - static class ConcatenatedIterator implements Iterator { + private static class ConcatenatedIterator implements Iterator { private final Iterator[] iterators; - private int index = 0; + private int index; - @SafeVarargs - @SuppressWarnings("varargs") - ConcatenatedIterator(Iterator... iterators) { - if (iterators == null) { - throw new NullPointerException("iterators"); - } - for (int i = 0; i < iterators.length; i++) { + ConcatenatedIterator(Iterator[] iterators, int startIndex) { + for (int i = startIndex; i < iterators.length; i++) { if (iterators[i] == null) { throw new NullPointerException("iterators[" + i + "]"); } } this.iterators = iterators; + this.index = startIndex; } @Override public boolean hasNext() { - boolean hasNext = false; - while (index < iterators.length && (hasNext = iterators[index].hasNext()) == false) { - index++; - } - - return hasNext; + return index < iterators.length; } @Override @@ -80,7 +81,11 @@ public T next() { if (hasNext() == false) { throw new NoSuchElementException(); } - return iterators[index].next(); + final T value = iterators[index].next(); + while (index < iterators.length && iterators[index].hasNext() == false) { + index++; + } + return value; } } @@ -110,4 +115,54 @@ public T next() { return array[index++]; } } + + public static Iterator flatMap(Iterator input, Function> fn) { + while (input.hasNext()) { + final var value = fn.apply(input.next()); + if (value.hasNext()) { + return new FlatMapIterator<>(input, fn, value); + } + } + + return Collections.emptyIterator(); + } + + private static final class FlatMapIterator implements Iterator { + + private final Iterator input; + private final Function> fn; + + @Nullable // if finished, otherwise currentOutput.hasNext() is true + private Iterator currentOutput; + + FlatMapIterator(Iterator input, Function> fn, Iterator firstOutput) { + this.input = input; + this.fn = fn; + this.currentOutput = firstOutput; + } + + @Override + public boolean hasNext() { + return currentOutput != null; + } + + @Override + public U next() { + if (hasNext() == false) { + throw new NoSuchElementException(); + } + // noinspection ConstantConditions this is for documentation purposes + assert currentOutput != null && currentOutput.hasNext(); + final U value = currentOutput.next(); + while (currentOutput != null && currentOutput.hasNext() == false) { + if (input.hasNext()) { + currentOutput = fn.apply(input.next()); + } else { + currentOutput = null; + } + } + return value; + } + } + } diff --git a/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContentHelper.java b/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContentHelper.java index 282fadac236b..19ebbe350a53 100644 --- a/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContentHelper.java +++ b/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContentHelper.java @@ -62,8 +62,8 @@ public static Iterator field(String name, boolean value) { return Iterators.single(((builder, params) -> builder.field(name, value))); } - public static Iterator array(String name, Iterable iterable) { - return Iterators.concat(ChunkedToXContentHelper.startArray(name), iterable.iterator(), ChunkedToXContentHelper.endArray()); + public static Iterator array(String name, Iterator contents) { + return Iterators.concat(ChunkedToXContentHelper.startArray(name), contents, ChunkedToXContentHelper.endArray()); } public static Iterator wrapWithObject(String name, Iterator iterator) { diff --git a/server/src/main/java/org/elasticsearch/health/Diagnosis.java b/server/src/main/java/org/elasticsearch/health/Diagnosis.java index b3adddd1b6cc..343fe86d8745 100644 --- a/server/src/main/java/org/elasticsearch/health/Diagnosis.java +++ b/server/src/main/java/org/elasticsearch/health/Diagnosis.java @@ -11,6 +11,7 @@ import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.xcontent.ChunkedToXContent; +import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.core.Nullable; import org.elasticsearch.xcontent.ToXContent; @@ -19,9 +20,6 @@ import java.util.Iterator; import java.util.List; import java.util.Objects; -import java.util.Spliterator; -import java.util.Spliterators; -import java.util.stream.StreamSupport; import static org.elasticsearch.health.HealthService.HEALTH_API_ID_PREFIX; @@ -78,7 +76,7 @@ public Resource(Collection nodes) { @Override public Iterator toXContentChunked(ToXContent.Params outerParams) { - Iterator valuesIterator; + final Iterator valuesIterator; if (nodes != null) { valuesIterator = nodes.stream().map(node -> (ToXContent) (builder, params) -> { builder.startObject(); @@ -92,11 +90,7 @@ public Iterator toXContentChunked(ToXContent.Params outerP } else { valuesIterator = values.stream().map(value -> (ToXContent) (builder, params) -> builder.value(value)).iterator(); } - return Iterators.concat( - Iterators.single((ToXContent) (builder, params) -> builder.startArray(type.displayValue)), - valuesIterator, - Iterators.single((builder, params) -> builder.endArray()) - ); + return ChunkedToXContentHelper.array(type.displayValue, valuesIterator); } @Override @@ -148,16 +142,11 @@ public String getUniqueId() { @Override public Iterator toXContentChunked(ToXContent.Params outerParams) { - Iterator resourcesIterator = Collections.emptyIterator(); - if (affectedResources != null && affectedResources.size() > 0) { - resourcesIterator = affectedResources.stream() - .flatMap( - s -> StreamSupport.stream( - Spliterators.spliteratorUnknownSize(s.toXContentChunked(outerParams), Spliterator.ORDERED), - false - ) - ) - .iterator(); + final Iterator resourcesIterator; + if (affectedResources == null) { + resourcesIterator = Collections.emptyIterator(); + } else { + resourcesIterator = Iterators.flatMap(affectedResources.iterator(), s -> s.toXContentChunked(outerParams)); } return Iterators.concat(Iterators.single((ToXContent) (builder, params) -> { builder.startObject(); diff --git a/server/src/main/java/org/elasticsearch/health/HealthIndicatorResult.java b/server/src/main/java/org/elasticsearch/health/HealthIndicatorResult.java index 8ce2818c371c..51985cc7395b 100644 --- a/server/src/main/java/org/elasticsearch/health/HealthIndicatorResult.java +++ b/server/src/main/java/org/elasticsearch/health/HealthIndicatorResult.java @@ -15,9 +15,6 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; -import java.util.Spliterator; -import java.util.Spliterators; -import java.util.stream.StreamSupport; public record HealthIndicatorResult( String name, @@ -29,16 +26,11 @@ public record HealthIndicatorResult( ) implements ChunkedToXContent { @Override public Iterator toXContentChunked(ToXContent.Params outerParams) { - Iterator diagnosisIterator = Collections.emptyIterator(); - if (diagnosisList != null && diagnosisList.isEmpty() == false) { - diagnosisIterator = diagnosisList.stream() - .flatMap( - s -> StreamSupport.stream( - Spliterators.spliteratorUnknownSize(s.toXContentChunked(outerParams), Spliterator.ORDERED), - false - ) - ) - .iterator(); + final Iterator diagnosisIterator; + if (diagnosisList == null) { + diagnosisIterator = Collections.emptyIterator(); + } else { + diagnosisIterator = Iterators.flatMap(diagnosisList.iterator(), s -> s.toXContentChunked(outerParams)); } return Iterators.concat(Iterators.single((ToXContent) (builder, params) -> { builder.startObject(); diff --git a/server/src/main/java/org/elasticsearch/ingest/IngestMetadata.java b/server/src/main/java/org/elasticsearch/ingest/IngestMetadata.java index eff4be93e78a..3816ade205df 100644 --- a/server/src/main/java/org/elasticsearch/ingest/IngestMetadata.java +++ b/server/src/main/java/org/elasticsearch/ingest/IngestMetadata.java @@ -98,7 +98,7 @@ public static IngestMetadata fromXContent(XContentParser parser) throws IOExcept @Override public Iterator toXContentChunked(ToXContent.Params ignored) { - return ChunkedToXContentHelper.array(PIPELINES_FIELD.getPreferredName(), pipelines.values()); + return ChunkedToXContentHelper.array(PIPELINES_FIELD.getPreferredName(), pipelines.values().iterator()); } @Override diff --git a/server/src/main/java/org/elasticsearch/persistent/PersistentTasksCustomMetadata.java b/server/src/main/java/org/elasticsearch/persistent/PersistentTasksCustomMetadata.java index 515540a0d79d..00f7fa031e12 100644 --- a/server/src/main/java/org/elasticsearch/persistent/PersistentTasksCustomMetadata.java +++ b/server/src/main/java/org/elasticsearch/persistent/PersistentTasksCustomMetadata.java @@ -558,7 +558,7 @@ public static NamedDiff readDiffFrom(StreamInput in) throws IOE public Iterator toXContentChunked(ToXContent.Params ignored) { return Iterators.concat( Iterators.single((builder, params) -> builder.field("last_allocation_id", lastAllocationId)), - ChunkedToXContentHelper.array("tasks", tasks.values()) + ChunkedToXContentHelper.array("tasks", tasks.values().iterator()) ); } diff --git a/server/src/test/java/org/elasticsearch/common/collect/IteratorsTests.java b/server/src/test/java/org/elasticsearch/common/collect/IteratorsTests.java index 3a750419c209..8d3bcdd61439 100644 --- a/server/src/test/java/org/elasticsearch/common/collect/IteratorsTests.java +++ b/server/src/test/java/org/elasticsearch/common/collect/IteratorsTests.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; public class IteratorsTests extends ESTestCase { public void testConcatentation() { @@ -153,6 +154,35 @@ public void testArrayIteratorOnNull() { expectThrows(NullPointerException.class, "Unable to iterate over a null array", () -> Iterators.forArray(null)); } + public void testFlatMap() { + final var array = randomIntegerArray(); + assertEmptyIterator(Iterators.flatMap(Iterators.forArray(array), i -> Iterators.concat())); + assertEmptyIterator(Iterators.flatMap(Iterators.concat(), i -> Iterators.single("foo"))); + + final var index = new AtomicInteger(); + Iterators.flatMap(Iterators.forArray(array), Iterators::single) + .forEachRemaining(i -> assertEquals(array[index.getAndIncrement()], i)); + assertEquals(array.length, index.get()); + + index.set(0); + Iterators.flatMap(Iterators.forArray(array), i -> Iterators.forArray(new Integer[] { i, i, i })) + .forEachRemaining(i -> assertEquals(array[(index.getAndIncrement() / 3)], i)); + assertEquals(array.length * 3, index.get()); + + final var input = new ArrayList<>(); + for (int i = 1; i <= 4; i++) { + IntStream.range(0, between(0, 2)).forEachOrdered(ignored -> input.add(0)); + input.add(i); + IntStream.range(0, between(0, 2)).forEachOrdered(ignored -> input.add(0)); + } + + index.set(0); + final var expectedArray = new Integer[] { 0, 0, 1, 0, 1, 2, 0, 1, 2, 3 }; + Iterators.flatMap(input.listIterator(), i -> IntStream.range(0, (int) i).iterator()) + .forEachRemaining(i -> assertEquals(expectedArray[(index.getAndIncrement())], i)); + assertEquals(expectedArray.length, index.get()); + } + private static Integer[] randomIntegerArray() { return Randomness.get().ints(randomIntBetween(0, 1000)).boxed().toArray(Integer[]::new); } From 64813424666684fed960c89103b6dbd5dd87e499 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Wed, 7 Dec 2022 11:02:44 -0500 Subject: [PATCH 192/919] Fix sneaky docs test failure (#91829) This prevents docs files from *starting* with a "response" because when that happens the response is converted to an assertion and appended to the last snippet that was processed. If that last snipper was in a different file then it's very hard to reason about the tests. That goes double because the order we iterate files isn't defined.... Anyway! This adds a guard in the build, removes the offending "response", and reenables the tests that we'd thought we failing here. Closes #91081 --- .../doc/RestTestsFromSnippetsTask.groovy | 6 +++ docs/reference/cluster/nodes-stats.asciidoc | 41 ++++++++----------- .../put-trained-model-vocabulary.asciidoc | 4 +- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/build-tools-internal/src/main/groovy/org/elasticsearch/gradle/internal/doc/RestTestsFromSnippetsTask.groovy b/build-tools-internal/src/main/groovy/org/elasticsearch/gradle/internal/doc/RestTestsFromSnippetsTask.groovy index b9b77da16bd6..eda86355ee30 100644 --- a/build-tools-internal/src/main/groovy/org/elasticsearch/gradle/internal/doc/RestTestsFromSnippetsTask.groovy +++ b/build-tools-internal/src/main/groovy/org/elasticsearch/gradle/internal/doc/RestTestsFromSnippetsTask.groovy @@ -210,6 +210,12 @@ class RestTestsFromSnippetsTask extends SnippetsTask { return } if (snippet.testResponse || snippet.language == 'console-result') { + if (previousTest == null) { + throw new InvalidUserDataException("$snippet: No paired previous test") + } + if (previousTest.path != snippet.path) { + throw new InvalidUserDataException("$snippet: Result can't be first in file") + } response(snippet) return } diff --git a/docs/reference/cluster/nodes-stats.asciidoc b/docs/reference/cluster/nodes-stats.asciidoc index da0402cac171..98e22eaafd84 100644 --- a/docs/reference/cluster/nodes-stats.asciidoc +++ b/docs/reference/cluster/nodes-stats.asciidoc @@ -2679,8 +2679,8 @@ requests. [[cluster-nodes-stats-api-example]] ==== {api-examples-title} -[source,console] --------------------------------------------------- +[source,console,id=nodes-stats-limit] +---- # return just indices GET /_nodes/stats/indices @@ -2689,8 +2689,7 @@ GET /_nodes/stats/os,process # return just process for node with IP address 10.0.0.1 GET /_nodes/10.0.0.1/stats/process --------------------------------------------------- -// TESTRESPONSE[skip:"AwaitsFix https://github.com/elastic/elasticsearch/issues/91081"] +---- All stats can be explicitly requested via `/_nodes/stats/_all` or `/_nodes/stats?metric=_all`. @@ -2698,8 +2697,8 @@ All stats can be explicitly requested via `/_nodes/stats/_all` or You can get information about indices stats on `node`, `indices`, or `shards` level. -[source,console] --------------------------------------------------- +[source,console,id=nodes-stats-indices] +---- # Fielddata summarized by node GET /_nodes/stats/indices/fielddata?fields=field1,field2 @@ -2711,21 +2710,19 @@ GET /_nodes/stats/indices/fielddata?level=shards&fields=field1,field2 # You can use wildcards for field names GET /_nodes/stats/indices/fielddata?fields=field* --------------------------------------------------- -// TESTRESPONSE[skip:"AwaitsFix https://github.com/elastic/elasticsearch/issues/91081"] +---- You can get statistics about search groups for searches executed on this node. -[source,console] --------------------------------------------------- +[source,console,id=nodes-stats-groups] +---- # All groups with all stats GET /_nodes/stats?groups=_all # Some groups from just the indices stats GET /_nodes/stats/indices?groups=foo,bar --------------------------------------------------- -// TESTRESPONSE[skip:"AwaitsFix https://github.com/elastic/elasticsearch/issues/91081"] +---- [[cluster-nodes-stats-ingest-ex]] ===== Retrieve ingest statistics only @@ -2734,25 +2731,23 @@ To return only ingest-related node statistics, set the `` path parameter to `ingest` and use the <> query parameter. -[source,console] --------------------------------------------------- +[source,console,id=nodes-stats-filter-path] +---- GET /_nodes/stats/ingest?filter_path=nodes.*.ingest --------------------------------------------------- -// TESTRESPONSE[skip:"AwaitsFix https://github.com/elastic/elasticsearch/issues/91081"] +---- You can use the `metric` and `filter_path` query parameters to get the same response. -[source,console] --------------------------------------------------- +[source,console,id=nodes-stats-metric-filter-path] +---- GET /_nodes/stats?metric=ingest&filter_path=nodes.*.ingest --------------------------------------------------- +---- To further refine the response, change the `filter_path` value. For example, the following request only returns ingest pipeline statistics. -[source,console] --------------------------------------------------- +[source,console,id=nodes-stats-metric-filter-path-refined] +---- GET /_nodes/stats?metric=ingest&filter_path=nodes.*.ingest.pipelines --------------------------------------------------- -// TESTRESPONSE[skip:"AwaitsFix https://github.com/elastic/elasticsearch/issues/91081"] +---- diff --git a/docs/reference/ml/trained-models/apis/put-trained-model-vocabulary.asciidoc b/docs/reference/ml/trained-models/apis/put-trained-model-vocabulary.asciidoc index c252fcd8f7d5..31a49bae8aad 100644 --- a/docs/reference/ml/trained-models/apis/put-trained-model-vocabulary.asciidoc +++ b/docs/reference/ml/trained-models/apis/put-trained-model-vocabulary.asciidoc @@ -56,7 +56,7 @@ preference. Example: ["f o", "fo o"]. Must be provided for RoBERTa and BART styl The following example shows how to create a model vocabulary for a previously stored trained model configuration. -[source,js] +[source,console] -------------------------------------------------- PUT _ml/trained_models/elastic__distilbert-base-uncased-finetuned-conll03-english/vocabulary { @@ -67,7 +67,7 @@ PUT _ml/trained_models/elastic__distilbert-base-uncased-finetuned-conll03-englis ] } -------------------------------------------------- -// NOTCONSOLE +// TEST[s/\.\.\./"[PAD]"/ skip:TBD] The API returns the following results: From 52a622cdfd4ca8324859faff49839480e500c274 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Wed, 7 Dec 2022 11:21:41 -0500 Subject: [PATCH 193/919] Make the cranky circuit breaker easier to reuse (#92172) This moves the `CrankyCircuitBreaker` into a top level class and documents it so it can be used outside of testing `Aggregator`s. It really is generally useful. It also makes constansts for the error messages it throws. And for the error message thrown by `MockBigArrays`. Just so it's easier to know which one throw what exception. --- .../common/util/MockBigArrays.java | 7 +- .../indices/CrankyCircuitBreakerService.java | 97 +++++++++++++++++++ .../aggregations/AggregatorTestCase.java | 84 ++-------------- .../topmetrics/TopMetricsAggregatorTests.java | 2 +- 4 files changed, 111 insertions(+), 79 deletions(-) create mode 100644 test/framework/src/main/java/org/elasticsearch/indices/CrankyCircuitBreakerService.java diff --git a/test/framework/src/main/java/org/elasticsearch/common/util/MockBigArrays.java b/test/framework/src/main/java/org/elasticsearch/common/util/MockBigArrays.java index 1aa956d4644c..700ee253c218 100644 --- a/test/framework/src/main/java/org/elasticsearch/common/util/MockBigArrays.java +++ b/test/framework/src/main/java/org/elasticsearch/common/util/MockBigArrays.java @@ -49,6 +49,11 @@ public class MockBigArrays extends BigArrays { private static final Logger logger = LogManager.getLogger(MockBigArrays.class); + /** + * Error message thrown by {@link BigArrays} produced with {@link MockBigArrays#MockBigArrays(PageCacheRecycler, ByteSizeValue)}. + */ + public static final String ERROR_MESSAGE = "over test limit"; + /** * Assert that a function returning a {@link Releasable} runs to completion * when allocated a breaker with that breaks when it uses more than {@code max} @@ -667,7 +672,7 @@ public LimitedBreaker(String name, ByteSizeValue max) { public void addEstimateBytesAndMaybeBreak(long bytes, String label) throws CircuitBreakingException { long total = used.addAndGet(bytes); if (total > max.getBytes()) { - throw new CircuitBreakingException("test error", bytes, max.getBytes(), Durability.TRANSIENT); + throw new CircuitBreakingException(ERROR_MESSAGE, bytes, max.getBytes(), Durability.TRANSIENT); } } diff --git a/test/framework/src/main/java/org/elasticsearch/indices/CrankyCircuitBreakerService.java b/test/framework/src/main/java/org/elasticsearch/indices/CrankyCircuitBreakerService.java new file mode 100644 index 000000000000..15ffa52569d0 --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/indices/CrankyCircuitBreakerService.java @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.indices; + +import org.elasticsearch.common.breaker.CircuitBreaker; +import org.elasticsearch.common.breaker.CircuitBreakingException; +import org.elasticsearch.indices.breaker.AllCircuitBreakerStats; +import org.elasticsearch.indices.breaker.CircuitBreakerService; +import org.elasticsearch.indices.breaker.CircuitBreakerStats; +import org.elasticsearch.test.ESTestCase; + +/** + * {@link CircuitBreakerService} that fails one twentieth of the time when you + * add bytes. This is useful to make sure code responds sensibly to circuit + * breaks at unpredictable times. + */ +public class CrankyCircuitBreakerService extends CircuitBreakerService { + /** + * Error message thrown when the breaker randomly trips. + */ + public static final String ERROR_MESSAGE = "cranky breaker"; + + private final CircuitBreaker breaker = new CircuitBreaker() { + @Override + public void circuitBreak(String fieldName, long bytesNeeded) { + + } + + @Override + public void addEstimateBytesAndMaybeBreak(long bytes, String label) throws CircuitBreakingException { + if (ESTestCase.random().nextInt(20) == 0) { + throw new CircuitBreakingException(ERROR_MESSAGE, Durability.PERMANENT); + } + } + + @Override + public void addWithoutBreaking(long bytes) { + + } + + @Override + public long getUsed() { + return 0; + } + + @Override + public long getLimit() { + return 0; + } + + @Override + public double getOverhead() { + return 0; + } + + @Override + public long getTrippedCount() { + return 0; + } + + @Override + public String getName() { + return CircuitBreaker.FIELDDATA; + } + + @Override + public Durability getDurability() { + return null; + } + + @Override + public void setLimitAndOverhead(long limit, double overhead) { + + } + }; + + @Override + public CircuitBreaker getBreaker(String name) { + return breaker; + } + + @Override + public AllCircuitBreakerStats stats() { + return new AllCircuitBreakerStats(new CircuitBreakerStats[] { stats(CircuitBreaker.FIELDDATA) }); + } + + @Override + public CircuitBreakerStats stats(String name) { + return new CircuitBreakerStats(CircuitBreaker.FIELDDATA, -1, -1, 0, 0); + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java b/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java index 26f417cfa889..bcb65192ab48 100644 --- a/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java @@ -103,11 +103,10 @@ import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.indices.CrankyCircuitBreakerService; import org.elasticsearch.indices.IndicesModule; import org.elasticsearch.indices.analysis.AnalysisModule; -import org.elasticsearch.indices.breaker.AllCircuitBreakerStats; import org.elasticsearch.indices.breaker.CircuitBreakerService; -import org.elasticsearch.indices.breaker.CircuitBreakerStats; import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.plugins.SearchPlugin; @@ -467,8 +466,10 @@ protected A searchAndReduc } /** - * This is extracted into a seperate function so that stack traces will indicate if a bad allocation happened in the - * cranky CB run or the happy path run. + * Run an aggregation test against the {@link CrankyCircuitBreakerService} + * which fails randomly. This is extracted into a separate function so that + * stack traces will indicate if a bad allocation happened in the cranky CB + * run or the happy path run. */ private void runWithCrankyCircuitBreaker(IndexSettings indexSettings, IndexSearcher searcher, AggTestConfig aggTestConfig) throws IOException { @@ -477,7 +478,8 @@ private void runWithCrankyCircuitBreaker(IndexSettings indexSettings, IndexSearc try { searchAndReduce(indexSettings, searcher, crankyService, aggTestConfig); } catch (CircuitBreakingException e) { - // expected + // Circuit breaks from the cranky breaker are expected - it randomly fails, after all + assertThat(e.getMessage(), equalTo(CrankyCircuitBreakerService.ERROR_MESSAGE)); } catch (IOException e) { throw e; } @@ -1432,78 +1434,6 @@ public List getAggregations() { } } - private static class CrankyCircuitBreakerService extends CircuitBreakerService { - - private final CircuitBreaker breaker = new CircuitBreaker() { - @Override - public void circuitBreak(String fieldName, long bytesNeeded) { - - } - - @Override - public void addEstimateBytesAndMaybeBreak(long bytes, String label) throws CircuitBreakingException { - if (random().nextInt(20) == 0) { - throw new CircuitBreakingException("fake error", Durability.PERMANENT); - } - } - - @Override - public void addWithoutBreaking(long bytes) { - - } - - @Override - public long getUsed() { - return 0; - } - - @Override - public long getLimit() { - return 0; - } - - @Override - public double getOverhead() { - return 0; - } - - @Override - public long getTrippedCount() { - return 0; - } - - @Override - public String getName() { - return CircuitBreaker.FIELDDATA; - } - - @Override - public Durability getDurability() { - return null; - } - - @Override - public void setLimitAndOverhead(long limit, double overhead) { - - } - }; - - @Override - public CircuitBreaker getBreaker(String name) { - return breaker; - } - - @Override - public AllCircuitBreakerStats stats() { - return new AllCircuitBreakerStats(new CircuitBreakerStats[] { stats(CircuitBreaker.FIELDDATA) }); - } - - @Override - public CircuitBreakerStats stats(String name) { - return new CircuitBreakerStats(CircuitBreaker.FIELDDATA, -1, -1, 0, 0); - } - } - public record AggTestConfig( Query query, AggregationBuilder builder, diff --git a/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/topmetrics/TopMetricsAggregatorTests.java b/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/topmetrics/TopMetricsAggregatorTests.java index e65c451f630f..35db1a0d93ab 100644 --- a/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/topmetrics/TopMetricsAggregatorTests.java +++ b/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/topmetrics/TopMetricsAggregatorTests.java @@ -435,7 +435,7 @@ public void testTonsOfBucketsTriggersBreaker() throws IOException { } } CircuitBreakingException e = expectThrows(CircuitBreakingException.class, () -> leaf.collect(0, bucketThatBreaks)); - assertThat(e.getMessage(), equalTo("test error")); + assertThat(e.getMessage(), equalTo(MockBigArrays.ERROR_MESSAGE)); assertThat(e.getByteLimit(), equalTo(max.getBytes())); assertThat(e.getBytesWanted(), equalTo(5872L)); } From b8780d3b2403f88939a9100418e6feb730ed6c0b Mon Sep 17 00:00:00 2001 From: Keith Massey Date: Wed, 7 Dec 2022 11:55:18 -0600 Subject: [PATCH 194/919] Align all usages of protobuf to be 3.21.9 (#92123) Updating repository-hdfs, repository-gcs, and vector-tile to all use the same more recent protobuf. --- build-tools-internal/version.properties | 1 + docs/changelog/92123.yaml | 5 +++++ gradle/verification-metadata.xml | 22 ++++++---------------- modules/repository-gcs/build.gradle | 4 ++-- plugins/repository-hdfs/build.gradle | 5 ++++- x-pack/plugin/vector-tile/build.gradle | 4 ++-- 6 files changed, 20 insertions(+), 21 deletions(-) create mode 100644 docs/changelog/92123.yaml diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 5ee8391c28e2..601b6ec9a652 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -36,6 +36,7 @@ httpcore = 4.4.13 httpasyncclient = 4.1.5 commonslogging = 1.2 commonscodec = 1.15 +protobuf = 3.21.9 # test dependencies randomizedrunner = 2.8.0 diff --git a/docs/changelog/92123.yaml b/docs/changelog/92123.yaml new file mode 100644 index 000000000000..d124315711f2 --- /dev/null +++ b/docs/changelog/92123.yaml @@ -0,0 +1,5 @@ +pr: 92123 +summary: Align all usages of protobuf to be 3.21.9 +area: Snapshot/Restore +type: upgrade +issues: [] diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 6928a0fd8231..d897088a9c4b 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -689,24 +689,14 @@ - - - + + + - - - - - - - - - - - - - + + + diff --git a/modules/repository-gcs/build.gradle b/modules/repository-gcs/build.gradle index dce2513fb3b8..7b0f62b54d6d 100644 --- a/modules/repository-gcs/build.gradle +++ b/modules/repository-gcs/build.gradle @@ -37,8 +37,8 @@ dependencies { api 'com.google.api:api-common:2.2.1' api 'com.google.api:gax:2.0.0' api 'org.threeten:threetenbp:1.5.1' - api 'com.google.protobuf:protobuf-java-util:3.17.3' - api 'com.google.protobuf:protobuf-java:3.21.1' + api "com.google.protobuf:protobuf-java-util:${versions.protobuf}" + api "com.google.protobuf:protobuf-java:${versions.protobuf}" api 'com.google.code.gson:gson:2.8.9' api 'com.google.api.grpc:proto-google-common-protos:2.3.2' api 'com.google.api.grpc:proto-google-iam-v1:1.0.14' diff --git a/plugins/repository-hdfs/build.gradle b/plugins/repository-hdfs/build.gradle index 6cd9053f2d47..5db8373dca8e 100644 --- a/plugins/repository-hdfs/build.gradle +++ b/plugins/repository-hdfs/build.gradle @@ -47,7 +47,7 @@ dependencies { } runtimeOnly "org.apache.hadoop:hadoop-client-runtime:${versions.hadoop}" implementation "org.apache.hadoop:hadoop-hdfs:${versions.hadoop}" - api 'com.google.protobuf:protobuf-java:3.4.0' + api "com.google.protobuf:protobuf-java:${versions.protobuf}" api "commons-logging:commons-logging:${versions.commonslogging}" api "org.apache.logging.log4j:log4j-1.2-api:${versions.log4j}" api 'commons-cli:commons-cli:1.2' @@ -304,8 +304,11 @@ tasks.named("thirdPartyAudit").configure { ignoreMissingClasses() ignoreViolations( // internal java api: sun.misc.Unsafe + 'com.google.protobuf.MessageSchema', 'com.google.protobuf.UnsafeUtil', 'com.google.protobuf.UnsafeUtil$1', + 'com.google.protobuf.UnsafeUtil$Android32MemoryAccessor', + 'com.google.protobuf.UnsafeUtil$Android64MemoryAccessor', 'com.google.protobuf.UnsafeUtil$JvmMemoryAccessor', 'com.google.protobuf.UnsafeUtil$MemoryAccessor', 'org.apache.hadoop.hdfs.server.datanode.checker.AbstractFuture$UnsafeAtomicHelper', diff --git a/x-pack/plugin/vector-tile/build.gradle b/x-pack/plugin/vector-tile/build.gradle index 46c3e2830ffb..a8a72080736a 100644 --- a/x-pack/plugin/vector-tile/build.gradle +++ b/x-pack/plugin/vector-tile/build.gradle @@ -29,11 +29,11 @@ dependencies { testImplementation(testArtifact(project(xpackModule('core')))) compileOnly "org.locationtech.jts:jts-core:${versions.jts}" api "com.wdtinc:mapbox-vector-tile:3.1.0" - api "com.google.protobuf:protobuf-java:3.16.3" + api "com.google.protobuf:protobuf-java:${versions.protobuf}" runtimeOnly("org.slf4j:slf4j-api:${versions.slf4j}") runtimeOnly("org.apache.logging.log4j:log4j-slf4j-impl:${versions.log4j}") javaRestTestImplementation("com.wdtinc:mapbox-vector-tile:3.1.0") - javaRestTestImplementation("com.google.protobuf:protobuf-java:3.16.3") + javaRestTestImplementation("com.google.protobuf:protobuf-java:${versions.protobuf}") } testClusters.configureEach { From 9d1f525685c9667ef3f94df9fde134537fa1332c Mon Sep 17 00:00:00 2001 From: Nikola Grcevski <6207777+grcevski@users.noreply.github.com> Date: Wed, 7 Dec 2022 14:15:35 -0500 Subject: [PATCH 195/919] Don't leak ansi console dir handle (#92122) Mostly harmless, but we should close properly the directory list file stream, otherwise it leaks the file handle. I found this while investigating an issue with CRaC/CRIU snapshots. --- .../main/java/org/elasticsearch/bootstrap/ConsoleLoader.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/bootstrap/ConsoleLoader.java b/server/src/main/java/org/elasticsearch/bootstrap/ConsoleLoader.java index 8b0d914e2da3..955e4439d1f7 100644 --- a/server/src/main/java/org/elasticsearch/bootstrap/ConsoleLoader.java +++ b/server/src/main/java/org/elasticsearch/bootstrap/ConsoleLoader.java @@ -53,9 +53,8 @@ static Supplier buildConsoleLoader(ClassLoader classLoader) { private static ClassLoader buildClassLoader(Environment env) { final Path libDir = env.libFile().resolve("tools").resolve("ansi-console"); - try { - final URL[] urls = Files.list(libDir) - .filter(each -> each.getFileName().toString().endsWith(".jar")) + try (var libDirFilesStream = Files.list(libDir)) { + final URL[] urls = libDirFilesStream.filter(each -> each.getFileName().toString().endsWith(".jar")) .map(ConsoleLoader::pathToURL) .toArray(URL[]::new); From 9e3dde8298364bd72bec3fcaeab48172bdc80889 Mon Sep 17 00:00:00 2001 From: William Brafford Date: Wed, 7 Dec 2022 14:52:17 -0500 Subject: [PATCH 196/919] Mute flaky windows tests (#92214) Test failure report: #91609 --- .../elasticsearch/plugins/UberModuleClassLoaderTests.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/plugins/UberModuleClassLoaderTests.java b/server/src/test/java/org/elasticsearch/plugins/UberModuleClassLoaderTests.java index 45544a471584..6359b9425c93 100644 --- a/server/src/test/java/org/elasticsearch/plugins/UberModuleClassLoaderTests.java +++ b/server/src/test/java/org/elasticsearch/plugins/UberModuleClassLoaderTests.java @@ -8,6 +8,7 @@ package org.elasticsearch.plugins; +import org.apache.lucene.util.Constants; import org.elasticsearch.common.Strings; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.compiler.InMemoryJavaCompiler; @@ -465,8 +466,9 @@ public static String demo() { JarUtils.createJarWithEntries(jar, jarEntries); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/91609") public void testServiceLoadingWithOptionalDependencies() throws Exception { - + assumeFalse("Tests frequently fail on Windows", Constants.WINDOWS); try (UberModuleClassLoader loader = getServiceTestLoader(true)) { Class serviceCallerClass = loader.loadClass("q.caller.ServiceCaller"); @@ -483,8 +485,9 @@ public void testServiceLoadingWithOptionalDependencies() throws Exception { } } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/91609") public void testServiceLoadingWithoutOptionalDependencies() throws Exception { - + assumeFalse("Tests frequently fail on Windows", Constants.WINDOWS); try (UberModuleClassLoader loader = getServiceTestLoader(false)) { Class serviceCallerClass = loader.loadClass("q.caller.ServiceCaller"); From 60dc2d381600c139726735af237c7fa335007528 Mon Sep 17 00:00:00 2001 From: Jake Landis Date: Wed, 7 Dec 2022 14:15:57 -0600 Subject: [PATCH 197/919] Minor additions for support SAN/dnsName for restricted trust (#91983) A follow up to #91946 with the minor requested changes. Changes included here are: * reuse of variables * additional unit test * convert to use enumeration instead of set of strings --- .../common/ssl/SslConfigurationKeys.java | 6 ++ .../common/ssl/SslConfigurationLoader.java | 29 +++++++-- .../elasticsearch/common/ssl/X509Field.java | 50 ++++++++++++++ .../common/ssl/X509FieldTests.java | 28 ++++++++ .../xpack/core/ssl/RestrictedTrustConfig.java | 8 +-- .../core/ssl/RestrictedTrustManager.java | 16 ++--- .../core/ssl/SSLConfigurationSettings.java | 30 +++------ .../xpack/core/ssl/SslSettingsLoader.java | 24 +++---- .../core/ssl/RestrictedTrustConfigTests.java | 9 ++- .../core/ssl/RestrictedTrustManagerTests.java | 65 +++++++++++++++++-- .../ssl/SSLConfigurationSettingsTests.java | 12 ++-- .../certs/simple/nodes/restricted.trust.crt | 21 ++++++ 12 files changed, 225 insertions(+), 73 deletions(-) create mode 100644 libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/X509Field.java create mode 100644 libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/X509FieldTests.java create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/restricted.trust.crt diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationKeys.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationKeys.java index 7884a417c39c..83dbd919a60e 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationKeys.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationKeys.java @@ -71,6 +71,12 @@ public class SslConfigurationKeys { */ public static final String TRUSTSTORE_ALGORITHM = "truststore.algorithm"; + /** + * The fields from the X509 certificate used for restricted trust. Internationally omitted from the list of setting returned by methods + * in this class. This is not a general purpose ssl configuration. + */ + public static final String TRUST_RESTRICTIONS_X509_FIELDS = "trust_restrictions.x509_fields"; + // Key Management // -- Keystore /** diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationLoader.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationLoader.java index 0dd72f4f94f1..38e7aa124b5e 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationLoader.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslConfigurationLoader.java @@ -8,12 +8,15 @@ package org.elasticsearch.common.ssl; +import org.elasticsearch.core.Nullable; + import java.nio.file.Path; import java.security.KeyStore; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -42,6 +45,7 @@ import static org.elasticsearch.common.ssl.SslConfigurationKeys.TRUSTSTORE_PATH; import static org.elasticsearch.common.ssl.SslConfigurationKeys.TRUSTSTORE_SECURE_PASSWORD; import static org.elasticsearch.common.ssl.SslConfigurationKeys.TRUSTSTORE_TYPE; +import static org.elasticsearch.common.ssl.SslConfigurationKeys.TRUST_RESTRICTIONS_X509_FIELDS; import static org.elasticsearch.common.ssl.SslConfigurationKeys.VERIFICATION_MODE; /** @@ -115,6 +119,7 @@ public abstract class SslConfigurationLoader { static final List DEFAULT_CIPHERS = JDK12_CIPHERS; private static final char[] EMPTY_PASSWORD = new char[0]; + public static final List GLOBAL_DEFAULT_RESTRICTED_TRUST_FIELDS = List.of(X509Field.SAN_OTHERNAME_COMMONNAME); private final String settingPrefix; @@ -124,6 +129,7 @@ public abstract class SslConfigurationLoader { private SslClientAuthenticationMode defaultClientAuth; private List defaultCiphers; private List defaultProtocols; + private List defaultRestrictedTrustFields; private Function keyStoreFilter; @@ -147,6 +153,7 @@ public SslConfigurationLoader(String settingPrefix) { this.defaultClientAuth = SslClientAuthenticationMode.OPTIONAL; this.defaultProtocols = DEFAULT_PROTOCOLS; this.defaultCiphers = DEFAULT_CIPHERS; + this.defaultRestrictedTrustFields = GLOBAL_DEFAULT_RESTRICTED_TRUST_FIELDS; } /** @@ -204,6 +211,10 @@ public void setKeyStoreFilter(Function keyStoreFilter) { this.keyStoreFilter = keyStoreFilter; } + public void setDefaultRestrictedTrustFields(List x509Fields) { + this.defaultRestrictedTrustFields = x509Fields; + } + /** * Clients of this class should implement this method to determine whether there are any settings for a given prefix. * This is used to populate {@link SslConfiguration#explicitlyConfigured()}. @@ -255,9 +266,14 @@ public SslConfiguration load(Path basePath) { final List ciphers = resolveListSetting(CIPHERS, Function.identity(), defaultCiphers); final SslVerificationMode verificationMode = resolveSetting(VERIFICATION_MODE, SslVerificationMode::parse, defaultVerificationMode); final SslClientAuthenticationMode clientAuth = resolveSetting(CLIENT_AUTH, SslClientAuthenticationMode::parse, defaultClientAuth); + final List trustRestrictionsX509Fields = resolveListSetting( + TRUST_RESTRICTIONS_X509_FIELDS, + X509Field::parseForRestrictedTrust, + defaultRestrictedTrustFields + ); final SslKeyConfig keyConfig = buildKeyConfig(basePath); - final SslTrustConfig trustConfig = buildTrustConfig(basePath, verificationMode, keyConfig); + final SslTrustConfig trustConfig = buildTrustConfig(basePath, verificationMode, keyConfig, Set.copyOf(trustRestrictionsX509Fields)); if (protocols == null || protocols.isEmpty()) { throw new SslConfigException("no protocols configured in [" + settingPrefix + PROTOCOLS + "]"); @@ -278,7 +294,12 @@ public SslConfiguration load(Path basePath) { ); } - protected SslTrustConfig buildTrustConfig(Path basePath, SslVerificationMode verificationMode, SslKeyConfig keyConfig) { + protected SslTrustConfig buildTrustConfig( + Path basePath, + SslVerificationMode verificationMode, + SslKeyConfig keyConfig, + @Nullable Set restrictedTrustFields + ) { final List certificateAuthorities = resolveListSetting(CERTIFICATE_AUTHORITIES, Function.identity(), null); final String trustStorePath = resolveSetting(TRUSTSTORE_PATH, Function.identity(), null); @@ -355,10 +376,6 @@ protected Path resolvePath(String settingKey, Path basePath) { return resolveSetting(settingKey, basePath::resolve, null); } - protected List resolveList(String settingKey, List defaultList) { - return resolveListSetting(settingKey, Function.identity(), defaultList); - } - private String expandSettingKey(String key) { return settingPrefix + key; } diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/X509Field.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/X509Field.java new file mode 100644 index 000000000000..64de9adb8c22 --- /dev/null +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/X509Field.java @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +package org.elasticsearch.common.ssl; + +import java.util.EnumSet; +import java.util.stream.Collectors; + +/** + * An enumeration for referencing parts of an X509 certificate by a canonical string value. + */ +public enum X509Field { + + SAN_OTHERNAME_COMMONNAME("subjectAltName.otherName.commonName", true), + SAN_DNS("subjectAltName.dnsName", true); + + private final String configValue; + private final boolean supportedForRestrictedTrust; + + X509Field(String configValue, boolean supportedForRestrictedTrust) { + this.configValue = configValue; + this.supportedForRestrictedTrust = supportedForRestrictedTrust; + } + + @Override + public String toString() { + return configValue; + } + + public static X509Field parseForRestrictedTrust(String s) { + return EnumSet.allOf(X509Field.class) + .stream() + .filter(v -> v.supportedForRestrictedTrust) + .filter(v -> v.configValue.equalsIgnoreCase(s)) + .findFirst() + .orElseThrow(() -> { + throw new SslConfigException( + s + + " is not a supported x509 field for trust restrictions. " + + "Recognised values are [" + + EnumSet.allOf(X509Field.class).stream().map(e -> e.configValue).collect(Collectors.toSet()) + + "]" + ); + }); + } +} diff --git a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/X509FieldTests.java b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/X509FieldTests.java new file mode 100644 index 000000000000..a0de14aec887 --- /dev/null +++ b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/X509FieldTests.java @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.common.ssl; + +import org.elasticsearch.test.ESTestCase; + +import static org.elasticsearch.common.ssl.X509Field.SAN_DNS; +import static org.elasticsearch.common.ssl.X509Field.SAN_OTHERNAME_COMMONNAME; +import static org.hamcrest.Matchers.containsString; + +public class X509FieldTests extends ESTestCase { + + public void testParseForRestrictedTrust() { + assertEquals(SAN_OTHERNAME_COMMONNAME, X509Field.parseForRestrictedTrust("subjectAltName.otherName.commonName")); + assertEquals(SAN_DNS, X509Field.parseForRestrictedTrust("subjectAltName.dnsName")); + SslConfigException exception = expectThrows(SslConfigException.class, () -> X509Field.parseForRestrictedTrust("foo.bar")); + assertThat(exception.getMessage(), containsString("foo.bar")); + assertThat(exception.getMessage(), containsString("is not a supported x509 field for trust restrictions")); + assertThat(exception.getMessage(), containsString(SAN_OTHERNAME_COMMONNAME.toString())); + assertThat(exception.getMessage(), containsString(SAN_DNS.toString())); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustConfig.java index 9afcafeb032d..8629ff93170f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustConfig.java @@ -10,6 +10,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.ssl.SslTrustConfig; import org.elasticsearch.common.ssl.StoredCertificate; +import org.elasticsearch.common.ssl.X509Field; import org.elasticsearch.common.util.CollectionUtils; import java.io.IOException; @@ -29,14 +30,11 @@ public final class RestrictedTrustConfig implements SslTrustConfig { private static final String RESTRICTIONS_KEY_SUBJECT_NAME = "trust.subject_name"; - public static final String SAN_OTHER_COMMON = "subjectAltName.otherName.commonName"; - public static final String SAN_DNS = "subjectAltName.dnsName"; - static final Set SUPPORTED_X_509_FIELDS = Set.of(SAN_OTHER_COMMON, SAN_DNS); private final Path groupConfigPath; private final SslTrustConfig delegate; - private final Set configuredX509Fields; + private final Set configuredX509Fields; - RestrictedTrustConfig(Path groupConfigPath, Set configuredX509Fields, SslTrustConfig delegate) { + RestrictedTrustConfig(Path groupConfigPath, Set configuredX509Fields, SslTrustConfig delegate) { this.configuredX509Fields = configuredX509Fields; this.groupConfigPath = Objects.requireNonNull(groupConfigPath); this.delegate = Objects.requireNonNull(delegate); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManager.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManager.java index 3b93dc7ed429..f79184c58ce3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManager.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManager.java @@ -9,6 +9,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.common.ssl.DerParser; +import org.elasticsearch.common.ssl.X509Field; import java.io.IOException; import java.net.Socket; @@ -19,7 +20,6 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -30,8 +30,6 @@ import javax.net.ssl.X509ExtendedTrustManager; import static org.elasticsearch.core.Strings.format; -import static org.elasticsearch.xpack.core.ssl.RestrictedTrustConfig.SAN_DNS; -import static org.elasticsearch.xpack.core.ssl.RestrictedTrustConfig.SAN_OTHER_COMMON; /** * An X509 trust manager that only trusts connections from a restricted set of predefined network entities (nodes, clients, etc). @@ -55,12 +53,12 @@ public final class RestrictedTrustManager extends X509ExtendedTrustManager { private final X509ExtendedTrustManager delegate; private final CertificateTrustRestrictions trustRestrictions; - private final Set x509Fields; + private final Set x509Fields; - public RestrictedTrustManager(X509ExtendedTrustManager delegate, CertificateTrustRestrictions restrictions, Set x509Fields) { + public RestrictedTrustManager(X509ExtendedTrustManager delegate, CertificateTrustRestrictions restrictions, Set x509Fields) { this.delegate = delegate; this.trustRestrictions = restrictions; - this.x509Fields = x509Fields.stream().map(s -> s.toLowerCase(Locale.ROOT)).collect(Collectors.toSet()); + this.x509Fields = x509Fields; logger.debug("Configured with trust restrictions: [{}]", restrictions); logger.debug("Configured with x509 fields: [{}]", x509Fields); } @@ -157,7 +155,7 @@ private boolean verifyCertificateNames(Set names) { private Set readX509Certificate(X509Certificate certificate) throws CertificateParsingException { Collection> sans = getSubjectAlternativeNames(certificate); Set values = new HashSet<>(); - if (x509Fields.contains(SAN_DNS.toLowerCase(Locale.ROOT))) { + if (x509Fields.contains(X509Field.SAN_DNS)) { Set dnsNames = sans.stream() .filter(pair -> ((Integer) pair.get(0)).intValue() == SAN_CODE_DNS) .map(pair -> pair.get(1)) @@ -166,8 +164,8 @@ private Set readX509Certificate(X509Certificate certificate) throws Cert .collect(Collectors.toSet()); values.addAll(dnsNames); } - if (x509Fields.contains(SAN_OTHER_COMMON.toLowerCase(Locale.ROOT))) { - Set otherNames = getSubjectAlternativeNames(certificate).stream() + if (x509Fields.contains(X509Field.SAN_OTHERNAME_COMMONNAME)) { + Set otherNames = sans.stream() .filter(pair -> ((Integer) pair.get(0)).intValue() == SAN_CODE_OTHERNAME) .map(pair -> pair.get(1)) .map(value -> decodeDerValue((byte[]) value, certificate)) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationSettings.java index 5fc7f0eb2709..9ceec66c6422 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationSettings.java @@ -12,9 +12,9 @@ import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.ssl.SslClientAuthenticationMode; -import org.elasticsearch.common.ssl.SslConfigException; import org.elasticsearch.common.ssl.SslConfigurationKeys; import org.elasticsearch.common.ssl.SslVerificationMode; +import org.elasticsearch.common.ssl.X509Field; import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.xpack.core.security.authc.RealmConfig; @@ -29,6 +29,8 @@ import javax.net.ssl.TrustManagerFactory; +import static org.elasticsearch.common.ssl.SslConfigurationLoader.GLOBAL_DEFAULT_RESTRICTED_TRUST_FIELDS; + /** * Bridges SSLConfiguration into the {@link Settings} framework, using {@link Setting} objects. */ @@ -44,7 +46,7 @@ public class SSLConfigurationSettings { final Setting truststoreAlgorithm; final Setting> truststoreType; final Setting> trustRestrictionsPath; - final Setting> trustRestrictionsX509Fields; + final Setting> trustRestrictionsX509Fields; final Setting> caPaths; final Setting> clientAuth; final Setting> verificationMode; @@ -157,30 +159,16 @@ public class SSLConfigurationSettings { TRUST_RESTRICTIONS_PATH_TEMPLATE ); - public static final Function>> TRUST_RESTRICTIONS_X509_FIELDS_TEMPLATE = key -> Setting.listSetting( + public static final Function>> TRUST_RESTRICTIONS_X509_FIELDS_TEMPLATE = key -> Setting.listSetting( key, - List.of("subjectAltName.otherName.commonName"), - s -> { - RestrictedTrustConfig.SUPPORTED_X_509_FIELDS.stream() - .filter(v -> v.equalsIgnoreCase(s)) - .findAny() - .ifPresentOrElse(v -> {}, () -> { - throw new SslConfigException( - s - + " is not a supported x509 field for trust restrictions. " - + "Recognised values are [" - + String.join(",", RestrictedTrustConfig.SUPPORTED_X_509_FIELDS) - + "]" - ); - }); - return s; - }, + GLOBAL_DEFAULT_RESTRICTED_TRUST_FIELDS.stream().map(X509Field::toString).collect(Collectors.toList()), + X509Field::parseForRestrictedTrust, Property.NodeScope, Property.Filtered ); - public static final SslSetting> TRUST_RESTRICTIONS_X509_FIELDS = SslSetting.setting( - "trust_restrictions.x509_fields", + public static final SslSetting> TRUST_RESTRICTIONS_X509_FIELDS = SslSetting.setting( + SslConfigurationKeys.TRUST_RESTRICTIONS_X509_FIELDS, TRUST_RESTRICTIONS_X509_FIELDS_TEMPLATE ); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SslSettingsLoader.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SslSettingsLoader.java index 820107f78976..2196558cf990 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SslSettingsLoader.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SslSettingsLoader.java @@ -18,6 +18,7 @@ import org.elasticsearch.common.ssl.SslKeyConfig; import org.elasticsearch.common.ssl.SslTrustConfig; import org.elasticsearch.common.ssl.SslVerificationMode; +import org.elasticsearch.common.ssl.X509Field; import org.elasticsearch.core.Nullable; import org.elasticsearch.env.Environment; @@ -29,9 +30,6 @@ import java.util.function.Function; import java.util.stream.Collectors; -import static org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings.TRUST_RESTRICTIONS_X509_FIELDS; -import static org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings.TRUST_RESTRICTIONS_X509_FIELDS_TEMPLATE; - /** * A configuration loader for SSL Settings */ @@ -115,22 +113,18 @@ protected char[] getSecureSetting(String key) { } @Override - protected SslTrustConfig buildTrustConfig(Path basePath, SslVerificationMode verificationMode, SslKeyConfig keyConfig) { - final SslTrustConfig trustConfig = super.buildTrustConfig(basePath, verificationMode, keyConfig); + protected SslTrustConfig buildTrustConfig( + Path basePath, + SslVerificationMode verificationMode, + SslKeyConfig keyConfig, + Set restrictedTrustFields + ) { + final SslTrustConfig trustConfig = super.buildTrustConfig(basePath, verificationMode, keyConfig, null); final Path trustRestrictions = super.resolvePath("trust_restrictions.path", basePath); if (trustRestrictions == null) { return trustConfig; } - return new RestrictedTrustConfig( - trustRestrictions, - Set.copyOf( - super.resolveList( - TRUST_RESTRICTIONS_X509_FIELDS.rawSetting().getKey(), - TRUST_RESTRICTIONS_X509_FIELDS_TEMPLATE.apply("").getDefault(settings) - ) - ), - trustConfig - ); + return new RestrictedTrustConfig(trustRestrictions, restrictedTrustFields, trustConfig); } public SslConfiguration load(Environment env) { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustConfigTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustConfigTests.java index bbd5cce7e757..f6033ff6d929 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustConfigTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustConfigTests.java @@ -9,6 +9,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.ssl.SslTrustConfig; import org.elasticsearch.common.ssl.StoredCertificate; +import org.elasticsearch.common.ssl.X509Field; import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.test.ESTestCase; @@ -23,8 +24,6 @@ import javax.net.ssl.X509ExtendedTrustManager; -import static org.elasticsearch.xpack.core.ssl.RestrictedTrustConfig.SAN_OTHER_COMMON; - public class RestrictedTrustConfigTests extends ESTestCase { public void testDelegationOfFilesToMonitor() throws Exception { @@ -71,7 +70,11 @@ public int hashCode() { } }; - final RestrictedTrustConfig restrictedTrustConfig = new RestrictedTrustConfig(groupConfigPath, Set.of(SAN_OTHER_COMMON), delegate); + final RestrictedTrustConfig restrictedTrustConfig = new RestrictedTrustConfig( + groupConfigPath, + Set.of(X509Field.SAN_OTHERNAME_COMMONNAME), + delegate + ); Collection filesToMonitor = restrictedTrustConfig.getDependentFiles(); List expectedPathList = new ArrayList<>(otherFiles); expectedPathList.add(groupConfigPath); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManagerTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManagerTests.java index f933216bd853..66fb5c7642f2 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManagerTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManagerTests.java @@ -6,6 +6,7 @@ */ package org.elasticsearch.xpack.core.ssl; +import org.elasticsearch.common.ssl.X509Field; import org.elasticsearch.test.ESTestCase; import org.hamcrest.Description; import org.hamcrest.TypeSafeMatcher; @@ -34,16 +35,13 @@ import javax.net.ssl.X509ExtendedTrustManager; -import static org.elasticsearch.xpack.core.ssl.RestrictedTrustConfig.SAN_DNS; -import static org.elasticsearch.xpack.core.ssl.RestrictedTrustConfig.SAN_OTHER_COMMON; - public class RestrictedTrustManagerTests extends ESTestCase { private X509ExtendedTrustManager baseTrustManager; private Map certificates; private int numberOfClusters; private int numberOfNodes; - private List fields; + private List fields; @Before public void readCertificates() throws GeneralSecurityException, IOException { @@ -97,7 +95,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO numberOfClusters = scaledRandomIntBetween(2, 8); numberOfNodes = scaledRandomIntBetween(2, 8); - fields = randomNonEmptySubsetOf(Set.of(SAN_OTHER_COMMON, SAN_DNS)); + fields = randomNonEmptySubsetOf(Set.of(X509Field.SAN_OTHERNAME_COMMONNAME, X509Field.SAN_DNS)); } public void testTrustsOnlyNameDns() throws Exception { @@ -110,7 +108,7 @@ public void testTrustsOnlyNameDns() throws Exception { List.of("localhost", "localhost.localdomain", "localhost4", "localhost4.localdomain4", "localhost6", "localhost6.localdomain6") ); final CertificateTrustRestrictions restrictions = new CertificateTrustRestrictions(validDnsNames); - final RestrictedTrustManager trustManager = new RestrictedTrustManager(baseTrustManager, restrictions, Set.of(SAN_DNS)); + final RestrictedTrustManager trustManager = new RestrictedTrustManager(baseTrustManager, restrictions, Set.of(X509Field.SAN_DNS)); assertTrusted(trustManager, "onlyDns"); } @@ -121,10 +119,63 @@ public void testTrustsOnlyNameOther() throws Exception { assertTrue(certs[0].getSubjectAlternativeNames().stream().filter(pair -> (Integer) pair.get(0) == 2).findAny().isEmpty()); certificates.put("onlyOtherName", certs); final CertificateTrustRestrictions restrictions = new CertificateTrustRestrictions(List.of("node.trusted")); - final RestrictedTrustManager trustManager = new RestrictedTrustManager(baseTrustManager, restrictions, Set.of(SAN_OTHER_COMMON)); + final RestrictedTrustManager trustManager = new RestrictedTrustManager( + baseTrustManager, + restrictions, + Set.of(X509Field.SAN_OTHERNAME_COMMONNAME) + ); assertTrusted(trustManager, "onlyOtherName"); } + public void testTrustWithVariedFields() throws Exception { + final Path cert = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/restricted.trust.crt"); + baseTrustManager = CertParsingUtils.getTrustManagerFromPEM(List.of(cert)); + X509Certificate[] certs = CertParsingUtils.readX509Certificates(Collections.singletonList(cert)); + assertTrue(certs[0].getSubjectAlternativeNames().stream().anyMatch(pair -> (Integer) pair.get(0) == 0)); // othername + assertTrue(certs[0].getSubjectAlternativeNames().stream().anyMatch(pair -> (Integer) pair.get(0) == 2)); // dns + assertTrue(certs[0].getSubjectAlternativeNames().stream().anyMatch(pair -> (Integer) pair.get(0) == 7)); // ip + certificates.put("varied", certs); + // othername/common name -> "instance03.cluster02.elasticsearch" + // dns -> "search.example.com" + // ip -> 50.100.150.200 + String failureMatchDns = ".*subjectAltName\\.dnsName.*search\\.example\\.com.*does not match.*"; + String failureMatchCommon = ".*subjectAltName\\.otherName\\.commonName.*instance03\\.cluster02\\.elasticsearch.*does not match.*"; + + // instance03.cluster02.elasticsearch -> *.cluster02.elasticsearch + CertificateTrustRestrictions restrictions = new CertificateTrustRestrictions(List.of("*.cluster02.elasticsearch")); + RestrictedTrustManager trustManager = new RestrictedTrustManager( + baseTrustManager, + restrictions, + Set.of(X509Field.SAN_OTHERNAME_COMMONNAME) + ); + assertTrusted(trustManager, "varied"); + + // search.example.com -> *.cluster02.elasticsearch + restrictions = new CertificateTrustRestrictions(List.of("*.cluster02.elasticsearch")); + trustManager = new RestrictedTrustManager(baseTrustManager, restrictions, Set.of(X509Field.SAN_DNS)); + assertNotValid(trustManager, "varied", failureMatchDns); // <-- this one + + // search.example.com -> *.example.com + restrictions = new CertificateTrustRestrictions(List.of("*.example.com")); + trustManager = new RestrictedTrustManager(baseTrustManager, restrictions, Set.of(X509Field.SAN_DNS)); + assertTrusted(trustManager, "varied"); + + // instance03.cluster02.elasticsearch -> *.example.com + restrictions = new CertificateTrustRestrictions(List.of("*.example.com")); + trustManager = new RestrictedTrustManager(baseTrustManager, restrictions, Set.of(X509Field.SAN_OTHERNAME_COMMONNAME)); + assertNotValid(trustManager, "varied", failureMatchCommon); + + // instance03.cluster02.elasticsearch or search.example.com -> *.150.200 + restrictions = new CertificateTrustRestrictions(List.of("*.150.200")); + trustManager = new RestrictedTrustManager( + baseTrustManager, + restrictions, + Set.of(X509Field.SAN_DNS, X509Field.SAN_OTHERNAME_COMMONNAME) + ); + assertNotValid(trustManager, "varied", failureMatchDns); + assertNotValid(trustManager, "varied", failureMatchCommon); + } + public void testTrustsExplicitCertificateName() throws Exception { final int trustedCluster = randomIntBetween(1, numberOfClusters); final List trustedNames = new ArrayList<>(numberOfNodes); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationSettingsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationSettingsTests.java index 0772eaeaca20..a800266a33a1 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationSettingsTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationSettingsTests.java @@ -9,14 +9,15 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.ssl.SslClientAuthenticationMode; +import org.elasticsearch.common.ssl.X509Field; import org.elasticsearch.test.ESTestCase; import java.util.Arrays; -import java.util.List; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.TrustManagerFactory; +import static org.elasticsearch.common.ssl.SslConfigurationLoader.GLOBAL_DEFAULT_RESTRICTED_TRUST_FIELDS; import static org.elasticsearch.test.TestMatchers.throwableWithMessage; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.instanceOf; @@ -84,14 +85,11 @@ public void testParseTrustRestrictionsListWithPrefix() { Settings settings = Settings.builder() .putList("ssl.trust_restrictions.x509_fields", "subjectAltName.otherName.commonName", "subjectAltName.dnsName") .build(); - assertThat( - ssl.trustRestrictionsX509Fields.get(settings), - is(Arrays.asList("subjectAltName.otherName.commonName", "subjectAltName.dnsName")) - ); + assertThat(ssl.trustRestrictionsX509Fields.get(settings), is(Arrays.asList(X509Field.SAN_OTHERNAME_COMMONNAME, X509Field.SAN_DNS))); // implicit configuration settings = Settings.builder().build(); - assertThat(ssl.trustRestrictionsX509Fields.get(settings), is(Arrays.asList("subjectAltName.otherName.commonName"))); + assertThat(ssl.trustRestrictionsX509Fields.get(settings), is(GLOBAL_DEFAULT_RESTRICTED_TRUST_FIELDS)); // invalid configuration final Settings invalid = Settings.builder().putList("ssl.trust_restrictions.x509_fields", "foo.bar").build(); @@ -122,7 +120,7 @@ public void testEmptySettingsParsesToDefaults() { assertThat(ssl.truststorePassword.exists(settings), is(false)); assertThat(ssl.truststorePath.get(settings).isPresent(), is(false)); assertThat(ssl.trustRestrictionsPath.get(settings).isPresent(), is(false)); - assertThat(ssl.trustRestrictionsX509Fields.get(settings), is(List.of("subjectAltName.otherName.commonName"))); + assertThat(ssl.trustRestrictionsX509Fields.get(settings), is(GLOBAL_DEFAULT_RESTRICTED_TRUST_FIELDS)); assertThat(ssl.verificationMode.get(settings).isPresent(), is(false)); } diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/restricted.trust.crt b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/restricted.trust.crt new file mode 100644 index 000000000000..344c186e0662 --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/nodes/restricted.trust.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDZTCCAk2gAwIBAgIUJYSa8ZSlu1fwxC/oYnmjO2KpFUcwDQYJKoZIhvcNAQEL +BQAwGzEZMBcGA1UEAxMQcmVzdHJpY3RlZC50cnVzdDAeFw0yMjExMjgyMjUzMDla +Fw00MjExMjMyMjUzMDlaMBsxGTAXBgNVBAMTEHJlc3RyaWN0ZWQudHJ1c3QwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBrbfvxy+OqGW0XjRpfyPJDB9I +aNozRT3DCioIBNlKyawh7/rdGXyRsenq2c6uB+J2+fCamEHUCGZWKuCdxhewV8rM +rLSBRwTOX0q+WjPVe2GN79sHmJHYgkNivdbURgePmL/4U1j+gbwDLGQy1aOMnay9 +TxpRnMbUTsBz8lj5OfA+F9eoSTJjBbf2S/zeQsQMydG/51Mrj2VYfz3dbJjHAxNr +PlEP8OvM/y9N8qMdMUE9YkaAw2gJKaPqmj/2nucwxrHRuioKTAatW2F3T/5AWJEB +262WxpZpSqAErR4LXqaUVBdOXwB+DwQKG/uD7gb9QRhuIx0yC4v5MC98mkHJAgMB +AAGjgaAwgZ0wHQYDVR0OBBYEFPSCxeMJ4lTTDuQ/k3cz6Poc8o96MB8GA1UdIwQY +MBaAFPSCxeMJ4lTTDuQ/k3cz6Poc8o96MFAGA1UdEQRJMEeCEnNlYXJjaC5leGFt +cGxlLmNvbYcEMmSWyKArBgNVBAOgJAwiaW5zdGFuY2UwMy5jbHVzdGVyMDIuZWxh +c3RpY3NlYXJjaDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQAq3ZPsnZ8k +2IdQEBHhbIPZKnK02sEMo7gK5sC4LbXnQGCks5wu9KUMpLNI+t4Q41j8+P6kuwZ9 +9XumWKUo2veMssxzlupwqQDX5/oFNKc3D5OAxTq7scs9qOW4oP7u8Scg0inqtJw1 +nPARV6mwVqzuz9dWJxaCugo8NuZ88d9fUuI4WRKixffTuMlRknHKa8DVL0NhKE5Z +W0TXtRg0VewaSEotKC5CqxI2UUOo7A2x0wteW8PHjz+He14rdDXA+Mw8RGHfjNXf +vZigDHFcEWYe+twHFYo91w1xNdu0oZFUCwvJnIdUS5el1CncBgKxHo0+M3DyqLX+ +5ZYk1NOCQkpA +-----END CERTIFICATE----- From 7acf9092b8555b600ce431c342b953173a0e9ab7 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski <6207777+grcevski@users.noreply.github.com> Date: Wed, 7 Dec 2022 16:14:35 -0500 Subject: [PATCH 198/919] Check reserved state in Metadata.isGlobalStateEquals (#92124) This fixes a bug where we didn't properly check for the reserved state in global state changes. The bug manifests itself as transient reserved state between node restarts. --- docs/changelog/92124.yaml | 5 ++ .../service/FileSettingsServiceIT.java | 32 +++++++++++ .../cluster/metadata/Metadata.java | 3 + .../cluster/metadata/MetadataTests.java | 56 +++++++++++++++++++ 4 files changed, 96 insertions(+) create mode 100644 docs/changelog/92124.yaml diff --git a/docs/changelog/92124.yaml b/docs/changelog/92124.yaml new file mode 100644 index 000000000000..ea4e7088ee1a --- /dev/null +++ b/docs/changelog/92124.yaml @@ -0,0 +1,5 @@ +pr: 92124 +summary: Check reserved state in Metadata.isGlobalStateEquals +area: Infra/Core +type: bug +issues: [] diff --git a/server/src/internalClusterTest/java/org/elasticsearch/reservedstate/service/FileSettingsServiceIT.java b/server/src/internalClusterTest/java/org/elasticsearch/reservedstate/service/FileSettingsServiceIT.java index cfde48d088db..60c04da7ad07 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/reservedstate/service/FileSettingsServiceIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/reservedstate/service/FileSettingsServiceIT.java @@ -186,6 +186,38 @@ public void testSettingsAppliedOnStart() throws Exception { assertClusterStateSaveOK(savedClusterState.v1(), savedClusterState.v2()); } + public void testReservedStatePersistsOnRestart() throws Exception { + internalCluster().setBootstrapMasterNodeIndex(0); + logger.info("--> start master node"); + final String masterNode = internalCluster().startMasterOnlyNode(); + assertMasterNode(internalCluster().masterClient(), masterNode); + var savedClusterState = setupClusterStateListener(masterNode); + + FileSettingsService masterFileSettingsService = internalCluster().getInstance(FileSettingsService.class, masterNode); + + assertTrue(masterFileSettingsService.watching()); + + logger.info("--> write some settings"); + writeJSONFile(masterNode, testJSON); + assertClusterStateSaveOK(savedClusterState.v1(), savedClusterState.v2()); + + logger.info("--> restart master"); + internalCluster().restartNode(masterNode); + + final ClusterStateResponse clusterStateResponse = client().admin().cluster().state(new ClusterStateRequest()).actionGet(); + assertEquals( + 1, + clusterStateResponse.getState() + .metadata() + .reservedStateMetadata() + .get(FileSettingsService.NAMESPACE) + .handlers() + .get(ReservedClusterSettingsAction.NAME) + .keys() + .size() + ); + } + private Tuple setupClusterStateListenerForError(String node) { ClusterService clusterService = internalCluster().clusterService(node); CountDownLatch savedClusterState = new CountDownLatch(1); diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java index 4eb9ced4f435..7f450bf73923 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java @@ -1299,6 +1299,9 @@ public static boolean isGlobalStateEquals(Metadata metadata1, Metadata metadata2 if (customCount1 != customCount2) { return false; } + if (Objects.equals(metadata1.reservedStateMetadata, metadata2.reservedStateMetadata) == false) { + return false; + } return true; } diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataTests.java index 107828e06b25..c9062e106aaf 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataTests.java @@ -29,6 +29,7 @@ import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.IndexSettings; @@ -44,6 +45,7 @@ import org.elasticsearch.xcontent.json.JsonXContent; import java.io.IOException; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -2312,6 +2314,60 @@ public void testChunkedToXContent() throws IOException { ); } + /** + * With this test we ensure that we consider whether a new field added to Metadata should be checked + * in Metadata.isGlobalStateEquals. We force the instance fields to be either in the checked list + * or in the excluded list. + *

+ * This prevents from accidentally forgetting that a new field should be checked in isGlobalStateEquals. + */ + @SuppressForbidden(reason = "need access to all fields, they are mostly private") + public void testEnsureMetadataFieldCheckedForGlobalStateChanges() { + Set checkedForGlobalStateChanges = Set.of( + "coordinationMetadata", + "persistentSettings", + "hashesOfConsistentSettings", + "templates", + "clusterUUID", + "clusterUUIDCommitted", + "customs", + "reservedStateMetadata" + ); + Set excludedFromGlobalStateCheck = Set.of( + "version", + "transientSettings", + "settings", + "indices", + "aliasedIndices", + "totalNumberOfShards", + "totalOpenIndexShards", + "allIndices", + "visibleIndices", + "allOpenIndices", + "visibleOpenIndices", + "allClosedIndices", + "visibleClosedIndices", + "indicesLookup", + "mappingsByHash", + "oldestIndexVersion" + ); + + var diff = new HashSet<>(checkedForGlobalStateChanges); + diff.removeAll(excludedFromGlobalStateCheck); + + // sanity check that the two field sets are mutually exclusive + assertEquals(checkedForGlobalStateChanges, diff); + + // any declared non-static field in metadata must be either in the list of fields + // we check for global state changes, or in the fields excluded from the global state check. + var unclassifiedFields = Arrays.stream(Metadata.class.getDeclaredFields()) + .filter(f -> Modifier.isStatic(f.getModifiers()) == false) + .map(f -> f.getName()) + .filter(n -> (checkedForGlobalStateChanges.contains(n) || excludedFromGlobalStateCheck.contains(n)) == false) + .collect(Collectors.toSet()); + assertThat(unclassifiedFields, empty()); + } + public static Metadata randomMetadata() { return randomMetadata(1); } From 5126f6d92a3d88b2734632245f2d2189f195dd7f Mon Sep 17 00:00:00 2001 From: Joe Gallo Date: Wed, 7 Dec 2022 17:12:50 -0500 Subject: [PATCH 199/919] Internal refactoring of some IngestService methods (#92203) --- .../action/bulk/TransportBulkAction.java | 2 +- .../elasticsearch/ingest/IngestService.java | 23 +++++------ .../bulk/TransportBulkActionIngestTests.java | 18 ++++----- .../ingest/IngestServiceTests.java | 38 +++++++++---------- 4 files changed, 41 insertions(+), 40 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java b/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java index 827cdc57d21a..94df5bda48c4 100644 --- a/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java +++ b/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java @@ -780,6 +780,7 @@ private void processBulkIndexIngestRequest( ingestService.executeBulkRequest( original.numberOfActions(), () -> bulkRequestModifier, + bulkRequestModifier::markItemAsDropped, bulkRequestModifier::markItemAsFailed, (originalThread, exception) -> { if (exception != null) { @@ -823,7 +824,6 @@ public boolean isForceExecution() { } } }, - bulkRequestModifier::markItemAsDropped, executorName ); } diff --git a/server/src/main/java/org/elasticsearch/ingest/IngestService.java b/server/src/main/java/org/elasticsearch/ingest/IngestService.java index 123731f54258..8a28b00a4186 100644 --- a/server/src/main/java/org/elasticsearch/ingest/IngestService.java +++ b/server/src/main/java/org/elasticsearch/ingest/IngestService.java @@ -668,13 +668,14 @@ void validatePipeline(Map ingestInfos, String pipelin } public void executeBulkRequest( - int numberOfActionRequests, - Iterable> actionRequests, - BiConsumer onFailure, - BiConsumer onCompletion, - IntConsumer onDropped, - String executorName + final int numberOfActionRequests, + final Iterable> actionRequests, + final IntConsumer onDropped, + final BiConsumer onFailure, + final BiConsumer onCompletion, + final String executorName ) { + assert numberOfActionRequests > 0 : "numberOfActionRequests must be greater than 0 but was [" + numberOfActionRequests + "]"; threadPool.executor(executorName).execute(new AbstractRunnable() { @@ -886,11 +887,11 @@ static String getProcessorName(Processor processor) { } private void innerExecute( - int slot, - IndexRequest indexRequest, - Pipeline pipeline, - IntConsumer itemDroppedHandler, - Consumer handler + final int slot, + final IndexRequest indexRequest, + final Pipeline pipeline, + final IntConsumer itemDroppedHandler, + final Consumer handler ) { if (pipeline.getProcessors().isEmpty()) { handler.accept(null); diff --git a/server/src/test/java/org/elasticsearch/action/bulk/TransportBulkActionIngestTests.java b/server/src/test/java/org/elasticsearch/action/bulk/TransportBulkActionIngestTests.java index 9da30ae04339..af9bbb3b2279 100644 --- a/server/src/test/java/org/elasticsearch/action/bulk/TransportBulkActionIngestTests.java +++ b/server/src/test/java/org/elasticsearch/action/bulk/TransportBulkActionIngestTests.java @@ -290,9 +290,9 @@ public void testIngestLocal() throws Exception { verify(ingestService).executeBulkRequest( eq(bulkRequest.numberOfActions()), bulkDocsItr.capture(), + any(), failureHandler.capture(), completionHandler.capture(), - any(), eq(Names.WRITE) ); completionHandler.getValue().accept(null, exception); @@ -332,9 +332,9 @@ public void testSingleItemBulkActionIngestLocal() throws Exception { verify(ingestService).executeBulkRequest( eq(1), bulkDocsItr.capture(), + any(), failureHandler.capture(), completionHandler.capture(), - any(), eq(Names.WRITE) ); completionHandler.getValue().accept(null, exception); @@ -378,9 +378,9 @@ public void testIngestSystemLocal() throws Exception { verify(ingestService).executeBulkRequest( eq(bulkRequest.numberOfActions()), bulkDocsItr.capture(), + any(), failureHandler.capture(), completionHandler.capture(), - any(), eq(Names.SYSTEM_WRITE) ); completionHandler.getValue().accept(null, exception); @@ -535,9 +535,9 @@ private void validatePipelineWithBulkUpsert(@Nullable String indexRequestIndexNa verify(ingestService).executeBulkRequest( eq(bulkRequest.numberOfActions()), bulkDocsItr.capture(), + any(), failureHandler.capture(), completionHandler.capture(), - any(), eq(Names.WRITE) ); assertEquals(indexRequest1.getPipeline(), "default_pipeline"); @@ -583,9 +583,9 @@ public void testDoExecuteCalledTwiceCorrectly() throws Exception { verify(ingestService).executeBulkRequest( eq(1), bulkDocsItr.capture(), + any(), failureHandler.capture(), completionHandler.capture(), - any(), eq(Names.WRITE) ); completionHandler.getValue().accept(null, exception); @@ -677,9 +677,9 @@ public void testFindDefaultPipelineFromTemplateMatch() { verify(ingestService).executeBulkRequest( eq(1), bulkDocsItr.capture(), + any(), failureHandler.capture(), completionHandler.capture(), - any(), eq(Names.WRITE) ); } @@ -721,9 +721,9 @@ public void testFindDefaultPipelineFromV2TemplateMatch() { verify(ingestService).executeBulkRequest( eq(1), bulkDocsItr.capture(), + any(), failureHandler.capture(), completionHandler.capture(), - any(), eq(Names.WRITE) ); } @@ -751,9 +751,9 @@ public void testIngestCallbackExceptionHandled() throws Exception { verify(ingestService).executeBulkRequest( eq(bulkRequest.numberOfActions()), bulkDocsItr.capture(), + any(), failureHandler.capture(), completionHandler.capture(), - any(), eq(Names.WRITE) ); indexRequest1.autoGenerateId(); @@ -788,9 +788,9 @@ private void validateDefaultPipeline(IndexRequest indexRequest) { verify(ingestService).executeBulkRequest( eq(1), bulkDocsItr.capture(), + any(), failureHandler.capture(), completionHandler.capture(), - any(), eq(Names.WRITE) ); assertEquals(indexRequest.getPipeline(), "default_pipeline"); diff --git a/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java b/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java index 196cb62bf018..8e61698c0f4b 100644 --- a/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java +++ b/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java @@ -195,9 +195,9 @@ public void testExecuteIndexPipelineDoesNotExist() { ingestService.executeBulkRequest( 1, Collections.singletonList(indexRequest), + indexReq -> {}, failureHandler, completionHandler, - indexReq -> {}, Names.WRITE ); @@ -963,9 +963,9 @@ public String getType() { ingestService.executeBulkRequest( bulkRequest.numberOfActions(), bulkRequest.requests(), + indexReq -> {}, failureHandler, completionHandler, - indexReq -> {}, Names.WRITE ); @@ -1009,9 +1009,9 @@ public void testExecuteBulkPipelineDoesNotExist() { ingestService.executeBulkRequest( bulkRequest.numberOfActions(), bulkRequest.requests(), + indexReq -> {}, failureHandler, completionHandler, - indexReq -> {}, Names.WRITE ); verify(failureHandler, times(1)).accept( @@ -1045,9 +1045,9 @@ public void testExecuteSuccess() { ingestService.executeBulkRequest( 1, Collections.singletonList(indexRequest), + indexReq -> {}, failureHandler, completionHandler, - indexReq -> {}, Names.WRITE ); verify(failureHandler, never()).accept(any(), any()); @@ -1085,9 +1085,9 @@ public void testDynamicTemplates() throws Exception { ingestService.executeBulkRequest( 1, Collections.singletonList(indexRequest), + indexReq -> {}, failureHandler, completionHandler, - indexReq -> {}, Names.WRITE ); latch.await(); @@ -1113,9 +1113,9 @@ public void testExecuteEmptyPipeline() throws Exception { ingestService.executeBulkRequest( 1, Collections.singletonList(indexRequest), + indexReq -> {}, failureHandler, completionHandler, - indexReq -> {}, Names.WRITE ); verify(failureHandler, never()).accept(any(), any()); @@ -1176,9 +1176,9 @@ public void testExecutePropagateAllMetadataUpdates() throws Exception { ingestService.executeBulkRequest( 1, Collections.singletonList(indexRequest), + indexReq -> {}, failureHandler, completionHandler, - indexReq -> {}, Names.WRITE ); verify(processor).execute(any(), any()); @@ -1220,9 +1220,9 @@ public void testExecuteFailure() throws Exception { ingestService.executeBulkRequest( 1, Collections.singletonList(indexRequest), + indexReq -> {}, failureHandler, completionHandler, - indexReq -> {}, Names.WRITE ); verify(processor).execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), emptyMap()), any()); @@ -1278,9 +1278,9 @@ public void testExecuteSuccessWithOnFailure() throws Exception { ingestService.executeBulkRequest( 1, Collections.singletonList(indexRequest), + indexReq -> {}, failureHandler, completionHandler, - indexReq -> {}, Names.WRITE ); verify(failureHandler, never()).accept(eq(0), any(IngestProcessorException.class)); @@ -1330,9 +1330,9 @@ public void testExecuteFailureWithNestedOnFailure() throws Exception { ingestService.executeBulkRequest( 1, Collections.singletonList(indexRequest), + indexReq -> {}, failureHandler, completionHandler, - indexReq -> {}, Names.WRITE ); verify(processor).execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), emptyMap()), any()); @@ -1393,9 +1393,9 @@ public void testBulkRequestExecutionWithFailures() throws Exception { ingestService.executeBulkRequest( numRequest, bulkRequest.requests(), + indexReq -> {}, requestItemErrorHandler, completionHandler, - indexReq -> {}, Names.WRITE ); @@ -1447,9 +1447,9 @@ public void testBulkRequestExecution() throws Exception { ingestService.executeBulkRequest( numRequest, bulkRequest.requests(), + indexReq -> {}, requestItemErrorHandler, completionHandler, - indexReq -> {}, Names.WRITE ); @@ -1517,9 +1517,9 @@ public void testStats() throws Exception { ingestService.executeBulkRequest( 1, Collections.singletonList(indexRequest), + indexReq -> {}, failureHandler, completionHandler, - indexReq -> {}, Names.WRITE ); final IngestStats afterFirstRequestStats = ingestService.stats(); @@ -1541,9 +1541,9 @@ public void testStats() throws Exception { ingestService.executeBulkRequest( 1, Collections.singletonList(indexRequest), + indexReq -> {}, failureHandler, completionHandler, - indexReq -> {}, Names.WRITE ); final IngestStats afterSecondRequestStats = ingestService.stats(); @@ -1570,9 +1570,9 @@ public void testStats() throws Exception { ingestService.executeBulkRequest( 1, Collections.singletonList(indexRequest), + indexReq -> {}, failureHandler, completionHandler, - indexReq -> {}, Names.WRITE ); final IngestStats afterThirdRequestStats = ingestService.stats(); @@ -1600,9 +1600,9 @@ public void testStats() throws Exception { ingestService.executeBulkRequest( 1, Collections.singletonList(indexRequest), + indexReq -> {}, failureHandler, completionHandler, - indexReq -> {}, Names.WRITE ); final IngestStats afterForthRequestStats = ingestService.stats(); @@ -1698,9 +1698,9 @@ public String getDescription() { ingestService.executeBulkRequest( bulkRequest.numberOfActions(), bulkRequest.requests(), + dropHandler, failureHandler, completionHandler, - dropHandler, Names.WRITE ); verify(failureHandler, never()).accept(any(), any()); @@ -1784,9 +1784,9 @@ public void testCBORParsing() throws Exception { ingestService.executeBulkRequest( 1, Collections.singletonList(indexRequest), + indexReq -> {}, (integer, e) -> {}, (thread, e) -> {}, - indexReq -> {}, Names.WRITE ); } @@ -1817,7 +1817,7 @@ public void testPostIngest() { bulkRequest.add(indexRequest1); bulkRequest.add(indexRequest2); - ingestService.executeBulkRequest(2, bulkRequest.requests(), (integer, e) -> {}, (thread, e) -> {}, indexReq -> {}, Names.WRITE); + ingestService.executeBulkRequest(2, bulkRequest.requests(), indexReq -> {}, (integer, e) -> {}, (thread, e) -> {}, Names.WRITE); assertThat(indexRequest1.getRawTimestamp(), equalTo(10)); assertThat(indexRequest2.getRawTimestamp(), nullValue()); From a635230e1cc19a18952ea16071f38d1e64dab165 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Thu, 8 Dec 2022 09:19:41 +0000 Subject: [PATCH 200/919] [ML] Unmute text-structure docs test (#92224) This test should no longer fail now that #91829 has fixed the underlying problem. --- docs/reference/text-structure/apis/find-structure.asciidoc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/reference/text-structure/apis/find-structure.asciidoc b/docs/reference/text-structure/apis/find-structure.asciidoc index 6798bd953da3..a65f87290b0a 100644 --- a/docs/reference/text-structure/apis/find-structure.asciidoc +++ b/docs/reference/text-structure/apis/find-structure.asciidoc @@ -538,8 +538,7 @@ If the request does not encounter errors, you receive the following result: } } ---- -// TESTRESPONSE[skip:"AwaitsFix https://github.com/elastic/elasticsearch/issues/92141"] -// original (put back after unmuting)[s/"sample_start" : ".*",/"sample_start" : "$body.sample_start",/] +// TESTRESPONSE[s/"sample_start" : ".*",/"sample_start" : "$body.sample_start",/] // The substitution is because the text is pre-processed by the test harness, // so the fields may get reordered in the JSON the endpoint sees From 4fdf260726e6876be1130fd4a800e9a0aa6166ea Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Thu, 8 Dec 2022 11:26:15 +0200 Subject: [PATCH 201/919] [ML] Validate rule filters are present on open anomaly detection api (#92207) When an anomaly detection job is opened and it is refererring a rule filter that is missing, the open action reports successful but the job goes to `failed` state immediately without explaining what the reason was. This commit improves this behavior by validating that referenced filters are present when the open action is called. If there is a missing filter then an error is returned and the job is not opened. --- docs/changelog/92207.yaml | 5 +++ .../ml/qa/ml-with-security/build.gradle | 1 + .../ml/action/TransportOpenJobAction.java | 22 ++++++++++++- .../rest-api-spec/test/ml/jobs_crud.yml | 32 +++++++++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/92207.yaml diff --git a/docs/changelog/92207.yaml b/docs/changelog/92207.yaml new file mode 100644 index 000000000000..f8041c30db51 --- /dev/null +++ b/docs/changelog/92207.yaml @@ -0,0 +1,5 @@ +pr: 92207 +summary: Validate rule filters are present on open anomaly detection api +area: Machine Learning +type: bug +issues: [] diff --git a/x-pack/plugin/ml/qa/ml-with-security/build.gradle b/x-pack/plugin/ml/qa/ml-with-security/build.gradle index 50b8b16c2dd0..e74e61fdb7d5 100644 --- a/x-pack/plugin/ml/qa/ml-with-security/build.gradle +++ b/x-pack/plugin/ml/qa/ml-with-security/build.gradle @@ -185,6 +185,7 @@ tasks.named("yamlRestTest").configure { 'ml/jobs_crud/Test put job with time field in analysis_config', 'ml/jobs_crud/Test put job with duplicate detector configurations', 'ml/jobs_crud/Test job with categorization_analyzer and categorization_filters', + 'ml/jobs_crud/Test job with rule referencing missing filter', 'ml/jobs_get/Test get job given missing job_id', 'ml/jobs_get_result_buckets/Test mutually-exclusive params', 'ml/jobs_get_result_buckets/Test mutually-exclusive params via body', diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportOpenJobAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportOpenJobAction.java index 08d4deaf3e62..ea9a51e66f88 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportOpenJobAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportOpenJobAction.java @@ -36,6 +36,7 @@ import org.elasticsearch.xpack.core.ml.MachineLearningField; import org.elasticsearch.xpack.core.ml.MlConfigIndex; import org.elasticsearch.xpack.core.ml.MlTasks; +import org.elasticsearch.xpack.core.ml.action.GetFiltersAction; import org.elasticsearch.xpack.core.ml.action.GetModelSnapshotsAction; import org.elasticsearch.xpack.core.ml.action.NodeAcknowledgedResponse; import org.elasticsearch.xpack.core.ml.action.OpenJobAction; @@ -50,7 +51,9 @@ import org.elasticsearch.xpack.ml.process.MlMemoryTracker; import java.util.Optional; +import java.util.Set; import java.util.function.Predicate; +import java.util.stream.Collectors; import static org.elasticsearch.core.Strings.format; import static org.elasticsearch.xpack.core.ClientHelper.ML_ORIGIN; @@ -169,7 +172,7 @@ public void onFailure(Exception e) { ); // Tell the job tracker to refresh the memory requirement for this job and all other jobs that have persistent tasks - ActionListener modelSnapshotValidationListener = ActionListener.wrap( + ActionListener referencedRuleFiltersPresentListener = ActionListener.wrap( response -> memoryTracker.refreshAnomalyDetectorJobMemoryAndAllOthers( jobParams.getJobId(), memoryRequirementRefreshListener @@ -177,6 +180,23 @@ public void onFailure(Exception e) { listener::onFailure ); + // Validate referenced rule filters are present + ActionListener modelSnapshotValidationListener = ActionListener.wrap(response -> { + Set referencedRuleFilters = jobParams.getJob().getAnalysisConfig().extractReferencedFilters(); + if (referencedRuleFilters.isEmpty()) { + referencedRuleFiltersPresentListener.onResponse(true); + } else { + GetFiltersAction.Request getFiltersRequest = new GetFiltersAction.Request(); + getFiltersRequest.setResourceId(referencedRuleFilters.stream().collect(Collectors.joining(","))); + getFiltersRequest.setAllowNoResources(false); + client.execute( + GetFiltersAction.INSTANCE, + getFiltersRequest, + ActionListener.wrap(filtersResponse -> referencedRuleFiltersPresentListener.onResponse(true), listener::onFailure) + ); + } + }, listener::onFailure); + // Validate the model snapshot is supported ActionListener getJobHandler = ActionListener.wrap(response -> { if (jobParams.getJob().getModelSnapshotId() == null) { diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/jobs_crud.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/jobs_crud.yml index 6113569f6303..3c4439444d1a 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/jobs_crud.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/jobs_crud.yml @@ -1670,3 +1670,35 @@ { "allow_no_match" : true } + +--- +"Test job with rule referencing missing filter": + + - do: + ml.put_job: + job_id: jobs-crud-rule-missing-filter + body: > + { + "analysis_config": { + "detectors": [ + { + "function": "count", + "by_field_name": "country", + "custom_rules": [ + { + "actions": ["skip_result"], + "scope": { + "country": {"filter_id": "safe_countries"} + } + } + ] + } + ] + }, + "data_description" : {} + } + + - do: + catch: /Unable to find filter \[safe_countries\]/ + ml.open_job: + job_id: jobs-crud-rule-missing-filter From 1be17d8c55f6e96255a5e94f15ad795378880758 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Thu, 8 Dec 2022 09:58:48 +0000 Subject: [PATCH 202/919] [ML] Correct the update datafeed docs (#92227) These docs previously implied that you could update datafeed properties while the datafeed was running, but then would have to stop and restart it for the changes to take effect. In fact datafeed updates can only be made while the datafeed is stopped (and this has been the case for many years, if not forever). --- .../ml/anomaly-detection/apis/update-datafeed.asciidoc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/reference/ml/anomaly-detection/apis/update-datafeed.asciidoc b/docs/reference/ml/anomaly-detection/apis/update-datafeed.asciidoc index dd9b2f16cb57..48893f1aadb8 100644 --- a/docs/reference/ml/anomaly-detection/apis/update-datafeed.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/update-datafeed.asciidoc @@ -25,8 +25,9 @@ Requires the `manage_ml` cluster privilege. This privilege is included in the [[ml-update-datafeed-desc]] == {api-description-title} -If you update a {dfeed} property, you must stop and start the {dfeed} for the -change to be applied. +You can only update a {dfeed} property while the {dfeed} is stopped. +However, it is possible to stop a {dfeed}, update one of its properties, +and restart it without closing the associated job. IMPORTANT: When {es} {security-features} are enabled, your {dfeed} remembers which roles the user who updated it had at the time of update and runs the query From 5236924dc38daa2eb87939d8e2d066e79e7c69b2 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Thu, 8 Dec 2022 10:02:30 +0000 Subject: [PATCH 203/919] Cleaning up some painless code & removing obsoleted functions (#92103) * Tidy up some lambda/function factory code * Remove stray arguments from def argument checking * We now always have the Java9 array length method handle * Remove pre-java9 string concat code --- .../java/org/elasticsearch/painless/Def.java | 127 ++---------------- .../elasticsearch/painless/DefBootstrap.java | 10 +- .../elasticsearch/painless/FunctionRef.java | 115 +++++++--------- .../painless/LambdaBootstrap.java | 18 +-- .../elasticsearch/painless/MethodWriter.java | 76 +++-------- .../painless/WriterConstants.java | 53 +++----- .../phase/DefaultUserTreeToIRTreePhase.java | 2 +- .../elasticsearch/painless/ArrayTests.java | 2 - .../elasticsearch/painless/StringTests.java | 20 +-- 9 files changed, 115 insertions(+), 308 deletions(-) diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Def.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Def.java index a1a4f7ea124e..8c424ad1d022 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Def.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Def.java @@ -46,90 +46,6 @@ */ public final class Def { - // TODO: Once Java has a factory for those in java.lang.invoke.MethodHandles, use it: - - /** Helper class for isolating MethodHandles and methods to get the length of arrays - * (to emulate a "arraystore" bytecode using MethodHandles). - * See: https://bugs.openjdk.java.net/browse/JDK-8156915 - */ - @SuppressWarnings("unused") // getArrayLength() methods are are actually used, javac just does not know :) - private static final class ArrayLengthHelper { - private static final MethodHandles.Lookup PRIVATE_METHOD_HANDLES_LOOKUP = MethodHandles.lookup(); - - private static final Map, MethodHandle> ARRAY_TYPE_MH_MAPPING = Collections.unmodifiableMap( - Stream.of( - boolean[].class, - byte[].class, - short[].class, - int[].class, - long[].class, - char[].class, - float[].class, - double[].class, - Object[].class - ).collect(Collectors.toMap(Function.identity(), type -> { - try { - return PRIVATE_METHOD_HANDLES_LOOKUP.findStatic( - PRIVATE_METHOD_HANDLES_LOOKUP.lookupClass(), - "getArrayLength", - MethodType.methodType(int.class, type) - ); - } catch (ReflectiveOperationException e) { - throw new AssertionError(e); - } - })) - ); - - private static final MethodHandle OBJECT_ARRAY_MH = ARRAY_TYPE_MH_MAPPING.get(Object[].class); - - static int getArrayLength(final boolean[] array) { - return array.length; - } - - static int getArrayLength(final byte[] array) { - return array.length; - } - - static int getArrayLength(final short[] array) { - return array.length; - } - - static int getArrayLength(final int[] array) { - return array.length; - } - - static int getArrayLength(final long[] array) { - return array.length; - } - - static int getArrayLength(final char[] array) { - return array.length; - } - - static int getArrayLength(final float[] array) { - return array.length; - } - - static int getArrayLength(final double[] array) { - return array.length; - } - - static int getArrayLength(final Object[] array) { - return array.length; - } - - static MethodHandle arrayLengthGetter(Class arrayType) { - if (arrayType.isArray() == false) { - throw new IllegalArgumentException("type must be an array"); - } - return (ARRAY_TYPE_MH_MAPPING.containsKey(arrayType)) - ? ARRAY_TYPE_MH_MAPPING.get(arrayType) - : OBJECT_ARRAY_MH.asType(OBJECT_ARRAY_MH.type().changeParameterType(0, arrayType)); - } - - private ArrayLengthHelper() {} - } - /** pointer to Map.get(Object) */ private static final MethodHandle MAP_GET; /** pointer to Map.put(Object,Object) */ @@ -144,8 +60,8 @@ private ArrayLengthHelper() {} private static final MethodHandle MAP_INDEX_NORMALIZE; /** pointer to {@link Def#listIndexNormalize}. */ private static final MethodHandle LIST_INDEX_NORMALIZE; - /** factory for arraylength MethodHandle (intrinsic) from Java 9 (pkg-private for tests) */ - static final MethodHandle JAVA9_ARRAY_LENGTH_MH_FACTORY; + /** factory for arraylength MethodHandle (intrinsic) */ + private static final MethodHandle ARRAY_LENGTH; public static final Map, MethodHandle> DEF_TO_BOXED_TYPE_IMPLICIT_CAST; @@ -168,23 +84,14 @@ private ArrayLengthHelper() {} "listIndexNormalize", MethodType.methodType(int.class, List.class, int.class) ); - } catch (final ReflectiveOperationException roe) { - throw new AssertionError(roe); - } - - // lookup up the factory for arraylength MethodHandle (intrinsic) from Java 9: - // https://bugs.openjdk.java.net/browse/JDK-8156915 - MethodHandle arrayLengthMHFactory; - try { - arrayLengthMHFactory = methodHandlesLookup.findStatic( + ARRAY_LENGTH = methodHandlesLookup.findStatic( MethodHandles.class, "arrayLength", MethodType.methodType(MethodHandle.class, Class.class) ); - } catch (final ReflectiveOperationException roe) { - arrayLengthMHFactory = null; + } catch (ReflectiveOperationException roe) { + throw new AssertionError(roe); } - JAVA9_ARRAY_LENGTH_MH_FACTORY = arrayLengthMHFactory; Map, MethodHandle> defToBoxedTypeImplicitCast = new HashMap<>(); @@ -232,15 +139,11 @@ static void rethrow(Throwable t) throws T { /** Returns an array length getter MethodHandle for the given array type */ static MethodHandle arrayLengthGetter(Class arrayType) { - if (JAVA9_ARRAY_LENGTH_MH_FACTORY != null) { - try { - return (MethodHandle) JAVA9_ARRAY_LENGTH_MH_FACTORY.invokeExact(arrayType); - } catch (Throwable t) { - rethrow(t); - throw new AssertionError(t); - } - } else { - return ArrayLengthHelper.arrayLengthGetter(arrayType); + try { + return (MethodHandle) ARRAY_LENGTH.invokeExact(arrayType); + } catch (Throwable t) { + rethrow(t); + throw new AssertionError(t); } } @@ -840,9 +743,8 @@ static MethodHandle newIterator(Class arrayType) { if (arrayType.isArray() == false) { throw new IllegalArgumentException("type must be an array"); } - return (ARRAY_TYPE_MH_MAPPING.containsKey(arrayType)) - ? ARRAY_TYPE_MH_MAPPING.get(arrayType) - : OBJECT_ARRAY_MH.asType(OBJECT_ARRAY_MH.type().changeParameterType(0, arrayType)); + MethodHandle iterator = ARRAY_TYPE_MH_MAPPING.get(arrayType); + return iterator != null ? iterator : OBJECT_ARRAY_MH.asType(OBJECT_ARRAY_MH.type().changeParameterType(0, arrayType)); } private ArrayIteratorHelper() {} @@ -1616,9 +1518,8 @@ static MethodHandle arrayIndexNormalizer(Class arrayType) { if (arrayType.isArray() == false) { throw new IllegalArgumentException("type must be an array"); } - return (ARRAY_TYPE_MH_MAPPING.containsKey(arrayType)) - ? ARRAY_TYPE_MH_MAPPING.get(arrayType) - : OBJECT_ARRAY_MH.asType(OBJECT_ARRAY_MH.type().changeParameterType(0, arrayType)); + MethodHandle handle = ARRAY_TYPE_MH_MAPPING.get(arrayType); + return handle != null ? handle : OBJECT_ARRAY_MH.asType(OBJECT_ARRAY_MH.type().changeParameterType(0, arrayType)); } private ArrayIndexNormalizeHelper() {} diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/DefBootstrap.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/DefBootstrap.java index 135673010c6f..3d35c5d0ab5e 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/DefBootstrap.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/DefBootstrap.java @@ -382,13 +382,13 @@ Object fallback(Object[] args) throws Throwable { } else if (type.parameterType(0) != Object.class) { // case 2: only the argument is unknown, just check that MethodType testType = MethodType.methodType(boolean.class, type); - MethodHandle unaryTest = CHECK_RHS.bindTo(clazz0).bindTo(clazz1); - test = unaryTest.asType(testType); + MethodHandle unaryTest = CHECK_RHS.bindTo(clazz1); + test = MethodHandles.dropArguments(unaryTest, 0, Object.class).asType(testType); nullCheck = MethodHandles.dropArguments(NON_NULL, 0, clazz0).asType(testType); } else { // case 3: check both receiver and argument MethodType testType = MethodType.methodType(boolean.class, type); - MethodHandle binaryTest = CHECK_BOTH.bindTo(clazz0).bindTo(clazz1); + MethodHandle binaryTest = MethodHandles.insertArguments(CHECK_BOTH, 0, clazz0, clazz1); test = binaryTest.asType(testType); nullCheck = BOTH_NON_NULL.asType(testType); } @@ -423,7 +423,7 @@ static boolean checkLHS(Class clazz, Object leftObject) { * guard method for inline caching: checks the first argument is the same * as the cached first argument. */ - static boolean checkRHS(Class left, Class right, Object leftObject, Object rightObject) { + static boolean checkRHS(Class right, Object rightObject) { return rightObject.getClass() == right; } @@ -460,7 +460,7 @@ static boolean bothNonNull(Object leftObject, Object rightObject) { CHECK_RHS = methodHandlesLookup.findStatic( methodHandlesLookup.lookupClass(), "checkRHS", - MethodType.methodType(boolean.class, Class.class, Class.class, Object.class, Object.class) + MethodType.methodType(boolean.class, Class.class, Object.class) ); CHECK_BOTH = methodHandlesLookup.findStatic( methodHandlesLookup.lookupClass(), diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/FunctionRef.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/FunctionRef.java index 816825a7700d..4da523bce634 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/FunctionRef.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/FunctionRef.java @@ -8,6 +8,7 @@ package org.elasticsearch.painless; +import org.elasticsearch.core.Strings; import org.elasticsearch.painless.lookup.PainlessConstructor; import org.elasticsearch.painless.lookup.PainlessLookup; import org.elasticsearch.painless.lookup.PainlessLookupUtility; @@ -22,7 +23,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.elasticsearch.painless.WriterConstants.CLASS_NAME; import static org.objectweb.asm.Opcodes.H_INVOKEINTERFACE; @@ -73,14 +74,12 @@ public static FunctionRef create( if (interfaceMethod == null) { throw new IllegalArgumentException( - "cannot convert function reference [" - + typeName - + "::" - + methodName - + "] " - + "to a non-functional interface [" - + targetClassName - + "]" + Strings.format( + "cannot convert function reference [%s::%s] to a non-functional interface [%s]", + typeName, + methodName, + targetClassName + ) ); } @@ -110,18 +109,14 @@ public static FunctionRef create( if (localFunction == null) { throw new IllegalArgumentException( - "function reference [this::" - + localFunctionKey - + "] " - + "matching [" - + targetClassName - + ", " - + interfaceMethodName - + "/" - + interfaceTypeParametersSize - + "] " - + "not found" - + (localFunctionKey.contains("$") ? " due to an incorrect number of arguments" : "") + Strings.format( + "function reference [this::%s] matching [%s, %s/%d] not found%s", + localFunctionKey, + targetClassName, + interfaceMethodName, + interfaceTypeParametersSize, + localFunctionKey.contains("$") ? " due to an incorrect number of arguments" : "" + ) ); } @@ -144,19 +139,14 @@ public static FunctionRef create( if (painlessConstructor == null) { throw new IllegalArgumentException( - "function reference [" - + typeName - + "::new/" - + interfaceTypeParametersSize - + "] " - + "matching [" - + targetClassName - + ", " - + interfaceMethodName - + "/" - + interfaceTypeParametersSize - + "] " - + "not found" + Strings.format( + "function reference [%s::new/%d] matching [%s, %s/%d] not found", + typeName, + interfaceTypeParametersSize, + targetClassName, + interfaceMethodName, + interfaceTypeParametersSize + ) ); } @@ -193,35 +183,25 @@ public static FunctionRef create( if (painlessMethod == null) { throw new IllegalArgumentException( - "function reference " - + "[" - + typeName - + "::" - + methodName - + "/" - + interfaceTypeParametersSize - + "] " - + "matching [" - + targetClassName - + ", " - + interfaceMethodName - + "/" - + interfaceTypeParametersSize - + "] " - + "not found" + Strings.format( + "function reference [%s::%s/%d] matching [%s, %s/%d] not found", + typeName, + methodName, + interfaceTypeParametersSize, + targetClassName, + interfaceMethodName, + interfaceTypeParametersSize + ) ); } } else if (captured) { throw new IllegalArgumentException( - "cannot use a static method as a function reference " - + "[" - + typeName - + "::" - + methodName - + "/" - + interfaceTypeParametersSize - + "] " - + "with a non-static captured variable" + Strings.format( + "cannot use a static method as a function reference [%s::%s/%d] with a non-static captured variable", + typeName, + methodName, + interfaceTypeParametersSize + ) ); } @@ -360,19 +340,20 @@ public String getFactoryMethodDescriptor() { if (factoryMethodReceiver == null) { return factoryMethodType.toMethodDescriptorString(); } - List arguments = factoryMethodType.parameterList().stream().map(Type::getType).collect(Collectors.toList()); - arguments.add(0, factoryMethodReceiver); - Type[] argArray = new Type[arguments.size()]; - arguments.toArray(argArray); - return Type.getMethodDescriptor(Type.getType(factoryMethodType.returnType()), argArray); + Type[] arguments = Stream.concat(Stream.of(factoryMethodReceiver), factoryMethodType.parameterList().stream().map(Type::getType)) + .toArray(Type[]::new); + return Type.getMethodDescriptor(Type.getType(factoryMethodType.returnType()), arguments); } /** Get the factory method type, updating the receiver if {@code factoryMethodReceiverClass} is non-null */ public Class[] factoryMethodParameters(Class factoryMethodReceiverClass) { - List> parameters = new ArrayList<>(factoryMethodType.parameterList()); + Class[] parameters = factoryMethodType.parameterList().toArray(Class[]::new); if (factoryMethodReceiverClass != null) { - parameters.add(0, factoryMethodReceiverClass); + Class[] withReceiver = new Class[parameters.length + 1]; + withReceiver[0] = factoryMethodReceiverClass; + System.arraycopy(parameters, 0, withReceiver, 1, parameters.length); + parameters = withReceiver; } - return parameters.toArray(new Class[0]); + return parameters; } } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/LambdaBootstrap.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/LambdaBootstrap.java index 54bfa3c86c71..a2fa5dea229d 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/LambdaBootstrap.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/LambdaBootstrap.java @@ -24,7 +24,6 @@ import java.security.AccessController; import java.security.PrivilegedAction; import java.util.List; -import java.util.stream.Collectors; import static java.lang.invoke.MethodHandles.Lookup; import static org.elasticsearch.painless.WriterConstants.CLASS_VERSION; @@ -400,9 +399,9 @@ private static void generateInterfaceMethod( iface.visitCode(); // Loads any captured variables onto the stack. - for (int captureCount = 0; captureCount < captures.length; ++captureCount) { + for (Capture capture : captures) { iface.loadThis(); - iface.getField(lambdaClassType, captures[captureCount].name, captures[captureCount].type); + iface.getField(lambdaClassType, capture.name, capture.type); } // Loads any passed in arguments onto the stack. @@ -442,13 +441,16 @@ private static void generateInterfaceMethod( delegateClassType = Type.getType(clazz); // functionalInterfaceWithCaptures needs to add the receiver and other captures - List parameters = interfaceMethodType.parameterList().stream().map(Type::getType).collect(Collectors.toList()); - parameters.add(0, delegateClassType); + Type[] parameters = new Type[captures.length + interfaceMethodType.parameterList().size()]; + int p = 0; + parameters[p++] = delegateClassType; for (int i = 1; i < captures.length; i++) { - parameters.add(i, captures[i].type); + parameters[p++] = captures[i].type; + } + for (Class pCls : interfaceMethodType.parameterList()) { + parameters[p++] = Type.getType(pCls); } - Type[] parametersArray = parameters.toArray(new Type[0]); - functionalInterfaceWithCaptures = Type.getMethodDescriptor(Type.getType(interfaceMethodType.returnType()), parametersArray); + functionalInterfaceWithCaptures = Type.getMethodDescriptor(Type.getType(interfaceMethodType.returnType()), parameters); // delegateMethod does not need the receiver List> factoryParameters = factoryMethodType.parameterList(); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/MethodWriter.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/MethodWriter.java index 878ff0e78b61..1e88edc788c6 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/MethodWriter.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/MethodWriter.java @@ -61,21 +61,10 @@ import static org.elasticsearch.painless.WriterConstants.DEF_TO_STRING_EXPLICIT; import static org.elasticsearch.painless.WriterConstants.DEF_TO_STRING_IMPLICIT; import static org.elasticsearch.painless.WriterConstants.DEF_UTIL_TYPE; -import static org.elasticsearch.painless.WriterConstants.INDY_STRING_CONCAT_BOOTSTRAP_HANDLE; import static org.elasticsearch.painless.WriterConstants.LAMBDA_BOOTSTRAP_HANDLE; -import static org.elasticsearch.painless.WriterConstants.MAX_INDY_STRING_CONCAT_ARGS; +import static org.elasticsearch.painless.WriterConstants.MAX_STRING_CONCAT_ARGS; import static org.elasticsearch.painless.WriterConstants.PAINLESS_ERROR_TYPE; -import static org.elasticsearch.painless.WriterConstants.STRINGBUILDER_APPEND_BOOLEAN; -import static org.elasticsearch.painless.WriterConstants.STRINGBUILDER_APPEND_CHAR; -import static org.elasticsearch.painless.WriterConstants.STRINGBUILDER_APPEND_DOUBLE; -import static org.elasticsearch.painless.WriterConstants.STRINGBUILDER_APPEND_FLOAT; -import static org.elasticsearch.painless.WriterConstants.STRINGBUILDER_APPEND_INT; -import static org.elasticsearch.painless.WriterConstants.STRINGBUILDER_APPEND_LONG; -import static org.elasticsearch.painless.WriterConstants.STRINGBUILDER_APPEND_OBJECT; -import static org.elasticsearch.painless.WriterConstants.STRINGBUILDER_APPEND_STRING; -import static org.elasticsearch.painless.WriterConstants.STRINGBUILDER_CONSTRUCTOR; -import static org.elasticsearch.painless.WriterConstants.STRINGBUILDER_TOSTRING; -import static org.elasticsearch.painless.WriterConstants.STRINGBUILDER_TYPE; +import static org.elasticsearch.painless.WriterConstants.STRING_CONCAT_BOOTSTRAP_HANDLE; import static org.elasticsearch.painless.WriterConstants.STRING_TO_CHAR; import static org.elasticsearch.painless.WriterConstants.STRING_TYPE; import static org.elasticsearch.painless.WriterConstants.UTILITY_TYPE; @@ -90,7 +79,7 @@ public final class MethodWriter extends GeneratorAdapter { private final BitSet statements; private final CompilerSettings settings; - private final Deque> stringConcatArgs = (INDY_STRING_CONCAT_BOOTSTRAP_HANDLE == null) ? null : new ArrayDeque<>(); + private final Deque> stringConcatArgs = new ArrayDeque<>(); public MethodWriter(int access, Method method, ClassVisitor cw, BitSet statements, CompilerSettings settings) { super( @@ -266,57 +255,28 @@ public static Type getType(Class clazz) { /** Starts a new string concat. * @return the size of arguments pushed to stack (the object that does string concats, e.g. a StringBuilder) */ - public int writeNewStrings() { - if (INDY_STRING_CONCAT_BOOTSTRAP_HANDLE != null) { - // Java 9+: we just push our argument collector onto deque - stringConcatArgs.push(new ArrayList<>()); - return 0; // nothing added to stack - } else { - // Java 8: create a StringBuilder in bytecode - newInstance(STRINGBUILDER_TYPE); - dup(); - invokeConstructor(STRINGBUILDER_TYPE, STRINGBUILDER_CONSTRUCTOR); - return 1; // StringBuilder on stack - } + public List writeNewStrings() { + List list = new ArrayList<>(); + stringConcatArgs.push(list); + return list; } public void writeAppendStrings(Class clazz) { - if (INDY_STRING_CONCAT_BOOTSTRAP_HANDLE != null) { - // Java 9+: record type information - stringConcatArgs.peek().add(getType(clazz)); - // prevent too many concat args. - // If there are too many, do the actual concat: - if (stringConcatArgs.peek().size() >= MAX_INDY_STRING_CONCAT_ARGS) { - writeToStrings(); - writeNewStrings(); - // add the return value type as new first param for next concat: - stringConcatArgs.peek().add(STRING_TYPE); - } - } else { - // Java 8: push a StringBuilder append - if (clazz == boolean.class) invokeVirtual(STRINGBUILDER_TYPE, STRINGBUILDER_APPEND_BOOLEAN); - else if (clazz == char.class) invokeVirtual(STRINGBUILDER_TYPE, STRINGBUILDER_APPEND_CHAR); - else if (clazz == byte.class || clazz == short.class || clazz == int.class) invokeVirtual( - STRINGBUILDER_TYPE, - STRINGBUILDER_APPEND_INT - ); - else if (clazz == long.class) invokeVirtual(STRINGBUILDER_TYPE, STRINGBUILDER_APPEND_LONG); - else if (clazz == float.class) invokeVirtual(STRINGBUILDER_TYPE, STRINGBUILDER_APPEND_FLOAT); - else if (clazz == double.class) invokeVirtual(STRINGBUILDER_TYPE, STRINGBUILDER_APPEND_DOUBLE); - else if (clazz == String.class) invokeVirtual(STRINGBUILDER_TYPE, STRINGBUILDER_APPEND_STRING); - else invokeVirtual(STRINGBUILDER_TYPE, STRINGBUILDER_APPEND_OBJECT); + List currentConcat = stringConcatArgs.peek(); + currentConcat.add(getType(clazz)); + // prevent too many concat args. + // If there are too many, do the actual concat: + if (currentConcat.size() >= MAX_STRING_CONCAT_ARGS) { + writeToStrings(); + currentConcat = writeNewStrings(); + // add the return value type as new first param for next concat: + currentConcat.add(STRING_TYPE); } } public void writeToStrings() { - if (INDY_STRING_CONCAT_BOOTSTRAP_HANDLE != null) { - // Java 9+: use type information and push invokeDynamic - final String desc = Type.getMethodDescriptor(STRING_TYPE, stringConcatArgs.pop().stream().toArray(Type[]::new)); - invokeDynamic("concat", desc, INDY_STRING_CONCAT_BOOTSTRAP_HANDLE); - } else { - // Java 8: call toString() on StringBuilder - invokeVirtual(STRINGBUILDER_TYPE, STRINGBUILDER_TOSTRING); - } + final String desc = Type.getMethodDescriptor(STRING_TYPE, stringConcatArgs.pop().toArray(Type[]::new)); + invokeDynamic("concat", desc, STRING_CONCAT_BOOTSTRAP_HANDLE); } /** Writes a dynamic binary instruction: returnType, lhs, and rhs can be different */ diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterConstants.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterConstants.java index b30d45733cec..26ead4361605 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterConstants.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterConstants.java @@ -17,10 +17,9 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; -import java.util.ArrayList; +import java.lang.invoke.StringConcatFactory; import java.util.Collection; import java.util.Iterator; -import java.util.List; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -163,39 +162,23 @@ public final class WriterConstants { false ); - /** dynamic invokedynamic bootstrap for indy string concats (Java 9+) */ - public static final Handle INDY_STRING_CONCAT_BOOTSTRAP_HANDLE; - static { - Handle bs; - try { - final Class factory = Class.forName("java.lang.invoke.StringConcatFactory"); - final String methodName = "makeConcat"; - final MethodType type = MethodType.methodType(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class); - // ensure it is there: - MethodHandles.publicLookup().findStatic(factory, methodName, type); - bs = new Handle(Opcodes.H_INVOKESTATIC, Type.getInternalName(factory), methodName, type.toMethodDescriptorString(), false); - } catch (ReflectiveOperationException e) { - // not Java 9 - we set it null, so MethodWriter uses StringBuilder: - bs = null; - } - INDY_STRING_CONCAT_BOOTSTRAP_HANDLE = bs; - } + public static final MethodType MAKE_CONCAT_TYPE = MethodType.methodType( + CallSite.class, + MethodHandles.Lookup.class, + String.class, + MethodType.class + ); + public static final Handle STRING_CONCAT_BOOTSTRAP_HANDLE = new Handle( + Opcodes.H_INVOKESTATIC, + Type.getInternalName(StringConcatFactory.class), + "makeConcat", + MAKE_CONCAT_TYPE.toMethodDescriptorString(), + false + ); - public static final int MAX_INDY_STRING_CONCAT_ARGS = 200; + public static final int MAX_STRING_CONCAT_ARGS = 200; public static final Type STRING_TYPE = Type.getType(String.class); - public static final Type STRINGBUILDER_TYPE = Type.getType(StringBuilder.class); - - public static final Method STRINGBUILDER_CONSTRUCTOR = getAsmMethod(void.class, CTOR_METHOD_NAME); - public static final Method STRINGBUILDER_APPEND_BOOLEAN = getAsmMethod(StringBuilder.class, "append", boolean.class); - public static final Method STRINGBUILDER_APPEND_CHAR = getAsmMethod(StringBuilder.class, "append", char.class); - public static final Method STRINGBUILDER_APPEND_INT = getAsmMethod(StringBuilder.class, "append", int.class); - public static final Method STRINGBUILDER_APPEND_LONG = getAsmMethod(StringBuilder.class, "append", long.class); - public static final Method STRINGBUILDER_APPEND_FLOAT = getAsmMethod(StringBuilder.class, "append", float.class); - public static final Method STRINGBUILDER_APPEND_DOUBLE = getAsmMethod(StringBuilder.class, "append", double.class); - public static final Method STRINGBUILDER_APPEND_STRING = getAsmMethod(StringBuilder.class, "append", String.class); - public static final Method STRINGBUILDER_APPEND_OBJECT = getAsmMethod(StringBuilder.class, "append", Object.class); - public static final Method STRINGBUILDER_TOSTRING = getAsmMethod(String.class, "toString"); public static final Type OBJECTS_TYPE = Type.getType(Objects.class); public static final Method EQUALS = getAsmMethod(boolean.class, "equals", Object.class, Object.class); @@ -203,12 +186,6 @@ public final class WriterConstants { public static final Type COLLECTION_TYPE = Type.getType(Collection.class); public static final Method COLLECTION_SIZE = getAsmMethod(int.class, "size"); - public static final Type LIST_TYPE = Type.getType(List.class); - public static final Method LIST_ADD = getAsmMethod(boolean.class, "add", Object.class); - - public static final Type ARRAY_LIST_TYPE = Type.getType(ArrayList.class); - public static final Method ARRAY_LIST_CTOR_WITH_SIZE = getAsmMethod(void.class, CTOR_METHOD_NAME, int.class); - private static Method getAsmMethod(final Class rtype, final String name, final Class... ptypes) { return new Method(name, MethodType.methodType(rtype, ptypes).toMethodDescriptorString()); } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultUserTreeToIRTreePhase.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultUserTreeToIRTreePhase.java index ddbdd2ce5ecb..4d34b68c1eaf 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultUserTreeToIRTreePhase.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultUserTreeToIRTreePhase.java @@ -905,7 +905,7 @@ public void visitAssignment(EAssignment userAssignmentNode, ScriptScope scriptSc irCompoundNode = stringConcatenationNode; // must handle the StringBuilder case for java version <= 8 - if (irLoadNode instanceof BinaryImplNode bin && WriterConstants.INDY_STRING_CONCAT_BOOTSTRAP_HANDLE == null) { + if (irLoadNode instanceof BinaryImplNode bin && WriterConstants.STRING_CONCAT_BOOTSTRAP_HANDLE == null) { bin.getLeftNode().attachDecoration(new IRDDepth(1)); } // handles when the operation is mathematical diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/ArrayTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/ArrayTests.java index 4a93a4dc6866..fbdce63036d5 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/ArrayTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/ArrayTests.java @@ -8,7 +8,6 @@ package org.elasticsearch.painless; -import org.apache.lucene.util.Constants; import org.hamcrest.Matcher; import java.lang.invoke.MethodHandle; @@ -34,7 +33,6 @@ protected Matcher outOfBoundsExceptionMessageMatcher(int index, int size } public void testArrayLengthHelper() throws Throwable { - assertEquals(Constants.JRE_IS_MINIMUM_JAVA9, Def.JAVA9_ARRAY_LENGTH_MH_FACTORY != null); assertArrayLength(2, new int[2]); assertArrayLength(3, new long[3]); assertArrayLength(4, new byte[4]); diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/StringTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/StringTests.java index 93ec854eb7e2..4c77c8f4ffce 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/StringTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/StringTests.java @@ -8,13 +8,11 @@ package org.elasticsearch.painless; -import org.apache.lucene.util.Constants; - import java.util.HashMap; import java.util.Map; import static java.util.Collections.singletonMap; -import static org.elasticsearch.painless.WriterConstants.MAX_INDY_STRING_CONCAT_ARGS; +import static org.elasticsearch.painless.WriterConstants.MAX_STRING_CONCAT_ARGS; public class StringTests extends ScriptTestCase { @@ -65,7 +63,7 @@ public void testAppendMultiple() { } public void testAppendMany() { - for (int i = MAX_INDY_STRING_CONCAT_ARGS - 5; i < MAX_INDY_STRING_CONCAT_ARGS + 5; i++) { + for (int i = MAX_STRING_CONCAT_ARGS - 5; i < MAX_STRING_CONCAT_ARGS + 5; i++) { doTestAppendMany(i); } } @@ -234,18 +232,14 @@ public void testBase64Augmentations() { assertEquals(rando, exec("params.rando.encodeBase64().decodeBase64()", singletonMap("rando", rando), true)); } - public void testJava9ConstantStringConcatBytecode() { - assumeTrue("Needs Java 9 to test indified String concat", Constants.JRE_IS_MINIMUM_JAVA9); - assertNotNull(WriterConstants.INDY_STRING_CONCAT_BOOTSTRAP_HANDLE); + public void testConstantStringConcatBytecode() { assertBytecodeExists( "String s = \"cat\"; return s + true + 'abc' + null;", "INVOKEDYNAMIC concat(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;" ); } - public void testJava9StringConcatBytecode() { - assumeTrue("Needs Java 9 to test indified String concat", Constants.JRE_IS_MINIMUM_JAVA9); - assertNotNull(WriterConstants.INDY_STRING_CONCAT_BOOTSTRAP_HANDLE); + public void testStringConcatBytecode() { assertBytecodeExists( "String s = \"cat\"; boolean t = true; Object u = null; return s + t + 'abc' + u;", "INVOKEDYNAMIC concat(Ljava/lang/String;ZLjava/lang/String;Ljava/lang/Object;)Ljava/lang/String;" @@ -260,10 +254,4 @@ public void testNullStringConcat() { assertEquals("" + 2 + null, exec("2 + '' + null")); assertEquals("" + null + 2, exec("null + '' + 2")); } - - public void testJava9NullStringConcatBytecode() { - assumeTrue("Needs Java 9 to test indified String concat", Constants.JRE_IS_MINIMUM_JAVA9); - assertNotNull(WriterConstants.INDY_STRING_CONCAT_BOOTSTRAP_HANDLE); - assertEquals("" + null + null, exec("'' + null + null")); - } } From deaf92878aced3370af8cff08507c625dca6c0f5 Mon Sep 17 00:00:00 2001 From: Rene Groeschke Date: Thu, 8 Dec 2022 11:11:13 +0100 Subject: [PATCH 204/919] Update to Gradle 7.6 (#89796) * Fix TestResultProcessor api changes * Fix inputs for generateProviderManifest * Ignore tests for now until gradle has fixed reporting issue * Fix dependency substitution in example plugins build * Use right java bin path on windows * Add hint to task onlyif when no docker is available --- .../gradle/wrapper/gradle-wrapper.properties | 5 ++-- ...elasticsearch.runtime-jdk-provision.gradle | 3 ++- .../elasticsearch/gradle/internal/Jdk.java | 7 +++++- .../internal/info/GlobalBuildInfoPlugin.java | 23 +++++++++--------- .../internal/test/DistroTestPlugin.java | 5 +++- .../test/StandaloneRestTestPlugin.java | 2 +- .../executer/RerunTestResultProcessor.java | 5 ++-- .../src/main/resources/minimumGradleVersion | 2 +- .../RerunTestResultProcessorTestSpec.groovy | 7 +++--- .../test/JavaRestTestPluginFuncTest.groovy | 2 ++ .../test/YamlRestTestPluginFuncTest.groovy | 2 ++ .../gradle/util/GradleUtils.java | 2 +- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 61574 bytes gradle/wrapper/gradle-wrapper.properties | 5 ++-- gradlew | 12 ++++++--- gradlew.bat | 1 + libs/x-content/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 5 ++-- plugins/examples/settings.gradle | 4 +-- 20 files changed, 59 insertions(+), 37 deletions(-) diff --git a/build-tools-internal/gradle/wrapper/gradle-wrapper.properties b/build-tools-internal/gradle/wrapper/gradle-wrapper.properties index e939ec976751..d1731eb7dbfa 100644 --- a/build-tools-internal/gradle/wrapper/gradle-wrapper.properties +++ b/build-tools-internal/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionSha256Sum=db9c8211ed63f61f60292c69e80d89196f9eb36665e369e7f00ac4cc841c2219 +distributionSha256Sum=312eb12875e1747e05c2f81a4789902d7e4ec5defbd1eefeaccc08acf096505d diff --git a/build-tools-internal/src/main/groovy/elasticsearch.runtime-jdk-provision.gradle b/build-tools-internal/src/main/groovy/elasticsearch.runtime-jdk-provision.gradle index 1d1112706fd0..e016374f3251 100644 --- a/build-tools-internal/src/main/groovy/elasticsearch.runtime-jdk-provision.gradle +++ b/build-tools-internal/src/main/groovy/elasticsearch.runtime-jdk-provision.gradle @@ -28,7 +28,8 @@ configure(allprojects) { } project.tasks.withType(Test).configureEach { Test test -> if (BuildParams.getIsRuntimeJavaHomeSet()) { - test.executable = "${BuildParams.runtimeJavaHome}/bin/java" + test.executable = "${BuildParams.runtimeJavaHome}/bin/java" + + (OS.current() == OS.WINDOWS ? '.exe' : '') } else { test.dependsOn(project.jdks.provisioned_runtime) test.executable = rootProject.jdks.provisioned_runtime.getBinJavaPath() diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/Jdk.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/Jdk.java index 09f655a7aece..deb556397d09 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/Jdk.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/Jdk.java @@ -152,11 +152,16 @@ public Object getBinJavaPath() { return new Object() { @Override public String toString() { - return getHomeRoot() + "/bin/java"; + return getHomeRoot() + getPlatformBinPath(); } }; } + private String getPlatformBinPath() { + boolean isWindows = "windows".equals(getPlatform()); + return "/bin/java" + (isWindows ? ".exe" : ""); + } + public Object getJavaHomePath() { return new Object() { @Override diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/GlobalBuildInfoPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/GlobalBuildInfoPlugin.java index f730c700f62b..b741be730407 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/GlobalBuildInfoPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/GlobalBuildInfoPlugin.java @@ -102,7 +102,7 @@ public void apply(Project project) { ) ); params.setIsRuntimeJavaHomeSet(isRuntimeJavaHomeSet); - JvmInstallationMetadata runtimeJdkMetaData = metadataDetector.getMetadata(getJavaInstallation(runtimeJavaHome).getLocation()); + JvmInstallationMetadata runtimeJdkMetaData = metadataDetector.getMetadata(getJavaInstallation(runtimeJavaHome)); params.setRuntimeJavaDetails(formatJavaVendorDetails(runtimeJdkMetaData)); params.setJavaVersions(getAvailableJavaVersions()); params.setMinimumCompilerVersion(minimumCompilerVersion); @@ -133,7 +133,7 @@ public void apply(Project project) { private Provider resolveToolchainSpecFromEnv() { return providers.environmentVariable("JAVA_TOOLCHAIN_HOME").map(toolChainEnvVariable -> { File toolChainDir = new File(toolChainEnvVariable); - JvmInstallationMetadata metadata = metadataDetector.getMetadata(toolChainDir); + JvmInstallationMetadata metadata = metadataDetector.getMetadata(getJavaInstallation(toolChainDir)); if (metadata.isValidInstallation() == false) { throw new GradleException( "Configured JAVA_TOOLCHAIN_HOME " + toolChainEnvVariable + " does not point to a valid jdk installation." @@ -166,17 +166,17 @@ private void logGlobalBuildInfo() { final String osVersion = System.getProperty("os.version"); final String osArch = System.getProperty("os.arch"); final Jvm gradleJvm = Jvm.current(); - JvmInstallationMetadata gradleJvmMetadata = metadataDetector.getMetadata(gradleJvm.getJavaHome()); + JvmInstallationMetadata gradleJvmMetadata = metadataDetector.getMetadata(getJavaInstallation(gradleJvm.getJavaHome())); final String gradleJvmVendorDetails = gradleJvmMetadata.getVendor().getDisplayName(); - final String gradleJvmImplementationVersion = gradleJvmMetadata.getImplementationVersion(); + final String gradleJvmImplementationVersion = gradleJvmMetadata.getJvmVersion(); LOGGER.quiet("======================================="); LOGGER.quiet("Elasticsearch Build Hamster says Hello!"); LOGGER.quiet(" Gradle Version : " + GradleVersion.current().getVersion()); LOGGER.quiet(" OS Info : " + osName + " " + osVersion + " (" + osArch + ")"); if (BuildParams.getIsRuntimeJavaHomeSet()) { - JvmInstallationMetadata runtimeJvm = metadataDetector.getMetadata(BuildParams.getRuntimeJavaHome()); + JvmInstallationMetadata runtimeJvm = metadataDetector.getMetadata(getJavaInstallation(BuildParams.getRuntimeJavaHome())); final String runtimeJvmVendorDetails = runtimeJvm.getVendor().getDisplayName(); - final String runtimeJvmImplementationVersion = runtimeJvm.getImplementationVersion(); + final String runtimeJvmImplementationVersion = runtimeJvm.getJvmVersion(); final String runtimeVersion = runtimeJvm.getRuntimeVersion(); final String runtimeExtraDetails = runtimeJvmVendorDetails + ", " + runtimeVersion; LOGGER.quiet(" Runtime JDK Version : " + runtimeJvmImplementationVersion + " (" + runtimeExtraDetails + ")"); @@ -198,7 +198,7 @@ private void logGlobalBuildInfo() { private JavaVersion determineJavaVersion(String description, File javaHome, JavaVersion requiredVersion) { InstallationLocation installation = getJavaInstallation(javaHome); - JavaVersion actualVersion = metadataDetector.getMetadata(installation.getLocation()).getLanguageVersion(); + JavaVersion actualVersion = metadataDetector.getMetadata(installation).getLanguageVersion(); if (actualVersion.isCompatibleWith(requiredVersion) == false) { throwInvalidJavaHomeException( description, @@ -231,10 +231,9 @@ private boolean isSameFile(File javaHome, InstallationLocation installationLocat */ private List getAvailableJavaVersions() { return getAvailableJavaInstallationLocationSteam().map(installationLocation -> { - File installationDir = installationLocation.getLocation(); - JvmInstallationMetadata metadata = metadataDetector.getMetadata(installationDir); + JvmInstallationMetadata metadata = metadataDetector.getMetadata(installationLocation); int actualVersion = Integer.parseInt(metadata.getLanguageVersion().getMajorVersion()); - return JavaHome.of(actualVersion, providers.provider(() -> installationDir)); + return JavaHome.of(actualVersion, providers.provider(() -> installationLocation.getLocation())); }).collect(Collectors.toList()); } @@ -357,8 +356,8 @@ private static class ErrorTraceMetadataDetector implements JvmMetadataDetector { } @Override - public JvmInstallationMetadata getMetadata(File file) { - JvmInstallationMetadata metadata = delegate.getMetadata(file); + public JvmInstallationMetadata getMetadata(InstallationLocation installationLocation) { + JvmInstallationMetadata metadata = delegate.getMetadata(installationLocation); if (metadata instanceof JvmInstallationMetadata.FailureInstallationMetadata) { throw new GradleException("Jvm Metadata cannot be resolved for " + metadata.getJavaHome().toString()); } diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/DistroTestPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/DistroTestPlugin.java index dac21dc78aa3..bd702097a700 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/DistroTestPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/DistroTestPlugin.java @@ -127,7 +127,10 @@ public void apply(Project project) { depsTask.configure(t -> t.dependsOn(examplePlugin.getDependencies())); depsTasks.put(taskname, depsTask); TaskProvider destructiveTask = configureTestTask(project, taskname, distribution, t -> { - t.onlyIf(t2 -> distribution.isDocker() == false || dockerSupport.get().getDockerAvailability().isAvailable()); + t.onlyIf( + "Docker is not available", + t2 -> distribution.isDocker() == false || dockerSupport.get().getDockerAvailability().isAvailable() + ); addDistributionSysprop(t, DISTRIBUTION_SYSPROP, distribution::getFilepath); addDistributionSysprop(t, EXAMPLE_PLUGIN_SYSPROP, () -> examplePlugin.getSingleFile().toString()); t.exclude("**/PackageUpgradeTests.class"); diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/StandaloneRestTestPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/StandaloneRestTestPlugin.java index 9ffaf396e7fb..888fd6821393 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/StandaloneRestTestPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/StandaloneRestTestPlugin.java @@ -71,7 +71,7 @@ public void apply(final Project project) { ); IdeaModel idea = project.getExtensions().getByType(IdeaModel.class); - idea.getModule().getTestSourceDirs().addAll(testSourceSet.getJava().getSrcDirs()); + idea.getModule().getTestSources().from(testSourceSet.getJava().getSrcDirs()); idea.getModule() .getScopes() .put( diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rerun/executer/RerunTestResultProcessor.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rerun/executer/RerunTestResultProcessor.java index 06bd7679a654..39b21df53133 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rerun/executer/RerunTestResultProcessor.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rerun/executer/RerunTestResultProcessor.java @@ -12,6 +12,7 @@ import org.gradle.api.internal.tasks.testing.TestDescriptorInternal; import org.gradle.api.internal.tasks.testing.TestResultProcessor; import org.gradle.api.internal.tasks.testing.TestStartEvent; +import org.gradle.api.tasks.testing.TestFailure; import org.gradle.api.tasks.testing.TestOutputEvent; import java.util.ArrayList; @@ -89,11 +90,11 @@ public void output(Object testId, TestOutputEvent testOutputEvent) { } @Override - public void failure(Object testId, Throwable throwable) { + public void failure(Object testId, TestFailure result) { if (activeDescriptorsById.containsKey(testId)) { activeDescriptorsById.remove(testId); try { - delegate.failure(testId, throwable); + delegate.failure(testId, result); } catch (IllegalArgumentException illegalArgumentException) { logTracing(testId, illegalArgumentException); } diff --git a/build-tools-internal/src/main/resources/minimumGradleVersion b/build-tools-internal/src/main/resources/minimumGradleVersion index 7501d508f743..f5cce03c304f 100644 --- a/build-tools-internal/src/main/resources/minimumGradleVersion +++ b/build-tools-internal/src/main/resources/minimumGradleVersion @@ -1 +1 @@ -7.5.1 \ No newline at end of file +7.6 \ No newline at end of file diff --git a/build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/test/rerun/executer/RerunTestResultProcessorTestSpec.groovy b/build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/test/rerun/executer/RerunTestResultProcessorTestSpec.groovy index bfcfea3e2da1..507a99f080df 100644 --- a/build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/test/rerun/executer/RerunTestResultProcessorTestSpec.groovy +++ b/build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/test/rerun/executer/RerunTestResultProcessorTestSpec.groovy @@ -12,6 +12,7 @@ import org.gradle.api.internal.tasks.testing.TestCompleteEvent import org.gradle.api.internal.tasks.testing.TestDescriptorInternal import org.gradle.api.internal.tasks.testing.TestResultProcessor import org.gradle.api.internal.tasks.testing.TestStartEvent +import org.gradle.api.tasks.testing.TestFailure import org.gradle.api.tasks.testing.TestOutputEvent import spock.lang.Specification @@ -75,20 +76,20 @@ class RerunTestResultProcessorTestSpec extends Specification { def testDescriptor2 = descriptor("testId2") def testStartEvent2 = startEvent("testId2") - def testError2 = Mock(Throwable) + def testFailure = Mock(TestFailure) when: processor.started(rootDescriptor, rootTestStartEvent) processor.started(testDescriptor1, testStartEvent1) processor.started(testDescriptor2, testStartEvent2) - processor.failure("testId2", testError2) + processor.failure("testId2", testFailure) processor.completed("rootId", rootCompleteEvent) then: 1 * delegate.started(rootDescriptor, rootTestStartEvent) 1 * delegate.started(testDescriptor1, testStartEvent1) 1 * delegate.started(testDescriptor2, testStartEvent2) - 1 * delegate.failure("testId2", testError2) + 1 * delegate.failure("testId2", testFailure) 0 * delegate.completed("rootId", rootCompleteEvent) when: diff --git a/build-tools/src/integTest/groovy/org/elasticsearch/gradle/test/JavaRestTestPluginFuncTest.groovy b/build-tools/src/integTest/groovy/org/elasticsearch/gradle/test/JavaRestTestPluginFuncTest.groovy index 2d6d122663da..1d3ac0030a0b 100644 --- a/build-tools/src/integTest/groovy/org/elasticsearch/gradle/test/JavaRestTestPluginFuncTest.groovy +++ b/build-tools/src/integTest/groovy/org/elasticsearch/gradle/test/JavaRestTestPluginFuncTest.groovy @@ -11,6 +11,7 @@ package org.elasticsearch.gradle.test import org.elasticsearch.gradle.VersionProperties import org.elasticsearch.gradle.fixtures.AbstractGradleFuncTest import org.gradle.testkit.runner.TaskOutcome +import spock.lang.Ignore class JavaRestTestPluginFuncTest extends AbstractGradleFuncTest { @@ -19,6 +20,7 @@ class JavaRestTestPluginFuncTest extends AbstractGradleFuncTest { configurationCacheCompatible = false } + @Ignore('https://github.com/gradle/gradle/issues/21868') def "declares default dependencies"() { given: buildFile << """ diff --git a/build-tools/src/integTest/groovy/org/elasticsearch/gradle/test/YamlRestTestPluginFuncTest.groovy b/build-tools/src/integTest/groovy/org/elasticsearch/gradle/test/YamlRestTestPluginFuncTest.groovy index 83c215ceea42..1598d6ab06ff 100644 --- a/build-tools/src/integTest/groovy/org/elasticsearch/gradle/test/YamlRestTestPluginFuncTest.groovy +++ b/build-tools/src/integTest/groovy/org/elasticsearch/gradle/test/YamlRestTestPluginFuncTest.groovy @@ -11,6 +11,7 @@ package org.elasticsearch.gradle.test import org.elasticsearch.gradle.VersionProperties import org.elasticsearch.gradle.fixtures.AbstractGradleFuncTest import org.gradle.testkit.runner.TaskOutcome +import spock.lang.Ignore class YamlRestTestPluginFuncTest extends AbstractGradleFuncTest { @@ -19,6 +20,7 @@ class YamlRestTestPluginFuncTest extends AbstractGradleFuncTest { configurationCacheCompatible = false } + @Ignore('https://github.com/gradle/gradle/issues/21868') def "declares default dependencies"() { given: buildFile << """ diff --git a/build-tools/src/main/java/org/elasticsearch/gradle/util/GradleUtils.java b/build-tools/src/main/java/org/elasticsearch/gradle/util/GradleUtils.java index 134b24fec16e..ce69c4ec476f 100644 --- a/build-tools/src/main/java/org/elasticsearch/gradle/util/GradleUtils.java +++ b/build-tools/src/main/java/org/elasticsearch/gradle/util/GradleUtils.java @@ -127,7 +127,7 @@ public static void setupIdeForTestSourceSet(Project project, SourceSet testSourc Configuration runtimeClasspathConfiguration = project.getConfigurations().getByName(runtimeClasspathName); project.getPluginManager().withPlugin("idea", p -> { IdeaModel idea = project.getExtensions().getByType(IdeaModel.class); - idea.getModule().setTestSourceDirs(testSourceSet.getJava().getSrcDirs()); + idea.getModule().getTestSources().from(testSourceSet.getJava().getSrcDirs()); idea.getModule().getScopes().put(testSourceSet.getName(), Map.of("plus", List.of(runtimeClasspathConfiguration))); }); project.getPluginManager().withPlugin("eclipse", p -> { diff --git a/build.gradle b/build.gradle index e1e11e60e110..e6ad0f439ce6 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ import org.elasticsearch.gradle.internal.info.BuildParams import org.elasticsearch.gradle.util.GradleUtils import org.gradle.plugins.ide.eclipse.model.AccessRule import org.gradle.plugins.ide.eclipse.model.ProjectDependency -import org.gradle.util.DistributionLocator +import org.gradle.util.internal.DistributionLocator import org.gradle.util.GradleVersion import java.nio.file.Files diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f090a2944b7473328c07c9755baa3196..943f0cbfa754578e88a3dae77fce6e3dea56edbf 100644 GIT binary patch delta 36524 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%0DGK?7GHTj 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 VwlMN}`%er%aGR6olj~j${vQ;P=LY}) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e939ec976751..d1731eb7dbfa 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionSha256Sum=db9c8211ed63f61f60292c69e80d89196f9eb36665e369e7f00ac4cc841c2219 +distributionSha256Sum=312eb12875e1747e05c2f81a4789902d7e4ec5defbd1eefeaccc08acf096505d diff --git a/gradlew b/gradlew index a69d9cb6c206..65dcd68d65c8 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,10 +80,10 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' @@ -143,12 +143,16 @@ fi if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac diff --git a/gradlew.bat b/gradlew.bat index 53a6b238d414..6689b85beecd 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% diff --git a/libs/x-content/build.gradle b/libs/x-content/build.gradle index bf110b9adb53..04785711bda8 100644 --- a/libs/x-content/build.gradle +++ b/libs/x-content/build.gradle @@ -69,7 +69,7 @@ tasks.named("dependencyLicenses").configure { File generatedResourcesDir = new File(buildDir, 'generated-resources') def generateProviderManifest = tasks.register("generateProviderManifest") { File manifestFile = new File(generatedResourcesDir, "LISTING.TXT") - inputs.property('jars', configurations.providerImpl) + inputs.files('jars', configurations.providerImpl).withPathSensitivity(PathSensitivity.RELATIVE) outputs.file(manifestFile) doLast { manifestFile.parentFile.mkdirs() diff --git a/plugins/examples/gradle/wrapper/gradle-wrapper.properties b/plugins/examples/gradle/wrapper/gradle-wrapper.properties index e939ec976751..d1731eb7dbfa 100644 --- a/plugins/examples/gradle/wrapper/gradle-wrapper.properties +++ b/plugins/examples/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionSha256Sum=db9c8211ed63f61f60292c69e80d89196f9eb36665e369e7f00ac4cc841c2219 +distributionSha256Sum=312eb12875e1747e05c2f81a4789902d7e4ec5defbd1eefeaccc08acf096505d diff --git a/plugins/examples/settings.gradle b/plugins/examples/settings.gradle index 2b4796eb061f..2fac4c21620d 100644 --- a/plugins/examples/settings.gradle +++ b/plugins/examples/settings.gradle @@ -36,8 +36,8 @@ gradle.projectsEvaluated { // When using composite builds we need to tell Gradle to use the project names since we rename the published artifacts substitute module('org.elasticsearch:elasticsearch') using module("org.elasticsearch:server:${elasticsearchVersion}") substitute module('org.elasticsearch.client:elasticsearch-rest-client') using module("org.elasticsearch.client:rest:${elasticsearchVersion}") - substitute module('org.elasticsearch.plugin:x-pack-core') with module("org.elasticsearch.plugin:core:${elasticsearchVersion}") - substitute module('org.elasticsearch.plugin:elasticsearch-scripting-painless-spi') with module("org.elasticsearch.plugin:spi:${elasticsearchVersion}") + substitute module('org.elasticsearch.plugin:x-pack-core') using module("org.elasticsearch.plugin:core:${elasticsearchVersion}") + substitute module('org.elasticsearch.plugin:elasticsearch-scripting-painless-spi') using module("org.elasticsearch.plugin:spi:${elasticsearchVersion}") substitute module('org.elasticsearch.distribution.integ-test-zip:elasticsearch') using variant(module("org.elasticsearch.distribution.integ-test-zip:integ-test-zip:${elasticsearchVersion}")) { attributes { attribute(Attribute.of("composite", Boolean.class), true) From 64a52ce18fdc955da63800dfa2a6d7fe2d4c4923 Mon Sep 17 00:00:00 2001 From: Keith Massey Date: Thu, 8 Dec 2022 08:49:12 -0600 Subject: [PATCH 205/919] Forwarding simulate calls to ingest nodes (#92171) Calls to the simulate pipeline API are now forwarded to an ingest node. If there is no ingest node, the request now fails. --- docs/changelog/92171.yaml | 5 + .../test/ingest_geoip/20_geoip_processor.yml | 36 + .../ingest_mustache/10_ingest_disabled.yml | 8 +- qa/smoke-test-multinode/build.gradle | 2 + .../test/smoke_test_multinode/40_simulate.yml | 978 ++++++++++++++++++ .../SimulatePipelineTransportAction.java | 100 +- .../ingest/SimulateProcessorResult.java | 13 +- .../common/settings/ClusterSettings.java | 2 + .../ingest/SimulateProcessorResultTests.java | 4 +- 9 files changed, 1124 insertions(+), 24 deletions(-) create mode 100644 docs/changelog/92171.yaml create mode 100644 qa/smoke-test-multinode/src/yamlRestTest/resources/rest-api-spec/test/smoke_test_multinode/40_simulate.yml diff --git a/docs/changelog/92171.yaml b/docs/changelog/92171.yaml new file mode 100644 index 000000000000..fa98bc20d576 --- /dev/null +++ b/docs/changelog/92171.yaml @@ -0,0 +1,5 @@ +pr: 92171 +summary: Forwarding simulate calls to ingest nodes +area: Ingest Node +type: bug +issues: [] diff --git a/modules/ingest-geoip/src/yamlRestTest/resources/rest-api-spec/test/ingest_geoip/20_geoip_processor.yml b/modules/ingest-geoip/src/yamlRestTest/resources/rest-api-spec/test/ingest_geoip/20_geoip_processor.yml index b09dac97eba2..84801cfb9ada 100644 --- a/modules/ingest-geoip/src/yamlRestTest/resources/rest-api-spec/test/ingest_geoip/20_geoip_processor.yml +++ b/modules/ingest-geoip/src/yamlRestTest/resources/rest-api-spec/test/ingest_geoip/20_geoip_processor.yml @@ -309,3 +309,39 @@ - match: { _source.geoip.asn: 29518 } - match: { _source.geoip.organization_name: "Bredband2 AB" } - match: { _source.geoip.network: "89.160.0.0/17" } + +--- +"Test simulate with Geoip Processor": +- do: + ingest.put_pipeline: + id: "pipeline1" + body: > + { + "processors": [ + { + "geoip": { + "field": "source.ip", + "target_field": "source.geo" + } + } + ] + } +- match: { acknowledged: true } + +- do: + ingest.simulate: + id: "pipeline1" + body: > + { + "docs": [ + { + "_source": { + "source": { + "ip": "89.160.20.128" + } + } + } + ] + } +- length: { docs: 1 } +- match: { docs.0.doc._source.source.geo.city_name: "Linköping" } diff --git a/qa/smoke-test-ingest-disabled/src/yamlRestTest/resources/rest-api-spec/test/ingest_mustache/10_ingest_disabled.yml b/qa/smoke-test-ingest-disabled/src/yamlRestTest/resources/rest-api-spec/test/ingest_mustache/10_ingest_disabled.yml index ed3c5f6f9228..0eb64954a45b 100644 --- a/qa/smoke-test-ingest-disabled/src/yamlRestTest/resources/rest-api-spec/test/ingest_mustache/10_ingest_disabled.yml +++ b/qa/smoke-test-ingest-disabled/src/yamlRestTest/resources/rest-api-spec/test/ingest_mustache/10_ingest_disabled.yml @@ -28,7 +28,7 @@ - match: { acknowledged: true } --- -"Test ingest simulate API works fine when node.ingest is set to false": +"Test ingest simulate API fails when node.ingest is set to false": - do: ingest.put_pipeline: id: "my_pipeline" @@ -47,6 +47,7 @@ - match: { acknowledged: true } - do: + catch: /There are no ingest nodes in this cluster, unable to forward request to an ingest node./ ingest.simulate: id: "my_pipeline" body: > @@ -61,11 +62,6 @@ } ] } - - length: { docs: 1 } - - match: { docs.0.doc._source.foo: "bar" } - - match: { docs.0.doc._source.field2: "_value" } - - length: { docs.0.doc._ingest: 1 } - - is_true: docs.0.doc._ingest.timestamp --- "Test index api with pipeline id fails when node.ingest is set to false": diff --git a/qa/smoke-test-multinode/build.gradle b/qa/smoke-test-multinode/build.gradle index 37615f938213..8fbe7229facd 100644 --- a/qa/smoke-test-multinode/build.gradle +++ b/qa/smoke-test-multinode/build.gradle @@ -22,6 +22,8 @@ File repo = file("$buildDir/testclusters/repo") testClusters.matching { it.name == "yamlRestTest" }.configureEach { numberOfNodes = 2 setting 'path.repo', repo.absolutePath + // The first node does not have the ingest role so we're sure ingest requests are forwarded: + nodes."yamlRestTest-0".setting 'node.roles', '[master,data,ml,remote_cluster_client,transform]' } testClusters.configureEach { diff --git a/qa/smoke-test-multinode/src/yamlRestTest/resources/rest-api-spec/test/smoke_test_multinode/40_simulate.yml b/qa/smoke-test-multinode/src/yamlRestTest/resources/rest-api-spec/test/smoke_test_multinode/40_simulate.yml new file mode 100644 index 000000000000..eb27b021eb11 --- /dev/null +++ b/qa/smoke-test-multinode/src/yamlRestTest/resources/rest-api-spec/test/smoke_test_multinode/40_simulate.yml @@ -0,0 +1,978 @@ +--- +teardown: + - do: + ingest.delete_pipeline: + id: "my_pipeline" + ignore: 404 + +--- +"Test simulate with stored ingest pipeline": + - do: + ingest.put_pipeline: + id: "my_pipeline" + body: > + { + "description": "_description", + "processors": [ + { + "set" : { + "field" : "field2", + "value" : "_value" + } + } + ] + } + - match: { acknowledged: true } + + - do: + ingest.simulate: + id: "my_pipeline" + body: > + { + "docs": [ + { + "_index": "index", + "_id": "id", + "_source": { + "foo": "bar" + } + } + ] + } + - length: { docs: 1 } + - match: { docs.0.doc._source.foo: "bar" } + - match: { docs.0.doc._source.field2: "_value" } + - length: { docs.0.doc._ingest: 1 } + - is_true: docs.0.doc._ingest.timestamp + +--- +"Test simulate with provided pipeline definition": + - do: + ingest.simulate: + body: > + { + "pipeline": { + "description": "_description", + "processors": [ + { + "set" : { + "field" : "field2", + "value" : "_value" + } + } + ] + }, + "docs": [ + { + "_index": "index", + "_id": "id", + "_source": { + "foo": "bar" + } + } + ] + } + - length: { docs: 1 } + +--- +"Test simulate with provided invalid pipeline definition": + - do: + catch: bad_request + ingest.simulate: + body: > + { + "pipeline": { + "description": "_description", + "processors": [ + { + "set" : { + "tag" : "fails", + "value" : "_value" + } + } + ] + }, + "docs": [ + { + "_index": "index", + "_id": "id", + "_source": { + "foo": "bar" + } + } + ] + } + - match: { error.root_cause.0.type: "parse_exception" } + - match: { error.root_cause.0.reason: "[field] required property is missing" } + - match: { error.root_cause.0.processor_tag: "fails" } + - match: { error.root_cause.0.processor_type: "set" } + - match: { error.root_cause.0.property_name: "field" } + +--- +"Test simulate without id": + - do: + ingest.simulate: + body: > + { + "pipeline": { + "description": "_description", + "processors": [ + { + "set" : { + "field" : "field2", + "value" : "_value" + } + } + ] + }, + "docs": [ + { + "_source": { + "foo": "bar" + } + } + ] + } + - length: { docs: 1 } + +--- +"Test simulate with provided pipeline definition with on_failure block": + - do: + ingest.simulate: + body: > + { + "pipeline": { + "description": "_description", + "processors": [ + { + "rename" : { + "field" : "does_not_exist", + "target_field" : "field2", + "on_failure" : [ + { + "set" : { + "field" : "field2", + "value" : "_value" + } + } + ] + } + } + ] + }, + "docs": [ + { + "_index": "index", + "_id": "id", + "_source": { + "foo": "bar" + } + } + ] + } + - length: { docs: 1 } + - match: { docs.0.doc._source.foo: "bar" } + - match: { docs.0.doc._source.field2: "_value" } + - length: { docs.0.doc._ingest: 1 } + - is_true: docs.0.doc._ingest.timestamp + +--- +"Test simulate with no provided pipeline or pipeline_id": + - do: + catch: bad_request + ingest.simulate: + body: > + { + "docs": [ + { + "_index": "index", + "_id": "id", + "_source": { + "foo": "bar" + } + } + ] + } + - is_false: error.root_cause.0.processor_type + - is_false: error.root_cause.0.processor_tag + - match: { error.root_cause.0.property_name: "pipeline" } + - match: { error.reason: "[pipeline] required property is missing" } + +--- +"Test simulate with invalid processor config": + - do: + catch: bad_request + ingest.simulate: + body: > + { + "pipeline": { + "description": "_description", + "processors": [ + { + "set" : { + "field" : "field2" + } + } + ] + }, + "docs": [ + { + "_index": "index", + "_id": "id", + "_source": { + "foo": "bar" + } + } + ] + } + - match: { error.root_cause.0.type: "parse_exception" } + - match: { error.root_cause.0.reason: "[value] required property is missing" } + - match: { error.root_cause.0.processor_type: "set" } + - match: { error.root_cause.0.property_name: "value" } + - is_false: error.root_cause.0.processor_tag + +--- +"Test simulate with verbose flag": + - do: + ingest.simulate: + verbose: true + body: > + { + "pipeline": { + "description": "_description", + "processors": [ + { + "set" : { + "tag" : "processor[set]-0", + "field" : "field2.value", + "value" : "_value" + } + }, + { + "set" : { + "field" : "field3", + "value" : "third_val" + } + }, + { + "uppercase" : { + "field" : "field2.value" + } + }, + { + "lowercase" : { + "field" : "foo.bar.0.item" + } + } + ] + }, + "docs": [ + { + "_index": "index", + "_id": "id", + "_source": { + "foo": { + "bar" : [ {"item": "HELLO"} ] + } + } + } + ] + } + - length: { docs: 1 } + - length: { docs.0.processor_results: 4 } + - match: { docs.0.processor_results.0.tag: "processor[set]-0" } + - length: { docs.0.processor_results.0.doc._source: 2 } + - match: { docs.0.processor_results.0.doc._source.foo.bar.0.item: "HELLO" } + - match: { docs.0.processor_results.0.doc._source.field2.value: "_value" } + - length: { docs.0.processor_results.0.doc._ingest: 2 } + - is_true: docs.0.processor_results.0.doc._ingest.timestamp + - is_true: docs.0.processor_results.0.doc._ingest.pipeline + - length: { docs.0.processor_results.1.doc._source: 3 } + - match: { docs.0.processor_results.1.doc._source.foo.bar.0.item: "HELLO" } + - match: { docs.0.processor_results.1.doc._source.field2.value: "_value" } + - match: { docs.0.processor_results.1.doc._source.field3: "third_val" } + - length: { docs.0.processor_results.1.doc._ingest: 2 } + - is_true: docs.0.processor_results.1.doc._ingest.timestamp + - is_true: docs.0.processor_results.1.doc._ingest.pipeline + - length: { docs.0.processor_results.2.doc._source: 3 } + - match: { docs.0.processor_results.2.doc._source.foo.bar.0.item: "HELLO" } + - match: { docs.0.processor_results.2.doc._source.field2.value: "_VALUE" } + - match: { docs.0.processor_results.2.doc._source.field3: "third_val" } + - length: { docs.0.processor_results.2.doc._ingest: 2 } + - is_true: docs.0.processor_results.2.doc._ingest.timestamp + - is_true: docs.0.processor_results.2.doc._ingest.pipeline + - length: { docs.0.processor_results.3.doc._source: 3 } + - match: { docs.0.processor_results.3.doc._source.foo.bar.0.item: "hello" } + - match: { docs.0.processor_results.3.doc._source.field2.value: "_VALUE" } + - match: { docs.0.processor_results.3.doc._source.field3: "third_val" } + - length: { docs.0.processor_results.3.doc._ingest: 2 } + - is_true: docs.0.processor_results.3.doc._ingest.timestamp + - is_true: docs.0.processor_results.3.doc._ingest.pipeline + +--- +"Test simulate with exception thrown": + - do: + ingest.simulate: + body: > + { + "pipeline": { + "description": "_description", + "processors": [ + { + "uppercase" : { + "field" : "foo" + } + } + ] + }, + "docs": [ + { + "_index": "index", + "_id": "id", + "_source": { + "not_foo": "bar" + } + }, + { + "_index": "index", + "_id": "id2", + "_source": { + "foo": "bar" + } + } + ] + } + - length: { docs: 2 } + - match: { docs.0.error.type: "illegal_argument_exception" } + - match: { docs.1.doc._source.foo: "BAR" } + - length: { docs.1.doc._ingest: 1 } + - is_true: docs.1.doc._ingest.timestamp + +--- +"Test verbose simulate with exception thrown": + - do: + ingest.simulate: + verbose: true + body: > + { + "pipeline": { + "description": "_description", + "processors": [ + { + "convert" : { + "field" : "foo", + "type" : "integer" + } + }, + { + "uppercase" : { + "field" : "bar" + } + } + ] + }, + "docs": [ + { + "_index": "index", + "_id": "id", + "_source": { + "foo": "bar", + "bar": "hello" + } + }, + { + "_index": "index", + "_id": "id2", + "_source": { + "foo": "5", + "bar": "hello" + } + } + ] + } + - length: { docs: 2 } + - length: { docs.0.processor_results: 1 } + - match: { docs.0.processor_results.0.error.type: "illegal_argument_exception" } + - length: { docs.1.processor_results: 2 } + - match: { docs.1.processor_results.0.doc._index: "index" } + - match: { docs.1.processor_results.0.doc._source.foo: 5 } + - match: { docs.1.processor_results.0.doc._source.bar: "hello" } + - length: { docs.1.processor_results.0.doc._ingest: 2 } + - is_true: docs.1.processor_results.0.doc._ingest.timestamp + - is_true: docs.1.processor_results.0.doc._ingest.pipeline + - match: { docs.1.processor_results.1.doc._source.foo: 5 } + - match: { docs.1.processor_results.1.doc._source.bar: "HELLO" } + - length: { docs.1.processor_results.1.doc._ingest: 2 } + - is_true: docs.1.processor_results.1.doc._ingest.timestamp + - is_true: docs.1.processor_results.1.doc._ingest.pipeline + +--- +"Test verbose simulate with error in pipeline": + - do: + ingest.put_pipeline: + id: "my_pipeline" + body: > + { + "description": "_description", + "processors": [ + { + "rename" : { + "field" : "does_not_exist", + "target_field" : "_value" + } + } + ] + } + - match: { acknowledged: true } + + - do: + ingest.simulate: + verbose: true + body: > + { + "pipeline": { + "description": "_description", + "processors": [ + { + "pipeline" : { + "name" : "my_pipeline" + } + } + ] + }, + "docs": [ + { + "_index": "index", + "_id": "id", + "_source": { + "foo": "bar", + "bar": "hello" + } + } + ] + } + - length: { docs: 1 } + - length: { docs.0.processor_results: 2 } + - match: { docs.0.processor_results.0.processor_type: "pipeline" } + - match: { docs.0.processor_results.0.status: "success" } + - match: { docs.0.processor_results.1.processor_type: "rename" } + - match: { docs.0.processor_results.1.status: "error" } + - match: { docs.0.processor_results.1.error.root_cause.0.type: "illegal_argument_exception" } + - match: { docs.0.processor_results.1.error.root_cause.0.reason: "field [does_not_exist] doesn't exist" } + - match: { docs.0.processor_results.1.error.type: "illegal_argument_exception" } + - match: { docs.0.processor_results.1.error.reason: "field [does_not_exist] doesn't exist" } + +--- +"Test verbose simulate with on_failure": + - do: + ingest.simulate: + verbose: true + body: > + { + "pipeline" : { + "description": "_description", + "processors": [ + { + "set" : { + "tag" : "setstatus-1", + "field" : "status", + "value" : 200 + } + }, + { + "rename" : { + "tag" : "rename-1", + "field" : "foofield", + "target_field" : "field1", + "on_failure" : [ + { + "set" : { + "tag" : "set on_failure rename", + "field" : "foofield", + "value" : "exists" + } + }, + { + "rename" : { + "field" : "foofield2", + "target_field" : "field1", + "on_failure" : [ + { + "set" : { + "field" : "foofield2", + "value" : "ran" + } + } + ] + } + } + ] + } + } + ] + }, + "docs": [ + { + "_index": "index", + "_id": "id", + "_source": { + "field1": "123.42 400 " + } + } + ] + } + - length: { docs: 1 } + - length: { docs.0.processor_results: 5 } + - match: { docs.0.processor_results.0.tag: "setstatus-1" } + - match: { docs.0.processor_results.0.doc._source.field1: "123.42 400 " } + - match: { docs.0.processor_results.0.doc._source.status: 200 } + - match: { docs.0.processor_results.1.tag: "rename-1" } + - match: { docs.0.processor_results.1.error.type: "illegal_argument_exception" } + - match: { docs.0.processor_results.1.error.reason: "field [foofield] doesn't exist" } + - match: { docs.0.processor_results.2.tag: "set on_failure rename" } + - is_false: docs.0.processor_results.3.tag + - is_false: docs.0.processor_results.4.tag + - match: { docs.0.processor_results.4.doc._source.foofield: "exists" } + - match: { docs.0.processor_results.4.doc._source.foofield2: "ran" } + - match: { docs.0.processor_results.4.doc._source.field1: "123.42 400 " } + - match: { docs.0.processor_results.4.doc._source.status: 200 } + +--- +"Test verbose simulate with ignore_failure and thrown exception": + - do: + ingest.simulate: + verbose: true + body: > + { + "pipeline" : { + "description": "_description", + "processors": [ + { + "set" : { + "tag" : "setstatus-1", + "field" : "status", + "value" : 200 + } + }, + { + "rename" : { + "tag" : "rename-1", + "field" : "foofield", + "target_field" : "field1", + "ignore_failure": true, + "on_failure" : [ + { + "set" : { + "tag" : "set on_failure rename", + "field" : "foofield", + "value" : "exists" + } + }, + { + "rename" : { + "field" : "foofield2", + "target_field" : "field1", + "on_failure" : [ + { + "set" : { + "field" : "foofield2", + "value" : "ran" + } + } + ] + } + } + ] + } + } + ] + }, + "docs": [ + { + "_index": "index", + "_id": "id", + "_source": { + "field1": "123.42 400 " + } + } + ] + } + - length: { docs: 1 } + - length: { docs.0.processor_results: 2 } + - match: { docs.0.processor_results.0.tag: "setstatus-1" } + - match: { docs.0.processor_results.0.doc._source.field1: "123.42 400 " } + - match: { docs.0.processor_results.0.doc._source.status: 200 } + - match: { docs.0.processor_results.0.status: "success" } + - match: { docs.0.processor_results.0.processor_type: "set" } + - match: { docs.0.processor_results.1.tag: "rename-1" } + - match: { docs.0.processor_results.1.ignored_error.error.type: "illegal_argument_exception" } + - match: { docs.0.processor_results.1.ignored_error.error.reason: "field [foofield] doesn't exist" } + - match: { docs.0.processor_results.1.doc._source.field1: "123.42 400 " } + - match: { docs.0.processor_results.1.doc._source.status: 200 } + - match: { docs.0.processor_results.1.status: "error_ignored" } + - match: { docs.0.processor_results.1.processor_type: "rename" } + +--- +"Test verbose simulate with ignore_failure and no exception thrown": + - do: + ingest.simulate: + verbose: true + body: > + { + "pipeline" : { + "description": "_description", + "processors": [ + { + "set" : { + "tag" : "setstatus-1", + "field" : "status", + "value" : 200 + } + }, + { + "rename" : { + "tag" : "rename-1", + "field" : "status", + "target_field" : "new_status", + "ignore_failure": true + } + } + ] + }, + "docs": [ + { + "_index": "index", + "_id": "id", + "_source": { + "field1": "123.42 400 " + } + } + ] + } + - length: { docs: 1 } + - length: { docs.0.processor_results: 2 } + - length: { docs.0.processor_results.0: 4 } + - match: { docs.0.processor_results.0.tag: "setstatus-1" } + - match: { docs.0.processor_results.0.status: "success" } + - match: { docs.0.processor_results.0.processor_type: "set" } + - match: { docs.0.processor_results.0.doc._source.field1: "123.42 400 " } + - match: { docs.0.processor_results.0.doc._source.status: 200 } + - length: { docs.0.processor_results.1: 4 } + - match: { docs.0.processor_results.1.tag: "rename-1" } + - match: { docs.0.processor_results.1.status: "success" } + - match: { docs.0.processor_results.1.processor_type: "rename" } + - match: { docs.0.processor_results.1.doc._source.new_status: 200 } + +--- +"Test verbose simulate with Pipeline Processor with Circular Pipelines": + - do: + ingest.put_pipeline: + id: "outer" + body: > + { + "description" : "outer pipeline", + "processors" : [ + { + "pipeline" : { + "name": "inner" + } + } + ] + } + - match: { acknowledged: true } + + - do: + ingest.put_pipeline: + id: "inner" + body: > + { + "description" : "inner pipeline", + "processors" : [ + { + "pipeline" : { + "name": "outer" + } + } + ] + } + - match: { acknowledged: true } + + - do: + ingest.simulate: + verbose: true + body: > + { + "pipeline": { + "processors" : [ + { + "pipeline" : { + "name": "outer" + } + } + ] + } + , + "docs": [ + { + "_index": "index", + "_id": "id", + "_source": { + "field1": "123.42 400 " + } + } + ] + } + - length: { docs: 1 } + - length: { docs.0.processor_results: 1 } + - match: { docs.0.processor_results.0.error.reason: "Cycle detected for pipeline: outer" } + +--- +"Test verbose simulate with Pipeline Processor with Multiple Pipelines": + - do: + ingest.put_pipeline: + id: "pipeline1" + body: > + { + "processors": [ + { + "set": { + "field": "pipeline1", + "value": true + } + }, + { + "pipeline": { + "name": "pipeline2" + } + } + ] + } + - match: { acknowledged: true } + + - do: + ingest.put_pipeline: + id: "pipeline2" + body: > + { + "processors": [ + { + "set": { + "field": "pipeline2", + "value": true + } + } + ] + } + - match: { acknowledged: true } + + - do: + ingest.simulate: + verbose: true + body: > + { + "pipeline": { + "processors": [ + { + "set": { + "field": "pipeline0", + "value": true, + "description" : "first_set" + } + }, + { + "pipeline": { + "name": "pipeline1" + } + } + ] + }, + "docs": [ + { + "_index": "index", + "_id": "id", + "_source": { + "field1": "123.42 400 " + } + } + ] + } + - length: { docs: 1 } + - length: { docs.0.processor_results: 5 } + - match: { docs.0.processor_results.0.doc._source.pipeline0: true } + - match: { docs.0.processor_results.0.status: "success" } + - match: { docs.0.processor_results.0.processor_type: "set" } + - match: { docs.0.processor_results.0.description: "first_set" } + - is_false: docs.0.processor_results.0.doc._source.pipeline1 + - is_false: docs.0.processor_results.0.doc._source.pipeline2 + - match: { docs.0.processor_results.1.doc: null } + - match: { docs.0.processor_results.1.status: "success" } + - match: { docs.0.processor_results.1.processor_type: "pipeline" } + - match: { docs.0.processor_results.2.doc._source.pipeline0: true } + - match: { docs.0.processor_results.2.doc._source.pipeline1: true } + - is_false: docs.0.processor_results.2.doc._source.pipeline2 + - match: { docs.0.processor_results.3.doc: null } + - match: { docs.0.processor_results.3.status: "success" } + - match: { docs.0.processor_results.3.processor_type: "pipeline" } + - match: { docs.0.processor_results.4.doc._source.pipeline0: true } + - match: { docs.0.processor_results.4.doc._source.pipeline1: true } + - match: { docs.0.processor_results.4.doc._source.pipeline2: true } + +--- +"Test verbose simulate with true conditional and on failure": + - do: + ingest.simulate: + verbose: true + body: > + { + "pipeline": { + "processors": [ + { + "rename": { + "tag": "gunna_fail", + "if": "true", + "field": "foo1", + "target_field": "fieldA", + "on_failure": [ + { + "set": { + "field": "failed1", + "value": "failed1", + "tag": "failed1" + } + }, + { + "rename": { + "tag": "gunna_fail_again", + "if": "true", + "field": "foo2", + "target_field": "fieldA", + "on_failure": [ + { + "set": { + "field": "failed2", + "value": "failed2", + "tag": "failed2" + } + } + ] + } + } + ] + } + } + ] + }, + "docs": [ + { + "_index": "index", + "_id": "id", + "_source": { + "foo": "bar" + } + } + ] + } + - length: { docs: 1 } + - length: { docs.0.processor_results: 4 } + - match: { docs.0.processor_results.0.tag: "gunna_fail" } + - match: { docs.0.processor_results.0.error.reason: "field [foo1] doesn't exist" } + - match: { docs.0.processor_results.0.status: "error" } + - match: { docs.0.processor_results.0.processor_type: "rename" } + - match: { docs.0.processor_results.1.tag: "failed1" } + - match: { docs.0.processor_results.1.doc._source.failed1: "failed1" } + - match: { docs.0.processor_results.1.doc._ingest.on_failure_processor_tag: "gunna_fail" } + - match: { docs.0.processor_results.1.status: "success" } + - match: { docs.0.processor_results.1.processor_type: "set" } + - match: { docs.0.processor_results.2.tag: "gunna_fail_again" } + - match: { docs.0.processor_results.2.error.reason: "field [foo2] doesn't exist" } + - match: { docs.0.processor_results.2.status: "error" } + - match: { docs.0.processor_results.2.processor_type: "rename" } + - match: { docs.0.processor_results.3.tag: "failed2" } + - match: { docs.0.processor_results.3.doc._source.failed1: "failed1" } + - match: { docs.0.processor_results.3.doc._source.failed2: "failed2" } + - match: { docs.0.processor_results.3.doc._ingest.on_failure_processor_tag: "gunna_fail_again" } + - match: { docs.0.processor_results.3.status: "success" } + - match: { docs.0.processor_results.3.processor_type: "set" } + + +--- +"Test simulate with pipeline with conditional and skipped and dropped": + - do: + ingest.simulate: + verbose: true + body: > + { + "pipeline": { + "description": "_description", + "processors": [ + { + "set" : { + "description": "processor_description", + "tag": "processor_tag", + "field" : "field2", + "value" : "_value" + } + }, + { + "drop" : { + "if": "false" + } + }, + { + "drop" : { + "if": "true" + } + } + ] + }, + "docs": [ + { + "_index": "index", + "_id": "id", + "_source": { + "foo": "bar" + } + } + ] + } + - length: { docs: 1 } + - length: { docs.0.processor_results: 3 } + - match: { docs.0.processor_results.0.doc._source.field2: "_value" } + - match: { docs.0.processor_results.0.description: "processor_description" } + - match: { docs.0.processor_results.0.tag: "processor_tag" } + - match: { docs.0.processor_results.0.status: "success" } + - match: { docs.0.processor_results.0.processor_type: "set" } + - match: { docs.0.processor_results.1.status: "skipped" } + - match: { docs.0.processor_results.1.processor_type: "drop" } + - match: { docs.0.processor_results.1.if.condition: "false" } + - match: { docs.0.processor_results.1.if.result: false } + - match: { docs.0.processor_results.2.status: "dropped" } + - match: { docs.0.processor_results.2.processor_type: "drop" } + - match: { docs.0.processor_results.2.if.condition: "true" } + - match: { docs.0.processor_results.2.if.result: true } +--- +"Test simulate with provided pipeline that does not exist": + - do: + ingest.simulate: + verbose: true + body: > + { + "pipeline": { + "description": "_description", + "processors": [ + { + "pipeline": { + "name": "____pipeline_doesnot_exist___" + } + } + ] + }, + "docs": [ + { + "_source": {} + } + ] + } + - match: { docs.0.processor_results.0.status: "error" } + - match: { docs.0.processor_results.0.error.root_cause.0.type: "illegal_argument_exception" } + - match: { docs.0.processor_results.0.error.root_cause.0.reason: "Pipeline processor configured for non-existent pipeline [____pipeline_doesnot_exist___]" } diff --git a/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineTransportAction.java b/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineTransportAction.java index 2a75ec3b7681..30d13ab93489 100644 --- a/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineTransportAction.java +++ b/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineTransportAction.java @@ -8,22 +8,47 @@ package org.elasticsearch.action.ingest; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionListenerResponseHandler; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.common.Randomness; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.core.TimeValue; import org.elasticsearch.ingest.IngestService; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportRequestOptions; import org.elasticsearch.transport.TransportService; +import java.util.Collection; import java.util.Map; +import java.util.Random; public class SimulatePipelineTransportAction extends HandledTransportAction { - + private static final Logger logger = LogManager.getLogger(SimulatePipelineTransportAction.class); + /** + * This is the amount of time given as the timeout for transport requests to the ingest node. + */ + public static final Setting INGEST_NODE_TRANSPORT_ACTION_TIMEOUT = Setting.timeSetting( + "ingest_node.transport_action_timeout", + TimeValue.timeValueSeconds(20), + TimeValue.timeValueMillis(1), + Setting.Property.NodeScope, + Setting.Property.Dynamic + ); private final IngestService ingestService; private final SimulateExecutionService executionService; + private final TransportService transportService; + private volatile TimeValue ingestNodeTransportActionTimeout; + // ThreadLocal because our unit testing framework does not like sharing Randoms across threads + private final ThreadLocal random = ThreadLocal.withInitial(Randomness::get); @Inject public SimulatePipelineTransportAction( @@ -35,30 +60,77 @@ public SimulatePipelineTransportAction( super(SimulatePipelineAction.NAME, transportService, actionFilters, SimulatePipelineRequest::new); this.ingestService = ingestService; this.executionService = new SimulateExecutionService(threadPool); + this.transportService = transportService; + this.ingestNodeTransportActionTimeout = INGEST_NODE_TRANSPORT_ACTION_TIMEOUT.get(ingestService.getClusterService().getSettings()); + ingestService.getClusterService() + .getClusterSettings() + .addSettingsUpdateConsumer( + INGEST_NODE_TRANSPORT_ACTION_TIMEOUT, + newTimeout -> this.ingestNodeTransportActionTimeout = newTimeout + ); } @Override protected void doExecute(Task task, SimulatePipelineRequest request, ActionListener listener) { final Map source = XContentHelper.convertToMap(request.getSource(), false, request.getXContentType()).v2(); - - final SimulatePipelineRequest.Parsed simulateRequest; + DiscoveryNodes discoveryNodes = ingestService.getClusterService().state().nodes(); + Map ingestNodes = discoveryNodes.getIngestNodes(); + if (ingestNodes.isEmpty()) { + /* + * Some resources used by pipelines, such as the geoip database, only exist on ingest nodes. Since we only run pipelines on + * nodes with the ingest role, we ought to only simulate a pipeline on nodes with the ingest role. + */ + listener.onFailure( + new IllegalStateException("There are no ingest nodes in this cluster, unable to forward request to an ingest node.") + ); + return; + } try { - if (request.getId() != null) { - simulateRequest = SimulatePipelineRequest.parseWithPipelineId( - request.getId(), - source, - request.isVerbose(), - ingestService, - request.getRestApiVersion() - ); + if (discoveryNodes.getLocalNode().isIngestNode()) { + final SimulatePipelineRequest.Parsed simulateRequest; + if (request.getId() != null) { + simulateRequest = SimulatePipelineRequest.parseWithPipelineId( + request.getId(), + source, + request.isVerbose(), + ingestService, + request.getRestApiVersion() + ); + } else { + simulateRequest = SimulatePipelineRequest.parse( + source, + request.isVerbose(), + ingestService, + request.getRestApiVersion() + ); + } + executionService.execute(simulateRequest, listener); } else { - simulateRequest = SimulatePipelineRequest.parse(source, request.isVerbose(), ingestService, request.getRestApiVersion()); + DiscoveryNode ingestNode = getRandomIngestNode(ingestNodes.values()); + logger.trace("forwarding request [{}] to ingest node [{}]", actionName, ingestNode); + ActionListenerResponseHandler handler = new ActionListenerResponseHandler<>( + listener, + SimulatePipelineAction.INSTANCE.getResponseReader() + ); + if (task == null) { + transportService.sendRequest(ingestNode, actionName, request, handler); + } else { + transportService.sendChildRequest( + ingestNode, + actionName, + request, + task, + TransportRequestOptions.timeout(ingestNodeTransportActionTimeout), + handler + ); + } } } catch (Exception e) { listener.onFailure(e); - return; } + } - executionService.execute(simulateRequest, listener); + private DiscoveryNode getRandomIngestNode(Collection ingestNodes) { + return ingestNodes.toArray(new DiscoveryNode[0])[random.get().nextInt(ingestNodes.size())]; } } diff --git a/server/src/main/java/org/elasticsearch/action/ingest/SimulateProcessorResult.java b/server/src/main/java/org/elasticsearch/action/ingest/SimulateProcessorResult.java index a40ff5fa955f..b83ed0ec6f82 100644 --- a/server/src/main/java/org/elasticsearch/action/ingest/SimulateProcessorResult.java +++ b/server/src/main/java/org/elasticsearch/action/ingest/SimulateProcessorResult.java @@ -8,6 +8,7 @@ package org.elasticsearch.action.ingest; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.Version; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; @@ -159,7 +160,11 @@ public SimulateProcessorResult(String type, String processorTag, String descript * Read from a stream. */ SimulateProcessorResult(StreamInput in) throws IOException { - this.processorTag = in.readString(); + if (in.getVersion().onOrAfter(Version.V_8_7_0)) { + this.processorTag = in.readOptionalString(); + } else { + this.processorTag = in.readString(); + } this.ingestDocument = in.readOptionalWriteable(WriteableIngestDocument::new); this.failure = in.readException(); this.description = in.readOptionalString(); @@ -174,7 +179,11 @@ public SimulateProcessorResult(String type, String processorTag, String descript @Override public void writeTo(StreamOutput out) throws IOException { - out.writeString(processorTag); + if (out.getVersion().onOrAfter(Version.V_8_7_0)) { + out.writeOptionalString(processorTag); + } else { + out.writeString(processorTag); + } out.writeOptionalWriteable(ingestDocument); out.writeException(failure); out.writeOptionalString(description); diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index b7e3d3360f7b..53eebac01337 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -11,6 +11,7 @@ import org.elasticsearch.action.admin.cluster.configuration.TransportAddVotingConfigExclusionsAction; import org.elasticsearch.action.admin.indices.close.TransportCloseIndexAction; import org.elasticsearch.action.bulk.WriteAckDelay; +import org.elasticsearch.action.ingest.SimulatePipelineTransportAction; import org.elasticsearch.action.search.TransportSearchAction; import org.elasticsearch.action.support.AutoCreateIndex; import org.elasticsearch.action.support.DestructiveOperations; @@ -537,6 +538,7 @@ public void apply(Settings value, Settings current, Settings previous) { HealthNodeTaskExecutor.ENABLED_SETTING, LocalHealthMonitor.POLL_INTERVAL_SETTING, TransportHealthNodeAction.HEALTH_NODE_TRANSPORT_ACTION_TIMEOUT, + SimulatePipelineTransportAction.INGEST_NODE_TRANSPORT_ACTION_TIMEOUT, WriteAckDelay.WRITE_ACK_DELAY_INTERVAL, WriteAckDelay.WRITE_ACK_DELAY_RANDOMNESS_BOUND, TcpTransport.isUntrustedRemoteClusterEnabled() ? RemoteClusterService.REMOTE_CLUSTER_AUTHORIZATION : null diff --git a/server/src/test/java/org/elasticsearch/action/ingest/SimulateProcessorResultTests.java b/server/src/test/java/org/elasticsearch/action/ingest/SimulateProcessorResultTests.java index 21b25d974571..d76df88e9818 100644 --- a/server/src/test/java/org/elasticsearch/action/ingest/SimulateProcessorResultTests.java +++ b/server/src/test/java/org/elasticsearch/action/ingest/SimulateProcessorResultTests.java @@ -61,8 +61,8 @@ public void testSerialization() throws IOException { static SimulateProcessorResult createTestInstance(boolean isSuccessful, boolean isIgnoredException, boolean hasCondition) { String type = randomAlphaOfLengthBetween(1, 10); - String processorTag = randomAlphaOfLengthBetween(1, 10); - String description = randomAlphaOfLengthBetween(1, 10); + String processorTag = randomBoolean() ? null : randomAlphaOfLengthBetween(1, 10); + String description = randomBoolean() ? null : randomAlphaOfLengthBetween(1, 10); Tuple conditionWithResult = hasCondition ? new Tuple<>(randomAlphaOfLengthBetween(1, 10), randomBoolean()) : null; SimulateProcessorResult simulateProcessorResult; if (isSuccessful) { From 93b04a5105d21de4b6aba7295b7a12210ded37a0 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Thu, 8 Dec 2022 17:50:19 +0100 Subject: [PATCH 206/919] Adjust InternalTimeSeries reduce logic (#92240) The `InternalTimeSeries#reduce(...)` method tries to be clever to not perform reduce when no duplicate tsid are encountered, since there is only bucket for a tsid. However some multi bucket aggregations always require that a reduce operation happens. (all aggregations that change `mustReduceOnSingleInternalAgg()` to return true) Also in tests we do assertions that implicitly require a reduce operation to happen, even if no reduce is really required. This change makes sure that reduce operation for the sub aggregations of time_series aggregation always happens. --- .../bucket/timeseries/InternalTimeSeries.java | 1 + .../timeseries/TimeSeriesAggregatorTests.java | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/timeseries/InternalTimeSeries.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/timeseries/InternalTimeSeries.java index 088568fefc03..1a40f4bea425 100644 --- a/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/timeseries/InternalTimeSeries.java +++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/timeseries/InternalTimeSeries.java @@ -240,6 +240,7 @@ protected boolean lessThan(IteratorAndCurrent a, IteratorAndCurr InternalBucket reducedBucket; if (bucketsWithSameKey.size() == 1) { reducedBucket = bucketsWithSameKey.get(0); + reducedBucket.aggregations = InternalAggregations.reduce(List.of(reducedBucket.aggregations), reduceContext); } else { reducedBucket = reduceBucket(bucketsWithSameKey, reduceContext); } diff --git a/modules/aggregations/src/test/java/org/elasticsearch/aggregations/bucket/timeseries/TimeSeriesAggregatorTests.java b/modules/aggregations/src/test/java/org/elasticsearch/aggregations/bucket/timeseries/TimeSeriesAggregatorTests.java index dfd3a7019b5f..2dc6676d9c4d 100644 --- a/modules/aggregations/src/test/java/org/elasticsearch/aggregations/bucket/timeseries/TimeSeriesAggregatorTests.java +++ b/modules/aggregations/src/test/java/org/elasticsearch/aggregations/bucket/timeseries/TimeSeriesAggregatorTests.java @@ -27,6 +27,7 @@ import org.elasticsearch.index.mapper.NumberFieldMapper; import org.elasticsearch.index.mapper.TimeSeriesIdFieldMapper; import org.elasticsearch.index.mapper.TimeSeriesIdFieldMapper.TimeSeriesIdBuilder; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; import org.elasticsearch.search.aggregations.bucket.histogram.InternalDateHistogram; @@ -39,6 +40,7 @@ import java.util.function.Consumer; import static org.elasticsearch.search.aggregations.AggregationBuilders.sum; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; @@ -135,6 +137,49 @@ public void testWithDateHistogramExecutedAsFilterByFilterWithTimeSeriesIndexSear ); } + public void testMultiBucketAggregationAsSubAggregation() throws IOException { + long startTime = DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.parseMillis("2023-01-01T00:00:00Z"); + CheckedConsumer buildIndex = iw -> { + writeTS(iw, startTime + 1, new Object[] { "dim1", "aaa", "dim2", "xxx" }, new Object[] {}); + writeTS(iw, startTime + 2, new Object[] { "dim1", "aaa", "dim2", "yyy" }, new Object[] {}); + writeTS(iw, startTime + 3, new Object[] { "dim1", "bbb", "dim2", "zzz" }, new Object[] {}); + writeTS(iw, startTime + 4, new Object[] { "dim1", "bbb", "dim2", "zzz" }, new Object[] {}); + writeTS(iw, startTime + 5, new Object[] { "dim1", "aaa", "dim2", "xxx" }, new Object[] {}); + writeTS(iw, startTime + 6, new Object[] { "dim1", "aaa", "dim2", "yyy" }, new Object[] {}); + writeTS(iw, startTime + 7, new Object[] { "dim1", "bbb", "dim2", "zzz" }, new Object[] {}); + writeTS(iw, startTime + 8, new Object[] { "dim1", "bbb", "dim2", "zzz" }, new Object[] {}); + }; + Consumer verifier = ts -> { + assertThat(ts.getBuckets(), hasSize(3)); + + assertThat(ts.getBucketByKey("{dim1=aaa, dim2=xxx}").docCount, equalTo(2L)); + InternalDateHistogram byTimeStampBucket = ts.getBucketByKey("{dim1=aaa, dim2=xxx}").getAggregations().get("by_timestamp"); + assertThat( + byTimeStampBucket.getBuckets(), + contains(new InternalDateHistogram.Bucket(startTime, 2, false, null, InternalAggregations.EMPTY)) + ); + assertThat(ts.getBucketByKey("{dim1=aaa, dim2=yyy}").docCount, equalTo(2L)); + byTimeStampBucket = ts.getBucketByKey("{dim1=aaa, dim2=yyy}").getAggregations().get("by_timestamp"); + assertThat( + byTimeStampBucket.getBuckets(), + contains(new InternalDateHistogram.Bucket(startTime, 2, false, null, InternalAggregations.EMPTY)) + ); + assertThat(ts.getBucketByKey("{dim1=bbb, dim2=zzz}").docCount, equalTo(4L)); + byTimeStampBucket = ts.getBucketByKey("{dim1=bbb, dim2=zzz}").getAggregations().get("by_timestamp"); + assertThat( + byTimeStampBucket.getBuckets(), + contains(new InternalDateHistogram.Bucket(startTime, 4, false, null, InternalAggregations.EMPTY)) + ); + }; + + DateHistogramAggregationBuilder dateBuilder = new DateHistogramAggregationBuilder("by_timestamp"); + dateBuilder.field("@timestamp"); + dateBuilder.fixedInterval(DateHistogramInterval.seconds(1)); + TimeSeriesAggregationBuilder tsBuilder = new TimeSeriesAggregationBuilder("by_tsid"); + tsBuilder.subAggregation(dateBuilder); + timeSeriesTestCase(tsBuilder, new MatchAllDocsQuery(), buildIndex, verifier); + } + private void timeSeriesTestCase( TimeSeriesAggregationBuilder builder, Query query, From 8b9f3b595f566274456e7028a5dc9a4e2f130628 Mon Sep 17 00:00:00 2001 From: Rene Groeschke Date: Thu, 8 Dec 2022 17:55:45 +0100 Subject: [PATCH 207/919] Revert "Update to Gradle 7.6 (#89796)" (#92241) We have seen reports of broken compilation after a clean build after updating to gradle 7.6. This reverts commit deaf92878aced3370af8cff08507c625dca6c0f5. --- .../gradle/wrapper/gradle-wrapper.properties | 5 ++-- ...elasticsearch.runtime-jdk-provision.gradle | 3 +-- .../elasticsearch/gradle/internal/Jdk.java | 7 +----- .../internal/info/GlobalBuildInfoPlugin.java | 23 +++++++++--------- .../internal/test/DistroTestPlugin.java | 5 +--- .../test/StandaloneRestTestPlugin.java | 2 +- .../executer/RerunTestResultProcessor.java | 5 ++-- .../src/main/resources/minimumGradleVersion | 2 +- .../RerunTestResultProcessorTestSpec.groovy | 7 +++--- .../test/JavaRestTestPluginFuncTest.groovy | 2 -- .../test/YamlRestTestPluginFuncTest.groovy | 2 -- .../gradle/util/GradleUtils.java | 2 +- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 61574 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 5 ++-- gradlew | 12 +++------ gradlew.bat | 1 - libs/x-content/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 5 ++-- plugins/examples/settings.gradle | 4 +-- 20 files changed, 37 insertions(+), 59 deletions(-) diff --git a/build-tools-internal/gradle/wrapper/gradle-wrapper.properties b/build-tools-internal/gradle/wrapper/gradle-wrapper.properties index d1731eb7dbfa..e939ec976751 100644 --- a/build-tools-internal/gradle/wrapper/gradle-wrapper.properties +++ b/build-tools-internal/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip -networkTimeout=10000 +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionSha256Sum=312eb12875e1747e05c2f81a4789902d7e4ec5defbd1eefeaccc08acf096505d +distributionSha256Sum=db9c8211ed63f61f60292c69e80d89196f9eb36665e369e7f00ac4cc841c2219 diff --git a/build-tools-internal/src/main/groovy/elasticsearch.runtime-jdk-provision.gradle b/build-tools-internal/src/main/groovy/elasticsearch.runtime-jdk-provision.gradle index e016374f3251..1d1112706fd0 100644 --- a/build-tools-internal/src/main/groovy/elasticsearch.runtime-jdk-provision.gradle +++ b/build-tools-internal/src/main/groovy/elasticsearch.runtime-jdk-provision.gradle @@ -28,8 +28,7 @@ configure(allprojects) { } project.tasks.withType(Test).configureEach { Test test -> if (BuildParams.getIsRuntimeJavaHomeSet()) { - test.executable = "${BuildParams.runtimeJavaHome}/bin/java" + - (OS.current() == OS.WINDOWS ? '.exe' : '') + test.executable = "${BuildParams.runtimeJavaHome}/bin/java" } else { test.dependsOn(project.jdks.provisioned_runtime) test.executable = rootProject.jdks.provisioned_runtime.getBinJavaPath() diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/Jdk.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/Jdk.java index deb556397d09..09f655a7aece 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/Jdk.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/Jdk.java @@ -152,16 +152,11 @@ public Object getBinJavaPath() { return new Object() { @Override public String toString() { - return getHomeRoot() + getPlatformBinPath(); + return getHomeRoot() + "/bin/java"; } }; } - private String getPlatformBinPath() { - boolean isWindows = "windows".equals(getPlatform()); - return "/bin/java" + (isWindows ? ".exe" : ""); - } - public Object getJavaHomePath() { return new Object() { @Override diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/GlobalBuildInfoPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/GlobalBuildInfoPlugin.java index b741be730407..f730c700f62b 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/GlobalBuildInfoPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/GlobalBuildInfoPlugin.java @@ -102,7 +102,7 @@ public void apply(Project project) { ) ); params.setIsRuntimeJavaHomeSet(isRuntimeJavaHomeSet); - JvmInstallationMetadata runtimeJdkMetaData = metadataDetector.getMetadata(getJavaInstallation(runtimeJavaHome)); + JvmInstallationMetadata runtimeJdkMetaData = metadataDetector.getMetadata(getJavaInstallation(runtimeJavaHome).getLocation()); params.setRuntimeJavaDetails(formatJavaVendorDetails(runtimeJdkMetaData)); params.setJavaVersions(getAvailableJavaVersions()); params.setMinimumCompilerVersion(minimumCompilerVersion); @@ -133,7 +133,7 @@ public void apply(Project project) { private Provider resolveToolchainSpecFromEnv() { return providers.environmentVariable("JAVA_TOOLCHAIN_HOME").map(toolChainEnvVariable -> { File toolChainDir = new File(toolChainEnvVariable); - JvmInstallationMetadata metadata = metadataDetector.getMetadata(getJavaInstallation(toolChainDir)); + JvmInstallationMetadata metadata = metadataDetector.getMetadata(toolChainDir); if (metadata.isValidInstallation() == false) { throw new GradleException( "Configured JAVA_TOOLCHAIN_HOME " + toolChainEnvVariable + " does not point to a valid jdk installation." @@ -166,17 +166,17 @@ private void logGlobalBuildInfo() { final String osVersion = System.getProperty("os.version"); final String osArch = System.getProperty("os.arch"); final Jvm gradleJvm = Jvm.current(); - JvmInstallationMetadata gradleJvmMetadata = metadataDetector.getMetadata(getJavaInstallation(gradleJvm.getJavaHome())); + JvmInstallationMetadata gradleJvmMetadata = metadataDetector.getMetadata(gradleJvm.getJavaHome()); final String gradleJvmVendorDetails = gradleJvmMetadata.getVendor().getDisplayName(); - final String gradleJvmImplementationVersion = gradleJvmMetadata.getJvmVersion(); + final String gradleJvmImplementationVersion = gradleJvmMetadata.getImplementationVersion(); LOGGER.quiet("======================================="); LOGGER.quiet("Elasticsearch Build Hamster says Hello!"); LOGGER.quiet(" Gradle Version : " + GradleVersion.current().getVersion()); LOGGER.quiet(" OS Info : " + osName + " " + osVersion + " (" + osArch + ")"); if (BuildParams.getIsRuntimeJavaHomeSet()) { - JvmInstallationMetadata runtimeJvm = metadataDetector.getMetadata(getJavaInstallation(BuildParams.getRuntimeJavaHome())); + JvmInstallationMetadata runtimeJvm = metadataDetector.getMetadata(BuildParams.getRuntimeJavaHome()); final String runtimeJvmVendorDetails = runtimeJvm.getVendor().getDisplayName(); - final String runtimeJvmImplementationVersion = runtimeJvm.getJvmVersion(); + final String runtimeJvmImplementationVersion = runtimeJvm.getImplementationVersion(); final String runtimeVersion = runtimeJvm.getRuntimeVersion(); final String runtimeExtraDetails = runtimeJvmVendorDetails + ", " + runtimeVersion; LOGGER.quiet(" Runtime JDK Version : " + runtimeJvmImplementationVersion + " (" + runtimeExtraDetails + ")"); @@ -198,7 +198,7 @@ private void logGlobalBuildInfo() { private JavaVersion determineJavaVersion(String description, File javaHome, JavaVersion requiredVersion) { InstallationLocation installation = getJavaInstallation(javaHome); - JavaVersion actualVersion = metadataDetector.getMetadata(installation).getLanguageVersion(); + JavaVersion actualVersion = metadataDetector.getMetadata(installation.getLocation()).getLanguageVersion(); if (actualVersion.isCompatibleWith(requiredVersion) == false) { throwInvalidJavaHomeException( description, @@ -231,9 +231,10 @@ private boolean isSameFile(File javaHome, InstallationLocation installationLocat */ private List getAvailableJavaVersions() { return getAvailableJavaInstallationLocationSteam().map(installationLocation -> { - JvmInstallationMetadata metadata = metadataDetector.getMetadata(installationLocation); + File installationDir = installationLocation.getLocation(); + JvmInstallationMetadata metadata = metadataDetector.getMetadata(installationDir); int actualVersion = Integer.parseInt(metadata.getLanguageVersion().getMajorVersion()); - return JavaHome.of(actualVersion, providers.provider(() -> installationLocation.getLocation())); + return JavaHome.of(actualVersion, providers.provider(() -> installationDir)); }).collect(Collectors.toList()); } @@ -356,8 +357,8 @@ private static class ErrorTraceMetadataDetector implements JvmMetadataDetector { } @Override - public JvmInstallationMetadata getMetadata(InstallationLocation installationLocation) { - JvmInstallationMetadata metadata = delegate.getMetadata(installationLocation); + public JvmInstallationMetadata getMetadata(File file) { + JvmInstallationMetadata metadata = delegate.getMetadata(file); if (metadata instanceof JvmInstallationMetadata.FailureInstallationMetadata) { throw new GradleException("Jvm Metadata cannot be resolved for " + metadata.getJavaHome().toString()); } diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/DistroTestPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/DistroTestPlugin.java index bd702097a700..dac21dc78aa3 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/DistroTestPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/DistroTestPlugin.java @@ -127,10 +127,7 @@ public void apply(Project project) { depsTask.configure(t -> t.dependsOn(examplePlugin.getDependencies())); depsTasks.put(taskname, depsTask); TaskProvider destructiveTask = configureTestTask(project, taskname, distribution, t -> { - t.onlyIf( - "Docker is not available", - t2 -> distribution.isDocker() == false || dockerSupport.get().getDockerAvailability().isAvailable() - ); + t.onlyIf(t2 -> distribution.isDocker() == false || dockerSupport.get().getDockerAvailability().isAvailable()); addDistributionSysprop(t, DISTRIBUTION_SYSPROP, distribution::getFilepath); addDistributionSysprop(t, EXAMPLE_PLUGIN_SYSPROP, () -> examplePlugin.getSingleFile().toString()); t.exclude("**/PackageUpgradeTests.class"); diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/StandaloneRestTestPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/StandaloneRestTestPlugin.java index 888fd6821393..9ffaf396e7fb 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/StandaloneRestTestPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/StandaloneRestTestPlugin.java @@ -71,7 +71,7 @@ public void apply(final Project project) { ); IdeaModel idea = project.getExtensions().getByType(IdeaModel.class); - idea.getModule().getTestSources().from(testSourceSet.getJava().getSrcDirs()); + idea.getModule().getTestSourceDirs().addAll(testSourceSet.getJava().getSrcDirs()); idea.getModule() .getScopes() .put( diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rerun/executer/RerunTestResultProcessor.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rerun/executer/RerunTestResultProcessor.java index 39b21df53133..06bd7679a654 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rerun/executer/RerunTestResultProcessor.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rerun/executer/RerunTestResultProcessor.java @@ -12,7 +12,6 @@ import org.gradle.api.internal.tasks.testing.TestDescriptorInternal; import org.gradle.api.internal.tasks.testing.TestResultProcessor; import org.gradle.api.internal.tasks.testing.TestStartEvent; -import org.gradle.api.tasks.testing.TestFailure; import org.gradle.api.tasks.testing.TestOutputEvent; import java.util.ArrayList; @@ -90,11 +89,11 @@ public void output(Object testId, TestOutputEvent testOutputEvent) { } @Override - public void failure(Object testId, TestFailure result) { + public void failure(Object testId, Throwable throwable) { if (activeDescriptorsById.containsKey(testId)) { activeDescriptorsById.remove(testId); try { - delegate.failure(testId, result); + delegate.failure(testId, throwable); } catch (IllegalArgumentException illegalArgumentException) { logTracing(testId, illegalArgumentException); } diff --git a/build-tools-internal/src/main/resources/minimumGradleVersion b/build-tools-internal/src/main/resources/minimumGradleVersion index f5cce03c304f..7501d508f743 100644 --- a/build-tools-internal/src/main/resources/minimumGradleVersion +++ b/build-tools-internal/src/main/resources/minimumGradleVersion @@ -1 +1 @@ -7.6 \ No newline at end of file +7.5.1 \ No newline at end of file diff --git a/build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/test/rerun/executer/RerunTestResultProcessorTestSpec.groovy b/build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/test/rerun/executer/RerunTestResultProcessorTestSpec.groovy index 507a99f080df..bfcfea3e2da1 100644 --- a/build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/test/rerun/executer/RerunTestResultProcessorTestSpec.groovy +++ b/build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/test/rerun/executer/RerunTestResultProcessorTestSpec.groovy @@ -12,7 +12,6 @@ import org.gradle.api.internal.tasks.testing.TestCompleteEvent import org.gradle.api.internal.tasks.testing.TestDescriptorInternal import org.gradle.api.internal.tasks.testing.TestResultProcessor import org.gradle.api.internal.tasks.testing.TestStartEvent -import org.gradle.api.tasks.testing.TestFailure import org.gradle.api.tasks.testing.TestOutputEvent import spock.lang.Specification @@ -76,20 +75,20 @@ class RerunTestResultProcessorTestSpec extends Specification { def testDescriptor2 = descriptor("testId2") def testStartEvent2 = startEvent("testId2") - def testFailure = Mock(TestFailure) + def testError2 = Mock(Throwable) when: processor.started(rootDescriptor, rootTestStartEvent) processor.started(testDescriptor1, testStartEvent1) processor.started(testDescriptor2, testStartEvent2) - processor.failure("testId2", testFailure) + processor.failure("testId2", testError2) processor.completed("rootId", rootCompleteEvent) then: 1 * delegate.started(rootDescriptor, rootTestStartEvent) 1 * delegate.started(testDescriptor1, testStartEvent1) 1 * delegate.started(testDescriptor2, testStartEvent2) - 1 * delegate.failure("testId2", testFailure) + 1 * delegate.failure("testId2", testError2) 0 * delegate.completed("rootId", rootCompleteEvent) when: diff --git a/build-tools/src/integTest/groovy/org/elasticsearch/gradle/test/JavaRestTestPluginFuncTest.groovy b/build-tools/src/integTest/groovy/org/elasticsearch/gradle/test/JavaRestTestPluginFuncTest.groovy index 1d3ac0030a0b..2d6d122663da 100644 --- a/build-tools/src/integTest/groovy/org/elasticsearch/gradle/test/JavaRestTestPluginFuncTest.groovy +++ b/build-tools/src/integTest/groovy/org/elasticsearch/gradle/test/JavaRestTestPluginFuncTest.groovy @@ -11,7 +11,6 @@ package org.elasticsearch.gradle.test import org.elasticsearch.gradle.VersionProperties import org.elasticsearch.gradle.fixtures.AbstractGradleFuncTest import org.gradle.testkit.runner.TaskOutcome -import spock.lang.Ignore class JavaRestTestPluginFuncTest extends AbstractGradleFuncTest { @@ -20,7 +19,6 @@ class JavaRestTestPluginFuncTest extends AbstractGradleFuncTest { configurationCacheCompatible = false } - @Ignore('https://github.com/gradle/gradle/issues/21868') def "declares default dependencies"() { given: buildFile << """ diff --git a/build-tools/src/integTest/groovy/org/elasticsearch/gradle/test/YamlRestTestPluginFuncTest.groovy b/build-tools/src/integTest/groovy/org/elasticsearch/gradle/test/YamlRestTestPluginFuncTest.groovy index 1598d6ab06ff..83c215ceea42 100644 --- a/build-tools/src/integTest/groovy/org/elasticsearch/gradle/test/YamlRestTestPluginFuncTest.groovy +++ b/build-tools/src/integTest/groovy/org/elasticsearch/gradle/test/YamlRestTestPluginFuncTest.groovy @@ -11,7 +11,6 @@ package org.elasticsearch.gradle.test import org.elasticsearch.gradle.VersionProperties import org.elasticsearch.gradle.fixtures.AbstractGradleFuncTest import org.gradle.testkit.runner.TaskOutcome -import spock.lang.Ignore class YamlRestTestPluginFuncTest extends AbstractGradleFuncTest { @@ -20,7 +19,6 @@ class YamlRestTestPluginFuncTest extends AbstractGradleFuncTest { configurationCacheCompatible = false } - @Ignore('https://github.com/gradle/gradle/issues/21868') def "declares default dependencies"() { given: buildFile << """ diff --git a/build-tools/src/main/java/org/elasticsearch/gradle/util/GradleUtils.java b/build-tools/src/main/java/org/elasticsearch/gradle/util/GradleUtils.java index ce69c4ec476f..134b24fec16e 100644 --- a/build-tools/src/main/java/org/elasticsearch/gradle/util/GradleUtils.java +++ b/build-tools/src/main/java/org/elasticsearch/gradle/util/GradleUtils.java @@ -127,7 +127,7 @@ public static void setupIdeForTestSourceSet(Project project, SourceSet testSourc Configuration runtimeClasspathConfiguration = project.getConfigurations().getByName(runtimeClasspathName); project.getPluginManager().withPlugin("idea", p -> { IdeaModel idea = project.getExtensions().getByType(IdeaModel.class); - idea.getModule().getTestSources().from(testSourceSet.getJava().getSrcDirs()); + idea.getModule().setTestSourceDirs(testSourceSet.getJava().getSrcDirs()); idea.getModule().getScopes().put(testSourceSet.getName(), Map.of("plus", List.of(runtimeClasspathConfiguration))); }); project.getPluginManager().withPlugin("eclipse", p -> { diff --git a/build.gradle b/build.gradle index e6ad0f439ce6..e1e11e60e110 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ import org.elasticsearch.gradle.internal.info.BuildParams import org.elasticsearch.gradle.util.GradleUtils import org.gradle.plugins.ide.eclipse.model.AccessRule import org.gradle.plugins.ide.eclipse.model.ProjectDependency -import org.gradle.util.internal.DistributionLocator +import org.gradle.util.DistributionLocator import org.gradle.util.GradleVersion import java.nio.file.Files diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 943f0cbfa754578e88a3dae77fce6e3dea56edbf..249e5832f090a2944b7473328c07c9755baa3196 100644 GIT binary patch delta 35766 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%0DGK?7GHTj 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 VwlMN}`%er%aGR6olj~j${vQ;P=LY}) delta 36524 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 Date: Thu, 8 Dec 2022 20:08:00 +0200 Subject: [PATCH 208/919] =?UTF-8?q?[ML]=20Better=20error=20when=20aggregat?= =?UTF-8?q?e=5Fmetric=5Fdouble=20used=20in=20scrolling=20data=E2=80=A6=20(?= =?UTF-8?q?#92232)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fields of type `aggregate_metric_double` cannot be used with anomaly detection datafeeds unless they are used in aggregations. This commit improves the error message in this scenario so that the user gets better informed of why they cannot use such fields. Closes #90592 Co-authored-by: David Roberts --- docs/changelog/92232.yaml | 6 +++ .../xpack/ml/integration/DatafeedJobsIT.java | 53 +++++++++++++++++++ .../scroll/ScrollDataExtractorFactory.java | 30 +++++++++++ 3 files changed, 89 insertions(+) create mode 100644 docs/changelog/92232.yaml diff --git a/docs/changelog/92232.yaml b/docs/changelog/92232.yaml new file mode 100644 index 000000000000..0ace8ab75f8f --- /dev/null +++ b/docs/changelog/92232.yaml @@ -0,0 +1,6 @@ +pr: 92232 +summary: Better error when `aggregate_metric_double` used in scrolling datafeeds +area: Machine Learning +type: enhancement +issues: + - 90592 diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsIT.java index c3cae87228d0..01a8f409ed6b 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsIT.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.ml.integration; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.Version; import org.elasticsearch.action.admin.cluster.node.hotthreads.NodeHotThreads; @@ -64,6 +65,7 @@ import static org.elasticsearch.xpack.ml.support.BaseMlIntegTestCase.createScheduledJob; import static org.elasticsearch.xpack.ml.support.BaseMlIntegTestCase.getDataCounts; import static org.elasticsearch.xpack.ml.support.BaseMlIntegTestCase.indexDocs; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.hasSize; @@ -828,4 +830,55 @@ public void testStartDatafeed_GivenTimeout_Returns408() throws Exception { assertThat(e.status(), equalTo(RestStatus.REQUEST_TIMEOUT)); } + + public void testStart_GivenAggregateMetricDoubleWithoutAggs() throws Exception { + final String index = "index-with-aggregate-metric-double"; + String mapping = """ + { + "properties": { + "time": { + "type": "date" + }, + "presum": { + "type": "aggregate_metric_double", + "metrics": [ "min", "max", "sum", "value_count" ], + "default_metric": "max" + } + } + }"""; + client().admin().indices().prepareCreate(index).setMapping(mapping).get(); + + DataDescription.Builder dataDescription = new DataDescription.Builder(); + + Detector.Builder d = new Detector.Builder("avg", "presum"); + AnalysisConfig.Builder analysisConfig = new AnalysisConfig.Builder(Collections.singletonList(d.build())); + analysisConfig.setBucketSpan(TimeValue.timeValueHours(1)); + + Job.Builder jobBuilder = new Job.Builder(); + jobBuilder.setId("job-with-aggregate-metric-double"); + jobBuilder.setAnalysisConfig(analysisConfig); + jobBuilder.setDataDescription(dataDescription); + + putJob(jobBuilder); + openJob(jobBuilder.getId()); + assertBusy(() -> assertEquals(getJobStats(jobBuilder.getId()).get(0).getState(), JobState.OPENED)); + + DatafeedConfig.Builder dfBuilder = new DatafeedConfig.Builder(jobBuilder.getId() + "-datafeed", jobBuilder.getId()); + dfBuilder.setIndices(Collections.singletonList(index)); + + DatafeedConfig datafeedConfig = dfBuilder.build(); + + putDatafeed(datafeedConfig); + + ElasticsearchStatusException e = expectThrows( + ElasticsearchStatusException.class, + () -> startDatafeed(datafeedConfig.getId(), 0L, null) + ); + + assertThat(e.status(), equalTo(RestStatus.BAD_REQUEST)); + assertThat( + e.getMessage(), + containsString("field [presum] is of type [aggregate_metric_double] and cannot be used in a datafeed without aggregations") + ); + } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorFactory.java index d35db03df6a0..7a485d08d064 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorFactory.java @@ -8,6 +8,7 @@ import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.fieldcaps.FieldCapabilities; import org.elasticsearch.action.fieldcaps.FieldCapabilitiesAction; import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest; import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse; @@ -25,10 +26,16 @@ import org.elasticsearch.xpack.ml.datafeed.DatafeedTimingStatsReporter; import org.elasticsearch.xpack.ml.datafeed.extractor.DataExtractorFactory; +import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; public class ScrollDataExtractorFactory implements DataExtractorFactory { + + // This field type is not supported for scrolling datafeeds. + private static final String AGGREGATE_METRIC_DOUBLE = "aggregate_metric_double"; + private final Client client; private final DatafeedConfig datafeedConfig; private final Job job; @@ -104,6 +111,17 @@ public static void create( ); return; } + Optional optionalAggregatedMetricDouble = findFirstAggregatedMetricDoubleField(fieldCapabilitiesResponse); + if (optionalAggregatedMetricDouble.isPresent()) { + listener.onFailure( + ExceptionsHelper.badRequestException( + "field [{}] is of type [{}] and cannot be used in a datafeed without aggregations", + optionalAggregatedMetricDouble.get(), + AGGREGATE_METRIC_DOUBLE + ) + ); + return; + } TimeBasedExtractedFields fields = TimeBasedExtractedFields.build(job, datafeed, fieldCapabilitiesResponse); listener.onResponse(new ScrollDataExtractorFactory(client, datafeed, job, fields, xContentRegistry, timingStatsReporter)); }, e -> { @@ -142,4 +160,16 @@ public static void create( return null; }); } + + private static Optional findFirstAggregatedMetricDoubleField(FieldCapabilitiesResponse fieldCapabilitiesResponse) { + Map> indexTofieldCapsMap = fieldCapabilitiesResponse.get(); + for (Map.Entry> indexToFieldCaps : indexTofieldCapsMap.entrySet()) { + for (Map.Entry typeToFieldCaps : indexToFieldCaps.getValue().entrySet()) { + if (AGGREGATE_METRIC_DOUBLE.equals(typeToFieldCaps.getKey())) { + return Optional.of(typeToFieldCaps.getValue().getName()); + } + } + } + return Optional.empty(); + } } From efeb0b213bd2e09643dcdbb379ec291b62272be7 Mon Sep 17 00:00:00 2001 From: Pooya Salehi Date: Thu, 8 Dec 2022 21:34:11 +0100 Subject: [PATCH 209/919] Bump versions after 8.5.3 release --- .ci/bwcVersions | 1 + .ci/snapshotBwcVersions | 2 +- server/src/main/java/org/elasticsearch/Version.java | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.ci/bwcVersions b/.ci/bwcVersions index c0b3dbb12249..3ff318966aba 100644 --- a/.ci/bwcVersions +++ b/.ci/bwcVersions @@ -79,5 +79,6 @@ BWC_VERSION: - "8.5.1" - "8.5.2" - "8.5.3" + - "8.5.4" - "8.6.0" - "8.7.0" diff --git a/.ci/snapshotBwcVersions b/.ci/snapshotBwcVersions index 2c26534044ec..549f4b3959a8 100644 --- a/.ci/snapshotBwcVersions +++ b/.ci/snapshotBwcVersions @@ -1,5 +1,5 @@ BWC_VERSION: - "7.17.8" - - "8.5.3" + - "8.5.4" - "8.6.0" - "8.7.0" diff --git a/server/src/main/java/org/elasticsearch/Version.java b/server/src/main/java/org/elasticsearch/Version.java index 88e1af2717a1..5c2bd4d325bc 100644 --- a/server/src/main/java/org/elasticsearch/Version.java +++ b/server/src/main/java/org/elasticsearch/Version.java @@ -127,6 +127,7 @@ public class Version implements Comparable, ToXContentFragment { public static final Version V_8_5_1 = new Version(8_05_01_99, org.apache.lucene.util.Version.LUCENE_9_4_1); public static final Version V_8_5_2 = new Version(8_05_02_99, org.apache.lucene.util.Version.LUCENE_9_4_1); public static final Version V_8_5_3 = new Version(8_05_03_99, org.apache.lucene.util.Version.LUCENE_9_4_2); + public static final Version V_8_5_4 = new Version(8_05_04_99, org.apache.lucene.util.Version.LUCENE_9_4_2); public static final Version V_8_6_0 = new Version(8_06_00_99, org.apache.lucene.util.Version.LUCENE_9_4_2); public static final Version V_8_7_0 = new Version(8_07_00_99, org.apache.lucene.util.Version.LUCENE_9_4_2); public static final Version CURRENT = V_8_7_0; From a10819c47e923ba088824c119056bd9b7abb0ec6 Mon Sep 17 00:00:00 2001 From: Pooya Salehi Date: Thu, 8 Dec 2022 21:36:16 +0100 Subject: [PATCH 210/919] Prune changelogs after 8.5.3 release --- docs/changelog/90931.yaml | 5 ----- docs/changelog/91334.yaml | 6 ------ docs/changelog/91338.yaml | 5 ----- docs/changelog/91409.yaml | 5 ----- docs/changelog/91490.yaml | 5 ----- docs/changelog/91506.yaml | 5 ----- docs/changelog/91510.yaml | 5 ----- docs/changelog/91567.yaml | 6 ------ docs/changelog/91622.yaml | 6 ------ docs/changelog/91704.yaml | 7 ------- docs/changelog/91772.yaml | 5 ----- docs/changelog/91823.yaml | 5 ----- docs/changelog/91917.yaml | 6 ------ docs/changelog/91946.yaml | 5 ----- docs/changelog/91981.yaml | 5 ----- 15 files changed, 81 deletions(-) delete mode 100644 docs/changelog/90931.yaml delete mode 100644 docs/changelog/91334.yaml delete mode 100644 docs/changelog/91338.yaml delete mode 100644 docs/changelog/91409.yaml delete mode 100644 docs/changelog/91490.yaml delete mode 100644 docs/changelog/91506.yaml delete mode 100644 docs/changelog/91510.yaml delete mode 100644 docs/changelog/91567.yaml delete mode 100644 docs/changelog/91622.yaml delete mode 100644 docs/changelog/91704.yaml delete mode 100644 docs/changelog/91772.yaml delete mode 100644 docs/changelog/91823.yaml delete mode 100644 docs/changelog/91917.yaml delete mode 100644 docs/changelog/91946.yaml delete mode 100644 docs/changelog/91981.yaml diff --git a/docs/changelog/90931.yaml b/docs/changelog/90931.yaml deleted file mode 100644 index 045ca8a1659b..000000000000 --- a/docs/changelog/90931.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 90931 -summary: Refactor enrich maintenance coordination logic -area: Ingest Node -type: enhancement -issues: [] diff --git a/docs/changelog/91334.yaml b/docs/changelog/91334.yaml deleted file mode 100644 index 5669524db61a..000000000000 --- a/docs/changelog/91334.yaml +++ /dev/null @@ -1,6 +0,0 @@ -pr: 91334 -summary: "Fix NPE in IndexService getNodeMappingStats" -area: "Stats" -type: bug -issues: - - 91259 diff --git a/docs/changelog/91338.yaml b/docs/changelog/91338.yaml deleted file mode 100644 index 50c08b3ce5f8..000000000000 --- a/docs/changelog/91338.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 91338 -summary: Extend systemd startup timeout to 900s -area: Infra/Core -type: enhancement -issues: [] diff --git a/docs/changelog/91409.yaml b/docs/changelog/91409.yaml deleted file mode 100644 index 0fc8febae6d7..000000000000 --- a/docs/changelog/91409.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 91409 -summary: Remove version limitations for CCS -area: EQL -type: enhancement -issues: [] diff --git a/docs/changelog/91490.yaml b/docs/changelog/91490.yaml deleted file mode 100644 index a25d1312deed..000000000000 --- a/docs/changelog/91490.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 91490 -summary: Avoid potential unsupported operation exception in doc bitset cache -area: Authorization -type: bug -issues: [] diff --git a/docs/changelog/91506.yaml b/docs/changelog/91506.yaml deleted file mode 100644 index 09263d2b7ec1..000000000000 --- a/docs/changelog/91506.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 91506 -summary: SLM uneahlthy policies diagnosis recommends correct URL in action -area: Health -type: bug -issues: [] diff --git a/docs/changelog/91510.yaml b/docs/changelog/91510.yaml deleted file mode 100644 index 06bbf0bc30eb..000000000000 --- a/docs/changelog/91510.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 91510 -summary: Refine bwc version checks on `EqlSearchRequest` -area: EQL -type: bug -issues: [] diff --git a/docs/changelog/91567.yaml b/docs/changelog/91567.yaml deleted file mode 100644 index c3eeb4dc0302..000000000000 --- a/docs/changelog/91567.yaml +++ /dev/null @@ -1,6 +0,0 @@ -pr: 91567 -summary: Refactor `DatabaseNodeService` as a cluster state listener -area: Ingest Node -type: bug -issues: - - 86999 diff --git a/docs/changelog/91622.yaml b/docs/changelog/91622.yaml deleted file mode 100644 index f105f27d95c6..000000000000 --- a/docs/changelog/91622.yaml +++ /dev/null @@ -1,6 +0,0 @@ -pr: 91622 -summary: Fix failure when resolving indices from CCS -area: Transform -type: bug -issues: - - 91550 diff --git a/docs/changelog/91704.yaml b/docs/changelog/91704.yaml deleted file mode 100644 index 66d368236d99..000000000000 --- a/docs/changelog/91704.yaml +++ /dev/null @@ -1,7 +0,0 @@ -pr: 91704 -summary: '`DoPrivileged` in `ElasticsearchEncaughtExceptionHandler` and check modify - thread' -area: Infra/Core -type: bug -issues: - - 91650 diff --git a/docs/changelog/91772.yaml b/docs/changelog/91772.yaml deleted file mode 100644 index 0fbe8e9dc32f..000000000000 --- a/docs/changelog/91772.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 91772 -summary: Add `trace.id` to request trace logs -area: Infra/Core -type: bug -issues: [88174] diff --git a/docs/changelog/91823.yaml b/docs/changelog/91823.yaml deleted file mode 100644 index 918b73a0653a..000000000000 --- a/docs/changelog/91823.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 91823 -summary: Upgrade Lucene to version 9.4.2 -area: Engine -type: upgrade -issues: [] diff --git a/docs/changelog/91917.yaml b/docs/changelog/91917.yaml deleted file mode 100644 index 92304d353c94..000000000000 --- a/docs/changelog/91917.yaml +++ /dev/null @@ -1,6 +0,0 @@ -pr: 91917 -summary: ML stats failures should not stop the usage API working -area: Machine Learning -type: bug -issues: - - 91893 diff --git a/docs/changelog/91946.yaml b/docs/changelog/91946.yaml deleted file mode 100644 index 83fb3b63382f..000000000000 --- a/docs/changelog/91946.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 91946 -summary: Support SAN/dnsName for restricted trust -area: TLS -type: enhancement -issues: [] diff --git a/docs/changelog/91981.yaml b/docs/changelog/91981.yaml deleted file mode 100644 index c34fd6959c8d..000000000000 --- a/docs/changelog/91981.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 91981 -summary: Handle any exception thrown while generating source for an `IngestDocument` -area: Ingest Node -type: bug -issues: [] From 74a5a9f271d24b39c83a485d61700c8848d81ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Fri, 9 Dec 2022 02:00:17 +0100 Subject: [PATCH 211/919] Update `kibana_system` privileges with access to `.apm-source-map` (#92160) In https://github.com/elastic/kibana/issues/146367 Kibana will create a system index `.apm-source-map` at startup. All access to the index (creation, indexing, reading) will happen through the system user (`kibana_system`). `kibana_system` already has access to `.apm-agent-configuration` and `.apm-custom-link`. Following this pattern the role should also be granted access to `.apm-source-map`. --- .../core/security/authz/store/ReservedRolesStore.java | 11 +++++++++++ .../security/authz/store/ReservedRolesStoreTests.java | 1 + 2 files changed, 12 insertions(+) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java index e77d29e4e10b..d8889003f366 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java @@ -694,24 +694,35 @@ public static RoleDescriptor kibanaSystemRoleDescriptor(String name) { .indices(".ml-annotations*", ".ml-notifications*") .privileges("read", "write") .build(), + // APM agent configuration - system index defined in KibanaPlugin RoleDescriptor.IndicesPrivileges.builder() .indices(".apm-agent-configuration") .privileges("all") .allowRestrictedIndices(true) .build(), + // APM custom link index creation - system index defined in KibanaPlugin RoleDescriptor.IndicesPrivileges.builder() .indices(".apm-custom-link") .privileges("all") .allowRestrictedIndices(true) .build(), + + // APM source map index creation - system index defined in KibanaPlugin + RoleDescriptor.IndicesPrivileges.builder() + .indices(".apm-source-map") + .privileges("all") + .allowRestrictedIndices(true) + .build(), + // APM telemetry queries APM indices in kibana task runner RoleDescriptor.IndicesPrivileges.builder().indices("apm-*").privileges("read", "read_cross_cluster").build(), RoleDescriptor.IndicesPrivileges.builder().indices("logs-apm.*").privileges("read", "read_cross_cluster").build(), RoleDescriptor.IndicesPrivileges.builder().indices("metrics-apm.*").privileges("read", "read_cross_cluster").build(), RoleDescriptor.IndicesPrivileges.builder().indices("traces-apm.*").privileges("read", "read_cross_cluster").build(), RoleDescriptor.IndicesPrivileges.builder().indices("traces-apm-*").privileges("read", "read_cross_cluster").build(), + // Data telemetry reads mappings, metadata and stats of indices RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges("view_index_metadata", "monitor").build(), // Endpoint diagnostic information. Kibana reads from these indices to send telemetry diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java index de368f5b7216..71554880cd8c 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java @@ -565,6 +565,7 @@ public void testKibanaSystemRole() { ".reporting-" + randomAlphaOfLength(randomIntBetween(0, 13)), ".apm-agent-configuration", ".apm-custom-link", + ".apm-source-map", ReservedRolesStore.ALERTS_LEGACY_INDEX + randomAlphaOfLength(randomIntBetween(0, 13)), ReservedRolesStore.ALERTS_BACKING_INDEX + randomAlphaOfLength(randomIntBetween(0, 13)), ReservedRolesStore.ALERTS_INDEX_ALIAS + randomAlphaOfLength(randomIntBetween(0, 13)), From 7d5065ee5b384e9d5938fb560a9a48ce6ba3a716 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 9 Dec 2022 11:02:31 +0000 Subject: [PATCH 212/919] AwaitsFix for #92181 --- .../java/org/elasticsearch/rest/action/cat/RestTableTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java b/server/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java index 780b7776ff9e..e25f4f8d5863 100644 --- a/server/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java +++ b/server/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java @@ -259,6 +259,7 @@ public void testMultiSort() { assertEquals(Arrays.asList(1, 0, 2), rowOrder); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/92181") public void testPlainTextChunking() throws Exception { final var expectedBody = new StringBuilder(); final var rowCount = between(10000, 20000); From a1c852a723adeae2672d9ad46e9616b35557442f Mon Sep 17 00:00:00 2001 From: Pooya Salehi Date: Fri, 9 Dec 2022 14:45:22 +0100 Subject: [PATCH 213/919] [DOCS] Forward-port doc changes after the 8.5.3 release (#92257) --- docs/reference/release-notes.asciidoc | 3 ++ docs/reference/release-notes/8.5.3.asciidoc | 45 +++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 docs/reference/release-notes/8.5.3.asciidoc diff --git a/docs/reference/release-notes.asciidoc b/docs/reference/release-notes.asciidoc index 9f5a32625a3f..2ce2fb8fe281 100644 --- a/docs/reference/release-notes.asciidoc +++ b/docs/reference/release-notes.asciidoc @@ -6,7 +6,9 @@ This section summarizes the changes in each release. + * <> +* <> * <> * <> * <> @@ -37,6 +39,7 @@ This section summarizes the changes in each release. -- include::release-notes/8.7.0.asciidoc[] +include::release-notes/8.5.3.asciidoc[] include::release-notes/8.5.2.asciidoc[] include::release-notes/8.5.1.asciidoc[] include::release-notes/8.5.0.asciidoc[] diff --git a/docs/reference/release-notes/8.5.3.asciidoc b/docs/reference/release-notes/8.5.3.asciidoc new file mode 100644 index 000000000000..ebc429faeda4 --- /dev/null +++ b/docs/reference/release-notes/8.5.3.asciidoc @@ -0,0 +1,45 @@ +[[release-notes-8.5.3]] +== {es} version 8.5.3 + +coming[8.5.3] + +Also see <>. + +[[bug-8.5.3]] +[float] +=== Bug fixes + +Infra/Core:: +* Add `trace.id` to request trace logs {es-pull}91772[#91772] (issue: {es-issue}88174[#88174]) +* `DoPrivileged` in `ElasticsearchEncaughtExceptionHandler` and check modify thread {es-pull}91704[#91704] (issue: {es-issue}91650[#91650]) + +Ingest Node:: +* Handle any exception thrown while generating source for an `IngestDocument` {es-pull}91981[#91981] + +Machine Learning:: +* ML stats failures should not stop the usage API working {es-pull}91917[#91917] (issue: {es-issue}91893[#91893]) + +Stats:: +* Fix NPE in IndexService getNodeMappingStats {es-pull}91334[#91334] (issue: {es-issue}91259[#91259]) + +Transform:: +* Fix failure when resolving indices from CCS {es-pull}91622[#91622] (issue: {es-issue}91550[#91550]) + +[[enhancement-8.5.3]] +[float] +=== Enhancements + +Ingest Node:: +* Refactor enrich maintenance coordination logic {es-pull}90931[#90931] + +TLS:: +* Support SAN/dnsName for restricted trust {es-pull}91946[#91946] + +[[upgrade-8.5.3]] +[float] +=== Upgrades + +Engine:: +* Upgrade Lucene to version 9.4.2 {es-pull}91823[#91823] + + From 62fb3337a604c80803b4b7f8d7eaa3cccc9e8346 Mon Sep 17 00:00:00 2001 From: David Turner Date: Sun, 11 Dec 2022 20:47:23 +0000 Subject: [PATCH 214/919] Fix testPlainTextChunking (#92256) This test had a (rare) off-by-one error in the computation of the number of expected chunks, but also the calculations were kind of opaque and contained too many magic numbers. This commit fixes the error and reworks the calculations to be clearer. Closes #92181 --- .../rest/action/cat/RestTableTests.java | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java b/server/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java index e25f4f8d5863..ff3b72463d86 100644 --- a/server/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java +++ b/server/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java @@ -11,6 +11,7 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.Table; import org.elasticsearch.common.recycler.Recycler; +import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.rest.AbstractRestChannel; import org.elasticsearch.rest.RestResponse; @@ -29,7 +30,6 @@ import static org.elasticsearch.rest.action.cat.RestTable.buildDisplayHeaders; import static org.elasticsearch.rest.action.cat.RestTable.buildResponse; -import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalToIgnoringCase; @@ -259,22 +259,28 @@ public void testMultiSort() { assertEquals(Arrays.asList(1, 0, 2), rowOrder); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/92181") public void testPlainTextChunking() throws Exception { + final var cells = randomArray(8, 8, String[]::new, () -> randomAlphaOfLengthBetween(1, 5)); + final var expectedRow = String.join(" ", cells) + "\n"; + + // OutputStreamWriter has an 8kiB buffer so all chunks are at least that big + final var bufferSize = ByteSizeUnit.KB.toIntBytes(8); + final var rowLength = expectedRow.length(); + final var expectedRowsPerChunk = 1 + bufferSize / rowLength; // end chunk after first row which overflows the buffer + final var expectedChunkSize = expectedRowsPerChunk * rowLength; + + final var rowCount = between(expectedRowsPerChunk + 1, expectedRowsPerChunk * 10); + final var expectedChunkCount = 1 + (rowCount * rowLength - 1) / expectedChunkSize; // ceil(rowCount * rowLength / expectedChunkSize) + assertThat(expectedChunkCount, greaterThan(1)); + final var expectedBody = new StringBuilder(); - final var rowCount = between(10000, 20000); for (int i = 0; i < rowCount; i++) { table.startRow(); - table.addCell("foo"); - table.addCell("foo"); - table.addCell("foo"); - table.addCell("foo"); - table.addCell("foo"); - table.addCell("foo"); - table.addCell("foo"); - table.addCell("foo"); + for (final var cell : cells) { + table.addCell(cell); + } table.endRow(); - expectedBody.append(TEXT_TABLE_BODY); + expectedBody.append(expectedRow); } final var request = new FakeRestRequest.Builder(xContentRegistry()).withHeaders( @@ -286,13 +292,10 @@ public void testPlainTextChunking() throws Exception { public void sendResponse(RestResponse response) {} }); - // OutputStreamWriter has an 8kiB buffer so all chunks are at least that big anyway - final var bodyChunks = getBodyChunks(response, 8192); - final var rowLength = TEXT_TABLE_BODY.length(); - final var expectedChunkSize = ((8193 + rowLength) / rowLength) * rowLength; // ceil(8193/rowLength) * rowLength - assertThat(bodyChunks.size(), allOf(greaterThan(1), equalTo((rowCount * rowLength + expectedChunkSize) / expectedChunkSize))); - assertThat(bodyChunks.get(0).length(), equalTo(expectedChunkSize)); - assertEquals(expectedBody.toString(), String.join("", bodyChunks)); + final var bodyChunks = getBodyChunks(response, bufferSize); + assertEquals("chunk count", expectedChunkCount, bodyChunks.size()); + assertEquals("first chunk size", expectedChunkSize, bodyChunks.get(0).length()); + assertEquals("body contents", expectedBody.toString(), String.join("", bodyChunks)); } public void testEmptyTable() throws Exception { From a598e6b8f7b783ee85f6b3439b0912485089f917 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 12 Dec 2022 10:39:01 +1100 Subject: [PATCH 215/919] Remove the deprecated Authentication#getSourceRealm method (#92222) This PR removes the deprecated Authentication#getSourceRealm method. Its usage is mostly replaced by #getEffectiveSubject#getRealm except for ApiKeyService#getCreatorRealmName and ApiKeyService#getCreatorRealmType which has a special handling to return authenticatingSubject's realm when run-as lookup fails. This is to maintain BWC since these information is used in audit logs. Therefore, even it is technically incorrect, we should not break it without careful planning. Relates: #88494 --- .../core/security/authc/Authentication.java | 52 ++++++++++--------- .../ManageOwnApiKeyClusterPrivilege.java | 4 +- .../authc/AuthenticationTestHelper.java | 9 +++- .../security/authc/AuthenticationTests.java | 30 +++-------- .../xpack/security/authc/ApiKeyService.java | 12 ++++- .../xpack/security/authc/TokenService.java | 7 +-- .../IndexServiceAccountTokenStore.java | 18 ++++--- .../operator/FileOperatorUsersStore.java | 5 +- .../audit/logfile/LoggingAuditTrailTests.java | 2 +- .../security/authc/ApiKeyServiceTests.java | 19 +++++-- .../authc/AuthenticationServiceTests.java | 12 ++--- .../IndexServiceAccountTokenStoreTests.java | 4 +- .../support/ApiKeyBoolQueryBuilderTests.java | 11 ++-- 13 files changed, 105 insertions(+), 80 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java index 85ce83baa373..d643ea99ab63 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java @@ -70,6 +70,22 @@ * Authentication is serialized and travels across the cluster nodes as the sub-requests are handled, * and can also be cached by long-running jobs that continue to act on behalf of the user, beyond * the lifetime of the original request. + * + * The authentication consists of two {@link Subject}s + *

+ * If {@link #isRunAs()} is {@code false}, the two {@link Subject}s will be the same object. + * + * Authentication also has a {@link #type} that indicates which mechanism the {@link #authenticatingSubject} + * uses to perform the authentication. + * + * The Authentication's version is its {@link Subject}'s version, i.e. {@code getEffectiveSubject().getVersion()}. + * It is guaranteed that the versions are identical for the two Subjects. Hence {@code getAuthenticatingSubject().getVersion()} + * will give out the same result. But using {@code getEffectiveSubject()} is more idiomatic since most callers + * of this class should just need to know about the {@link #effectiveSubject}. That is, often times, the caller + * begins with {@code authentication.getEffectiveSubject()} for interrogating an Authentication object. */ public final class Authentication implements ToXContentObject { @@ -167,22 +183,8 @@ public boolean isRunAs() { return authenticatingSubject != effectiveSubject; } - /** - * Get the realm where the effective user comes from. - * The effective user is the es-security-runas-user if present or the authenticated user. - * - * Use {@code getEffectiveSubject().getRealm()} instead. - */ - @Deprecated - public RealmRef getSourceRealm() { - // TODO: This code retains the existing behaviour which is slightly wrong because - // when run-as lookup fails, the effectiveSubject will have a null realm. In this - // case, the code returns the authenticatingSubject's realm. This is wrong in theory - // because it is not the intention of this method. In practice, it does not matter - // because failed lookup will be rejected at authZ time. But fixing it causes test - // failures. So leave it for now. - final RealmRef sourceRealm = effectiveSubject.getRealm(); - return sourceRealm == null ? authenticatingSubject.getRealm() : sourceRealm; + public boolean isFailedRunAs() { + return isRunAs() && effectiveSubject.getRealm() == null; } /** @@ -228,9 +230,6 @@ public Authentication maybeRewriteForOlderVersion(Version olderVersion) { ); } - if (isAssignedToDomain() && false == newAuthentication.isAssignedToDomain()) { - logger.info("Rewriting authentication [" + this + "] without domain"); - } return newAuthentication; } @@ -262,7 +261,6 @@ public Authentication runAs(User runAs, @Nullable RealmRef lookupRealmRef) { public Authentication token() { assert false == isServiceAccount(); final Authentication newTokenAuthentication = new Authentication(effectiveSubject, authenticatingSubject, AuthenticationType.TOKEN); - assert Objects.equals(getDomain(), newTokenAuthentication.getDomain()); return newTokenAuthentication; } @@ -325,14 +323,15 @@ public Authentication maybeAddAnonymousRoles(@Nullable AnonymousUser anonymousUs } } + // Package private for tests /** * Returns {@code true} if the effective user belongs to a realm under a domain. - * See also {@link #getDomain()} and {@link #getSourceRealm()}. */ - public boolean isAssignedToDomain() { + boolean isAssignedToDomain() { return getDomain() != null; } + // Package private for tests /** * Returns the {@link RealmDomain} that the effective user belongs to. * A user belongs to a realm which in turn belongs to a domain. @@ -340,8 +339,12 @@ public boolean isAssignedToDomain() { * The same username can be authenticated by different realms (e.g. with different credential types), * but resources created across realms cannot be accessed unless the realms are also part of the same domain. */ - public @Nullable RealmDomain getDomain() { - return getSourceRealm().getDomain(); + @Nullable + RealmDomain getDomain() { + if (isFailedRunAs()) { + return null; + } + return getEffectiveSubject().getRealm().getDomain(); } public boolean isAuthenticatedWithServiceAccount() { @@ -861,6 +864,7 @@ public static Authentication newApiKeyAuthentication(AuthenticationResult private static RealmRef maybeRewriteRealmRef(Version streamVersion, RealmRef realmRef) { if (realmRef != null && realmRef.getDomain() != null && streamVersion.before(VERSION_REALM_DOMAINS)) { + logger.info("Rewriting realm [" + realmRef + "] without domain"); // security domain erasure new RealmRef(realmRef.getName(), realmRef.getType(), realmRef.getNodeName(), null); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageOwnApiKeyClusterPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageOwnApiKeyClusterPrivilege.java index 9368666019d7..14b563fcc478 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageOwnApiKeyClusterPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ManageOwnApiKeyClusterPrivilege.java @@ -145,11 +145,11 @@ private static boolean checkIfUserIsOwnerOfApiKeys( if (false == username.equals(authentication.getEffectiveSubject().getUser().principal())) { return false; } - RealmDomain domain = authentication.getSourceRealm().getDomain(); + RealmDomain domain = authentication.getEffectiveSubject().getRealm().getDomain(); if (domain != null) { return domain.realms().stream().anyMatch(realmIdentifier -> realmName.equals(realmIdentifier.getName())); } else { - return realmName.equals(authentication.getSourceRealm().getName()); + return realmName.equals(authentication.getEffectiveSubject().getRealm().getName()); } } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTestHelper.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTestHelper.java index abfb0e5e6190..7971b7cd30de 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTestHelper.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTestHelper.java @@ -377,7 +377,12 @@ public Authentication build() { return build(ESTestCase.randomBoolean()); } - public Authentication build(boolean runAsIfNotAlready) { + /** + * @param maybeRunAsIfNotAlready If the authentication is *not* run-as and the subject is a realm user, it will be transformed + * into a run-as authentication by moving the realm user to be the run-as user. The authenticating + * subject can be either a realm user or an API key (in general any subject type that can run-as). + */ + public Authentication build(boolean maybeRunAsIfNotAlready) { if (authenticatingAuthentication != null) { if (user == null) { user = randomUser(); @@ -402,7 +407,7 @@ public Authentication build(boolean runAsIfNotAlready) { realmRef = randomRealmRef(isRealmUnderDomain == null ? ESTestCase.randomBoolean() : isRealmUnderDomain); } assert false == SYNTHETIC_REALM_TYPES.contains(realmRef.getType()) : "use dedicate methods for synthetic realms"; - if (runAsIfNotAlready) { + if (maybeRunAsIfNotAlready) { authentication = builder().runAs().user(user).realmRef(realmRef).build(); } else { authentication = Authentication.newRealmAuthentication(user, realmRef); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTests.java index 8f8f82fc84d0..7ddc0f91c8b8 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTests.java @@ -40,28 +40,14 @@ public class AuthenticationTests extends ESTestCase { - public void testWillGetLookedUpByWhenItExists() { - final RealmRef authenticatedBy = new RealmRef("auth_by", "auth_by_type", "node"); - final RealmRef lookedUpBy = new RealmRef("lookup_by", "lookup_by_type", "node"); - final Authentication authentication = AuthenticationTestHelper.builder() - .user(new User("not-user")) - .realmRef(authenticatedBy) - .runAs() - .user(new User("user")) - .realmRef(lookedUpBy) - .build(); - - assertEquals(lookedUpBy, authentication.getSourceRealm()); - } - - public void testWillGetAuthenticateByWhenLookupIsNull() { - final RealmRef authenticatedBy = new RealmRef("auth_by", "auth_by_type", "node"); - final Authentication authentication = AuthenticationTestHelper.builder() - .user(new User("user")) - .realmRef(authenticatedBy) - .build(false); - - assertEquals(authenticatedBy, authentication.getSourceRealm()); + public void testIsFailedRunAs() { + final Authentication failedAuthentication = randomRealmAuthentication(randomBoolean()).runAs(randomUser(), null); + assertTrue(failedAuthentication.isRunAs()); + assertTrue(failedAuthentication.isFailedRunAs()); + + final Authentication authentication = AuthenticationTestHelper.builder().realm().runAs().build(); + assertTrue(authentication.isRunAs()); + assertFalse(authentication.isFailedRunAs()); } public void testCanAccessResourcesOf() { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index e18a8a6ea011..4fda6d853229 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -1748,7 +1748,11 @@ public static String getCreatorRealmName(final Authentication authentication) { } else { // TODO we should use the effective subject realm here but need to handle the failed lookup scenario, in which the realm may be // `null`. Since this method is used in audit logging, this requires some care. - return authentication.getSourceRealm().getName(); + if (authentication.isFailedRunAs()) { + return authentication.getAuthenticatingSubject().getRealm().getName(); + } else { + return authentication.getEffectiveSubject().getRealm().getName(); + } } } @@ -1791,7 +1795,11 @@ public static String getCreatorRealmType(final Authentication authentication) { } else { // TODO we should use the effective subject realm here but need to handle the failed lookup scenario, in which the realm may be // `null`. Since this method is used in audit logging, this requires some care. - return authentication.getSourceRealm().getType(); + if (authentication.isFailedRunAs()) { + return authentication.getAuthenticatingSubject().getRealm().getType(); + } else { + return authentication.getEffectiveSubject().getRealm().getType(); + } } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java index 993dd666702b..2488c9b12034 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java @@ -1805,12 +1805,13 @@ static BytesReference createTokenDocument( } builder.endObject().endObject(); } + final Authentication.RealmRef userTokenEffectiveRealm = userToken.getAuthentication().getEffectiveSubject().getRealm(); builder.startObject("access_token") .field("invalidated", false) .field("user_token", userToken) - .field("realm", userToken.getAuthentication().getSourceRealm().getName()); - if (userToken.getAuthentication().getSourceRealm().getDomain() != null) { - builder.field("realm_domain", userToken.getAuthentication().getSourceRealm().getDomain()); + .field("realm", userTokenEffectiveRealm.getName()); + if (userTokenEffectiveRealm.getDomain() != null) { + builder.field("realm_domain", userTokenEffectiveRealm.getDomain()); } builder.endObject().endObject(); return BytesReference.bytes(builder); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountTokenStore.java index 134931160c2f..36a0b7667a7a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountTokenStore.java @@ -47,6 +47,7 @@ import org.elasticsearch.xpack.core.security.action.service.TokenInfo; import org.elasticsearch.xpack.core.security.action.service.TokenInfo.TokenSource; import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.Subject; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken.ServiceAccountTokenId; @@ -269,15 +270,16 @@ private XContentBuilder newDocument(Authentication authentication, ServiceAccoun .field("creation_time", clock.instant().toEpochMilli()) .field("enabled", true); { + final Subject effectiveSubject = authentication.getEffectiveSubject(); builder.startObject("creator") - .field("principal", authentication.getEffectiveSubject().getUser().principal()) - .field("full_name", authentication.getEffectiveSubject().getUser().fullName()) - .field("email", authentication.getEffectiveSubject().getUser().email()) - .field("metadata", authentication.getEffectiveSubject().getUser().metadata()) - .field("realm", authentication.getSourceRealm().getName()) - .field("realm_type", authentication.getSourceRealm().getType()); - if (authentication.getSourceRealm().getDomain() != null) { - builder.field("realm_domain", authentication.getSourceRealm().getDomain()); + .field("principal", effectiveSubject.getUser().principal()) + .field("full_name", effectiveSubject.getUser().fullName()) + .field("email", effectiveSubject.getUser().email()) + .field("metadata", effectiveSubject.getUser().metadata()) + .field("realm", effectiveSubject.getRealm().getName()) + .field("realm_type", effectiveSubject.getRealm().getType()); + if (effectiveSubject.getRealm().getDomain() != null) { + builder.field("realm_domain", effectiveSubject.getRealm().getDomain()); } builder.endObject(); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java index c65f75b1deb8..ee3496b91aff 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java @@ -68,8 +68,11 @@ public boolean isOperatorUser(Authentication authentication) { // If not null, it will be compared exactly as well. // The special handling for realm name is because there can only be one file or native realm and it does // not matter what the name is. + final Authentication.RealmRef realm = authentication.getEffectiveSubject().getRealm(); + if (realm == null) { + return false; + } return operatorUsersDescriptor.groups.stream().anyMatch(group -> { - final Authentication.RealmRef realm = authentication.getSourceRealm(); final boolean match = group.usernames.contains(authentication.getEffectiveSubject().getUser().principal()) && group.authenticationType == authentication.getAuthenticationType() && realm.getType().equals(group.realmType) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java index e458d4ef7666..fcd6601360a9 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java @@ -2019,7 +2019,7 @@ public void testAccessGrantedInternalSystemAction() throws Exception { .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "access_granted") .put(LoggingAuditTrail.AUTHENTICATION_TYPE_FIELD_NAME, authentication.getAuthenticationType().toString()) .put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, systemUser.principal()) - .put(LoggingAuditTrail.PRINCIPAL_REALM_FIELD_NAME, authentication.getSourceRealm().getName()) + .put(LoggingAuditTrail.PRINCIPAL_REALM_FIELD_NAME, authentication.getEffectiveSubject().getRealm().getName()) .put(LoggingAuditTrail.ACTION_FIELD_NAME, "internal:_action") .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, request.getClass().getSimpleName()) .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index a9c7b14afa6e..b7a1cfd05d5a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -1512,8 +1512,8 @@ public void testGetCreatorRealm() { // Realm final Authentication authentication3 = AuthenticationTests.randomRealmAuthentication(randomBoolean()); - assertThat(ApiKeyService.getCreatorRealmName(authentication3), equalTo(authentication3.getSourceRealm().getName())); - assertThat(ApiKeyService.getCreatorRealmType(authentication3), equalTo(authentication3.getSourceRealm().getType())); + assertThat(ApiKeyService.getCreatorRealmName(authentication3), equalTo(authentication3.getEffectiveSubject().getRealm().getName())); + assertThat(ApiKeyService.getCreatorRealmType(authentication3), equalTo(authentication3.getEffectiveSubject().getRealm().getType())); // Realm run-as final Authentication authentication4 = authentication3.runAs(AuthenticationTests.randomUser(), lookupRealmRef); @@ -1526,8 +1526,19 @@ public void testGetCreatorRealm() { AuthenticationTests.randomAnonymousAuthentication(), AuthenticationTests.randomInternalAuthentication() ); - assertThat(ApiKeyService.getCreatorRealmName(authentication5), equalTo(authentication5.getSourceRealm().getName())); - assertThat(ApiKeyService.getCreatorRealmType(authentication5), equalTo(authentication5.getSourceRealm().getType())); + assertThat(ApiKeyService.getCreatorRealmName(authentication5), equalTo(authentication5.getEffectiveSubject().getRealm().getName())); + assertThat(ApiKeyService.getCreatorRealmType(authentication5), equalTo(authentication5.getEffectiveSubject().getRealm().getType())); + + // Failed run-as returns authenticating subject's realm + final Authentication authentication6 = authentication3.runAs(AuthenticationTests.randomUser(), null); + assertThat( + ApiKeyService.getCreatorRealmName(authentication6), + equalTo(authentication6.getAuthenticatingSubject().getRealm().getName()) + ); + assertThat( + ApiKeyService.getCreatorRealmType(authentication6), + equalTo(authentication6.getAuthenticatingSubject().getRealm().getType()) + ); } public void testGetOwnersRealmNames() { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index fdc2a613c8ad..ac604fb7a5ea 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -388,7 +388,7 @@ public void testTokenFirstMissingSecondFound() throws Exception { service.authenticate("action", transportRequest, true, ActionListener.wrap(authentication -> { assertThat(threadContext.getTransient(AuthenticationResult.THREAD_CONTEXT_KEY), is(authenticationResult)); assertThat(threadContext.getTransient(AuthenticationField.AUTHENTICATION_KEY), is(authentication)); - assertThat(authentication.getDomain(), is(secondDomain)); + assertThat(authentication.getEffectiveSubject().getRealm().getDomain(), is(secondDomain)); verify(auditTrail).authenticationSuccess(anyString(), eq(authentication), eq("action"), eq(transportRequest)); setCompletedToTrue(completed); }, this::logAndFail)); @@ -992,7 +992,7 @@ public void testAuthenticateTransportContextAndHeader() throws Exception { assertThat(authentication, notNullValue()); assertThat(authentication.getEffectiveSubject().getUser(), sameInstance(user1)); assertThat(authentication.getAuthenticationType(), is(AuthenticationType.REALM)); - assertThat(authentication.getDomain(), is(firstDomain)); + assertThat(authentication.getEffectiveSubject().getRealm().getDomain(), is(firstDomain)); assertThreadContextContainsAuthentication(authentication); authRef.set(authentication); authHeaderRef.set(threadContext.getHeader(AuthenticationField.AUTHENTICATION_KEY)); @@ -1270,7 +1270,7 @@ public void testAnonymousUserRest() throws Exception { assertThat(result, notNullValue()); assertThat(result.v1().getEffectiveSubject().getUser(), sameInstance((Object) anonymousUser)); assertThat(result.v1().getAuthenticationType(), is(AuthenticationType.ANONYMOUS)); - assertThat(result.v1().getDomain(), nullValue()); + assertThat(result.v1().getEffectiveSubject().getRealm().getDomain(), nullValue()); assertThreadContextContainsAuthentication(result.v1()); assertThat(expectAuditRequestId(threadContext), is(result.v2())); verify(auditTrail).authenticationSuccess(result.v2(), result.v1(), request); @@ -1345,7 +1345,7 @@ public void testAnonymousUserTransportNoDefaultUser() throws Exception { assertThat(expectAuditRequestId(threadContext), is(result.v2())); assertThat(result.v1().getEffectiveSubject().getUser(), sameInstance(anonymousUser)); assertThat(result.v1().getAuthenticationType(), is(AuthenticationType.ANONYMOUS)); - assertThat(result.v1().getDomain(), nullValue()); + assertThat(result.v1().getEffectiveSubject().getRealm().getDomain(), nullValue()); assertThreadContextContainsAuthentication(result.v1()); verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result.v1()), eq(threadContext)); }); @@ -1382,7 +1382,7 @@ public void testAnonymousUserTransportWithDefaultUser() throws Exception { assertThat(expectAuditRequestId(threadContext), is(result.v2())); assertThat(result.v1().getEffectiveSubject().getUser(), sameInstance(SystemUser.INSTANCE)); assertThat(result.v1().getAuthenticationType(), is(AuthenticationType.INTERNAL)); - assertThat(result.v1().getDomain(), nullValue()); + assertThat(result.v1().getEffectiveSubject().getRealm().getDomain(), nullValue()); assertThreadContextContainsAuthentication(result.v1()); verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result.v1()), eq(threadContext)); }); @@ -2133,7 +2133,7 @@ public void testApiKeyAuth() { assertThat(result.v1().getEffectiveSubject().getUser().fullName(), is("john doe")); assertThat(result.v1().getEffectiveSubject().getUser().email(), is("john@doe.com")); assertThat(result.v1().getAuthenticationType(), is(AuthenticationType.API_KEY)); - assertThat(result.v1().getDomain(), nullValue()); + assertThat(result.v1().getEffectiveSubject().getRealm().getDomain(), nullValue()); verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result.v1()), eq(threadContext)); }); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountTokenStoreTests.java index cb4279f15316..f536a696a8e2 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountTokenStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountTokenStoreTests.java @@ -218,8 +218,8 @@ public void testCreateToken() throws ExecutionException, InterruptedException { assertThat(creatorMap.get("full_name"), equalTo(authentication.getEffectiveSubject().getUser().fullName())); assertThat(creatorMap.get("email"), equalTo(authentication.getEffectiveSubject().getUser().email())); assertThat(creatorMap.get("metadata"), equalTo(authentication.getEffectiveSubject().getUser().metadata())); - assertThat(creatorMap.get("realm"), equalTo(authentication.getSourceRealm().getName())); - assertThat(creatorMap.get("realm_type"), equalTo(authentication.getSourceRealm().getType())); + assertThat(creatorMap.get("realm"), equalTo(authentication.getEffectiveSubject().getRealm().getName())); + assertThat(creatorMap.get("realm_type"), equalTo(authentication.getEffectiveSubject().getRealm().getType())); final CreateServiceAccountTokenResponse createServiceAccountTokenResponse1 = future1.get(); assertNotNull(createServiceAccountTokenResponse1); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilderTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilderTests.java index 73393beaae50..d7f994d7499f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilderTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilderTests.java @@ -73,10 +73,15 @@ public void testQueryForDomainAuthentication() { apiKeysQuery.filter().get(1), is(QueryBuilders.termQuery("creator.principal", authentication.getEffectiveSubject().getUser().principal())) ); - if (authentication.getDomain().realms().size() == 1) { + if (authentication.getEffectiveSubject().getRealm().getDomain().realms().size() == 1) { assertThat( apiKeysQuery.filter().get(2), - is(QueryBuilders.termQuery("creator.realm", authentication.getDomain().realms().stream().findFirst().get().getName())) + is( + QueryBuilders.termQuery( + "creator.realm", + authentication.getEffectiveSubject().getRealm().getDomain().realms().stream().findFirst().get().getName() + ) + ) ); } else { assertThat(apiKeysQuery.filter().get(2), instanceOf(BoolQueryBuilder.class)); @@ -84,7 +89,7 @@ public void testQueryForDomainAuthentication() { assertThat(((BoolQueryBuilder) apiKeysQuery.filter().get(2)).mustNot().size(), is(0)); assertThat(((BoolQueryBuilder) apiKeysQuery.filter().get(2)).filter().size(), is(0)); assertThat(((BoolQueryBuilder) apiKeysQuery.filter().get(2)).minimumShouldMatch(), is("1")); - for (RealmConfig.RealmIdentifier realmIdentifier : authentication.getDomain().realms()) { + for (RealmConfig.RealmIdentifier realmIdentifier : authentication.getEffectiveSubject().getRealm().getDomain().realms()) { assertThat( ((BoolQueryBuilder) apiKeysQuery.filter().get(2)).should(), hasItem(QueryBuilders.termQuery("creator.realm", realmIdentifier.getName())) From 8584d0ee4fd6fc400fbb652560bbda8246e23491 Mon Sep 17 00:00:00 2001 From: Joe Gallo Date: Sun, 11 Dec 2022 18:42:21 -0500 Subject: [PATCH 216/919] Switch the order of the GroupedActionListener args (#92253) --- .../node/tasks/CancellableTasksIT.java | 4 +-- .../snapshots/ConcurrentSnapshotsIT.java | 2 +- .../snapshots/SnapshotStatusApisIT.java | 4 +-- .../TransportResetFeatureStateAction.java | 4 +-- .../get/TransportGetSnapshotsAction.java | 4 +-- .../TransportGetShardSnapshotAction.java | 4 +-- .../action/search/ClearScrollController.java | 4 +-- .../action/support/GroupedActionListener.java | 4 +-- .../cluster/NodeConnectionsService.java | 8 ++--- .../allocation/DiskThresholdMonitor.java | 2 +- .../index/seqno/ReplicationTracker.java | 11 ++++--- .../indices/SystemIndexManager.java | 4 +-- .../elasticsearch/indices/SystemIndices.java | 4 +-- .../blobstore/BlobStoreRepository.java | 28 ++++++++--------- .../service/ReservedClusterStateService.java | 21 +++++++------ .../rest/action/cat/RestIndicesAction.java | 4 +-- .../snapshots/RestoreService.java | 25 ++++++++------- .../snapshots/SnapshotsService.java | 4 +-- .../tasks/TaskCancellationService.java | 8 ++--- .../transport/RemoteClusterService.java | 2 +- .../support/GroupedActionListenerTests.java | 8 ++--- .../ShardSnapshotTaskRunnerTests.java | 4 +-- .../snapshots/SnapshotResiliencyTests.java | 10 +++--- .../AbstractSnapshotIntegTestCase.java | 2 +- .../ccr/action/AutoFollowCoordinator.java | 4 +-- .../ccr/action/TransportUnfollowAction.java | 31 ++++++++++--------- .../store/RoleReferenceIntersection.java | 21 +++++++------ .../core/LocalStateCompositeXPackPlugin.java | 4 +-- .../TransportDeprecationInfoAction.java | 4 +-- .../xpack/slm/SnapshotRetentionTask.java | 6 ++-- .../ml/action/TransportCloseJobAction.java | 4 +-- .../rest/cat/RestCatTrainedModelsAction.java | 4 +-- .../cache/common/SparseFileTracker.java | 8 ++--- .../store/SearchableSnapshotDirectory.java | 13 +++++--- .../InitialNodeSecurityAutoConfiguration.java | 4 +-- .../ReservedRoleMappingAction.java | 4 +-- .../TransportSamlInvalidateSessionAction.java | 4 +-- .../action/user/TransportGetUsersAction.java | 4 +-- .../support/mapper/CompositeRoleMapper.java | 4 +-- .../security/authz/AuthorizationService.java | 2 +- .../authz/store/CompositeRolesStore.java | 4 +-- .../authz/store/NativePrivilegeStore.java | 8 ++--- .../blobstore/testkit/BlobAnalyzeAction.java | 4 +-- .../action/TransportGetCheckpointAction.java | 4 +-- .../action/TransportStopTransformAction.java | 8 ++--- .../checkpoint/DefaultCheckpointProvider.java | 2 +- 46 files changed, 172 insertions(+), 154 deletions(-) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/action/admin/cluster/node/tasks/CancellableTasksIT.java b/server/src/internalClusterTest/java/org/elasticsearch/action/admin/cluster/node/tasks/CancellableTasksIT.java index 2ce42b653c1f..3bcd7626a5f0 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/action/admin/cluster/node/tasks/CancellableTasksIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/action/admin/cluster/node/tasks/CancellableTasksIT.java @@ -508,8 +508,8 @@ protected void doExecute(Task task, TestRequest request, ActionListener subRequests = request.subRequests; GroupedActionListener groupedListener = new GroupedActionListener<>( - listener.map(r -> new TestResponse()), - subRequests.size() + 1 + subRequests.size() + 1, + listener.map(r -> new TestResponse()) ); transportService.getThreadPool().generic().execute(ActionRunnable.supply(groupedListener, () -> { assertTrue(beforeExecuteLatches.get(request).await(60, TimeUnit.SECONDS)); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/snapshots/ConcurrentSnapshotsIT.java b/server/src/internalClusterTest/java/org/elasticsearch/snapshots/ConcurrentSnapshotsIT.java index 9c656109dfd2..28c4b85df389 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/snapshots/ConcurrentSnapshotsIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/snapshots/ConcurrentSnapshotsIT.java @@ -219,7 +219,7 @@ public void testDeletesAreBatched() throws Exception { logger.info("--> waiting for batched deletes to finish"); final PlainActionFuture> allDeletesDone = new PlainActionFuture<>(); - final ActionListener deletesListener = new GroupedActionListener<>(allDeletesDone, deleteFutures.size()); + final ActionListener deletesListener = new GroupedActionListener<>(deleteFutures.size(), allDeletesDone); for (StepListener deleteFuture : deleteFutures) { deleteFuture.addListener(deletesListener); } diff --git a/server/src/internalClusterTest/java/org/elasticsearch/snapshots/SnapshotStatusApisIT.java b/server/src/internalClusterTest/java/org/elasticsearch/snapshots/SnapshotStatusApisIT.java index 131ace6a3364..f07057c496ae 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/snapshots/SnapshotStatusApisIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/snapshots/SnapshotStatusApisIT.java @@ -661,8 +661,8 @@ public void testConcurrentCreateAndStatusAPICalls() throws Exception { final var waitForCompletion = randomBoolean(); final var createsListener = new PlainActionFuture(); final var createsGroupedListener = new GroupedActionListener( - createsListener.map(ignored -> null), - snapshotNames.length + snapshotNames.length, + createsListener.map(ignored -> null) ); for (final var snapshotName : snapshotNames) { clusterAdmin().prepareCreateSnapshot(repoName, snapshotName) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/TransportResetFeatureStateAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/TransportResetFeatureStateAction.java index 6fa73bf2ea14..c0c928e2743d 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/TransportResetFeatureStateAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/TransportResetFeatureStateAction.java @@ -73,11 +73,11 @@ protected void masterOperation( final int features = systemIndices.getFeatures().size(); GroupedActionListener groupedActionListener = new GroupedActionListener<>( + systemIndices.getFeatures().size(), listener.map(responses -> { assert features == responses.size(); return new ResetFeatureStateResponse(new ArrayList<>(responses)); - }), - systemIndices.getFeatures().size() + }) ); for (SystemIndices.Feature feature : systemIndices.getFeatures()) { diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java index 1235afc3b860..3df54bc64363 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java @@ -176,7 +176,7 @@ private void getMultipleReposSnapshotInfo( return; } final GroupedActionListener, SnapshotsInRepo>> groupedActionListener = - new GroupedActionListener<>(listener.map(responses -> { + new GroupedActionListener<>(repos.size(), listener.map(responses -> { assert repos.size() == responses.size(); final List allSnapshots = responses.stream() .map(Tuple::v2) @@ -203,7 +203,7 @@ private void getMultipleReposSnapshotInfo( responses.stream().map(Tuple::v2).filter(Objects::nonNull).mapToInt(s -> s.totalCount).sum(), remaining ); - }), repos.size()); + })); for (final RepositoryMetadata repo : repos) { final String repoName = repo.name(); diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/shard/TransportGetShardSnapshotAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/shard/TransportGetShardSnapshotAction.java index 6cc255956f20..a58e1a778295 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/shard/TransportGetShardSnapshotAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/shard/TransportGetShardSnapshotAction.java @@ -85,8 +85,8 @@ protected void masterOperation( } GroupedActionListener, RepositoryException>> groupedActionListener = new GroupedActionListener<>( - listener.map(TransportGetShardSnapshotAction::transformToResponse), - repositories.size() + repositories.size(), + listener.map(TransportGetShardSnapshotAction::transformToResponse) ); BlockingQueue repositoriesQueue = new LinkedBlockingQueue<>(repositories); diff --git a/server/src/main/java/org/elasticsearch/action/search/ClearScrollController.java b/server/src/main/java/org/elasticsearch/action/search/ClearScrollController.java index 1a0f0ad6621d..8a19876777de 100644 --- a/server/src/main/java/org/elasticsearch/action/search/ClearScrollController.java +++ b/server/src/main/java/org/elasticsearch/action/search/ClearScrollController.java @@ -170,8 +170,8 @@ public static void closeContexts( } lookupListener.whenComplete(nodeLookup -> { final GroupedActionListener groupedListener = new GroupedActionListener<>( - listener.map(rs -> Math.toIntExact(rs.stream().filter(r -> r).count())), - contextIds.size() + contextIds.size(), + listener.map(rs -> Math.toIntExact(rs.stream().filter(r -> r).count())) ); for (SearchContextIdForNode contextId : contextIds) { final DiscoveryNode node = nodeLookup.apply(contextId.getClusterAlias(), contextId.getNode()); diff --git a/server/src/main/java/org/elasticsearch/action/support/GroupedActionListener.java b/server/src/main/java/org/elasticsearch/action/support/GroupedActionListener.java index 7a65f100456a..b3d7e74bf88a 100644 --- a/server/src/main/java/org/elasticsearch/action/support/GroupedActionListener.java +++ b/server/src/main/java/org/elasticsearch/action/support/GroupedActionListener.java @@ -31,10 +31,10 @@ public final class GroupedActionListener extends ActionListener.Delegating> delegate, int groupSize) { + public GroupedActionListener(int groupSize, ActionListener> delegate) { super(delegate); if (groupSize <= 0) { assert false : "illegal group size [" + groupSize + "]"; diff --git a/server/src/main/java/org/elasticsearch/cluster/NodeConnectionsService.java b/server/src/main/java/org/elasticsearch/cluster/NodeConnectionsService.java index f88ada956582..077771dbcc76 100644 --- a/server/src/main/java/org/elasticsearch/cluster/NodeConnectionsService.java +++ b/server/src/main/java/org/elasticsearch/cluster/NodeConnectionsService.java @@ -98,8 +98,8 @@ public void connectToNodes(DiscoveryNodes discoveryNodes, Runnable onCompletion) } final GroupedActionListener listener = new GroupedActionListener<>( - ActionListener.wrap(onCompletion), - discoveryNodes.getSize() + discoveryNodes.getSize(), + ActionListener.wrap(onCompletion) ); final List runnables = new ArrayList<>(discoveryNodes.getSize()); @@ -160,8 +160,8 @@ void ensureConnections(Runnable onCompletion) { } else { logger.trace("ensureConnections: {}", targetsByNode); final GroupedActionListener listener = new GroupedActionListener<>( - ActionListener.wrap(onCompletion), - connectionTargets.size() + connectionTargets.size(), + ActionListener.wrap(onCompletion) ); for (final ConnectionTarget connectionTarget : connectionTargets) { runnables.add(connectionTarget.connect(listener)); diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/DiskThresholdMonitor.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/DiskThresholdMonitor.java index f5a4e1ff3a1e..60598d62df01 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/DiskThresholdMonitor.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/DiskThresholdMonitor.java @@ -301,7 +301,7 @@ public void onNewInfo(ClusterInfo info) { } } - final ActionListener listener = new GroupedActionListener<>(ActionListener.wrap(this::checkFinished), 3); + final ActionListener listener = new GroupedActionListener<>(3, ActionListener.wrap(this::checkFinished)); if (reroute) { logger.debug("rerouting shards: [{}]", explanation); diff --git a/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java b/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java index f7140210cfd1..5e1f9c4fc604 100644 --- a/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java +++ b/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java @@ -1472,10 +1472,13 @@ public synchronized boolean hasAllPeerRecoveryRetentionLeases() { public synchronized void createMissingPeerRecoveryRetentionLeases(ActionListener listener) { if (hasAllPeerRecoveryRetentionLeases == false) { final List shardRoutings = routingTable.assignedShards(); - final GroupedActionListener groupedActionListener = new GroupedActionListener<>(ActionListener.wrap(vs -> { - setHasAllPeerRecoveryRetentionLeases(); - listener.onResponse(null); - }, listener::onFailure), shardRoutings.size()); + final GroupedActionListener groupedActionListener = new GroupedActionListener<>( + shardRoutings.size(), + ActionListener.wrap(vs -> { + setHasAllPeerRecoveryRetentionLeases(); + listener.onResponse(null); + }, listener::onFailure) + ); for (ShardRouting shardRouting : shardRoutings) { if (retentionLeases.contains(getPeerRecoveryRetentionLeaseId(shardRouting))) { groupedActionListener.onResponse(null); diff --git a/server/src/main/java/org/elasticsearch/indices/SystemIndexManager.java b/server/src/main/java/org/elasticsearch/indices/SystemIndexManager.java index 2b0243277519..d46d4a4baea0 100644 --- a/server/src/main/java/org/elasticsearch/indices/SystemIndexManager.java +++ b/server/src/main/java/org/elasticsearch/indices/SystemIndexManager.java @@ -116,8 +116,8 @@ public void clusterChanged(ClusterChangedEvent event) { // Use a GroupedActionListener so that we only release the lock once all upgrade attempts have succeeded or failed. // The failures are logged in upgradeIndexMetadata(), so we don't actually care about them here. ActionListener listener = new GroupedActionListener<>( - ActionListener.wrap(() -> isUpgradeInProgress.set(false)), - descriptors.size() + descriptors.size(), + ActionListener.wrap(() -> isUpgradeInProgress.set(false)) ); descriptors.forEach(descriptor -> upgradeIndexMetadata(descriptor, listener)); diff --git a/server/src/main/java/org/elasticsearch/indices/SystemIndices.java b/server/src/main/java/org/elasticsearch/indices/SystemIndices.java index 28750d663582..54e2f04877b6 100644 --- a/server/src/main/java/org/elasticsearch/indices/SystemIndices.java +++ b/server/src/main/java/org/elasticsearch/indices/SystemIndices.java @@ -922,6 +922,7 @@ public static void cleanUpFeature( } GroupedActionListener groupedListener = new GroupedActionListener<>( + taskCount, ActionListener.wrap(listenerResults -> { List errors = listenerResults.stream() .filter(status -> status.getStatus() == ResetFeatureStateResponse.ResetFeatureStateStatus.Status.FAILURE) @@ -936,8 +937,7 @@ public static void cleanUpFeature( errors.forEach(e -> logger.warn(() -> "error while resetting feature [" + name + "]", e.getException())); listener.onResponse(ResetFeatureStateStatus.failure(name, new Exception(exceptions.toString()))); } - }, listener::onFailure), - taskCount + }, listener::onFailure) ); // Send cleanup for the associated indices, they don't need special origin since they are not protected diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java index d2e20b2182fc..3c536328522c 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -964,7 +964,7 @@ private void doDeleteShardSnapshots( writeUpdatedRepoDataStep.whenComplete(updatedRepoData -> { listener.onRepositoryDataWritten(updatedRepoData); // Run unreferenced blobs cleanup in parallel to shard-level snapshot deletion - final ActionListener afterCleanupsListener = new GroupedActionListener<>(ActionListener.wrap(listener::onDone), 2); + final ActionListener afterCleanupsListener = new GroupedActionListener<>(2, ActionListener.wrap(listener::onDone)); cleanupUnlinkedRootAndIndicesBlobs(snapshotIds, foundIndices, rootBlobs, updatedRepoData, afterCleanupsListener); asyncCleanupUnlinkedShardLevelBlobs( repositoryData, @@ -978,10 +978,10 @@ private void doDeleteShardSnapshots( final RepositoryData updatedRepoData = repositoryData.removeSnapshots(snapshotIds, ShardGenerations.EMPTY); writeIndexGen(updatedRepoData, repositoryStateId, repoMetaVersion, Function.identity(), ActionListener.wrap(newRepoData -> { // Run unreferenced blobs cleanup in parallel to shard-level snapshot deletion - final ActionListener afterCleanupsListener = new GroupedActionListener<>(ActionListener.wrap(() -> { + final ActionListener afterCleanupsListener = new GroupedActionListener<>(2, ActionListener.wrap(() -> { listener.onRepositoryDataWritten(newRepoData); listener.onDone(); - }), 2); + })); cleanupUnlinkedRootAndIndicesBlobs(snapshotIds, foundIndices, rootBlobs, newRepoData, afterCleanupsListener); final StepListener> writeMetaAndComputeDeletesStep = new StepListener<>(); writeUpdatedShardMetaDataAndComputeDeletes(snapshotIds, repositoryData, false, writeMetaAndComputeDeletesStep); @@ -1043,8 +1043,8 @@ private void writeUpdatedShardMetaDataAndComputeDeletes( // Listener that flattens out the delete results for each index final ActionListener> deleteIndexMetadataListener = new GroupedActionListener<>( - onAllShardsCompleted.map(res -> res.stream().flatMap(Collection::stream).toList()), - indices.size() + indices.size(), + onAllShardsCompleted.map(res -> res.stream().flatMap(Collection::stream).toList()) ); for (IndexId indexId : indices) { @@ -1058,8 +1058,8 @@ private void writeUpdatedShardMetaDataAndComputeDeletes( .map(id -> oldRepositoryData.indexMetaDataGenerations().indexMetaBlobId(id, indexId)) .collect(Collectors.toSet()); final ActionListener allShardCountsListener = new GroupedActionListener<>( - shardCountListener, - indexMetaGenerations.size() + indexMetaGenerations.size(), + shardCountListener ); final BlobContainer indexContainer = indexContainer(indexId); for (String indexMetaGeneration : indexMetaGenerations) { @@ -1088,8 +1088,8 @@ private void writeUpdatedShardMetaDataAndComputeDeletes( } // Listener for collecting the results of removing the snapshot from each shard's metadata in the current index final ActionListener allShardsListener = new GroupedActionListener<>( - deleteIndexMetadataListener, - shardCount + shardCount, + deleteIndexMetadataListener ); for (int shardId = 0; shardId < shardCount; shardId++) { final int finalShardId = shardId; @@ -1188,13 +1188,13 @@ private void cleanupStaleBlobs( RepositoryData newRepoData, ActionListener listener ) { - final GroupedActionListener groupedListener = new GroupedActionListener<>(ActionListener.wrap(deleteResults -> { + final GroupedActionListener groupedListener = new GroupedActionListener<>(2, ActionListener.wrap(deleteResults -> { DeleteResult deleteResult = DeleteResult.ZERO; for (DeleteResult result : deleteResults) { deleteResult = deleteResult.add(result); } listener.onResponse(deleteResult); - }, listener::onFailure), 2); + }, listener::onFailure)); final Executor executor = threadPool.executor(ThreadPool.Names.SNAPSHOT); final List staleRootBlobs = staleRootBlobs(newRepoData, rootBlobs.keySet()); @@ -1414,7 +1414,7 @@ public void finalizeSnapshot(final FinalizeSnapshotContext finalizeSnapshotConte indexMetaIdentifiers = null; } - final ActionListener allMetaListener = new GroupedActionListener<>(ActionListener.wrap(v -> { + final ActionListener allMetaListener = new GroupedActionListener<>(2 + indices.size(), ActionListener.wrap(v -> { final String slmPolicy = slmPolicy(snapshotInfo); final SnapshotDetails snapshotDetails = new SnapshotDetails( snapshotInfo.state(), @@ -1437,7 +1437,7 @@ public void finalizeSnapshot(final FinalizeSnapshotContext finalizeSnapshotConte } }, onUpdateFailure) ); - }, onUpdateFailure), 2 + indices.size()); + }, onUpdateFailure)); // We ignore all FileAlreadyExistsException when writing metadata since otherwise a master failover while in this method will // mean that no snap-${uuid}.dat blob is ever written for this snapshot. This is safe because any updated version of the @@ -3152,7 +3152,7 @@ private static ActionListener fileQueueListener( int numberOfFiles, ActionListener> listener ) { - return new GroupedActionListener<>(listener, numberOfFiles).delegateResponse((l, e) -> { + return new GroupedActionListener<>(numberOfFiles, listener).delegateResponse((l, e) -> { files.clear(); // Stop uploading the remaining files if we run into any exception l.onFailure(e); }); diff --git a/server/src/main/java/org/elasticsearch/reservedstate/service/ReservedClusterStateService.java b/server/src/main/java/org/elasticsearch/reservedstate/service/ReservedClusterStateService.java index 0a3a9f447d0e..251bd5b90d45 100644 --- a/server/src/main/java/org/elasticsearch/reservedstate/service/ReservedClusterStateService.java +++ b/server/src/main/java/org/elasticsearch/reservedstate/service/ReservedClusterStateService.java @@ -351,17 +351,20 @@ void executeNonStateTransformationSteps( return; } - GroupedActionListener postTasksListener = new GroupedActionListener<>(new ActionListener<>() { - @Override - public void onResponse(Collection updateKeyTaskResult) { - listener.onResponse(updateKeyTaskResult); - } + GroupedActionListener postTasksListener = new GroupedActionListener<>( + nonStateTransforms.size(), + new ActionListener<>() { + @Override + public void onResponse(Collection updateKeyTaskResult) { + listener.onResponse(updateKeyTaskResult); + } - @Override - public void onFailure(Exception e) { - listener.onFailure(e); + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } } - }, nonStateTransforms.size()); + ); for (var transform : nonStateTransforms) { // non cluster state transforms don't modify the cluster state, they however are given a chance to return a more diff --git a/server/src/main/java/org/elasticsearch/rest/action/cat/RestIndicesAction.java b/server/src/main/java/org/elasticsearch/rest/action/cat/RestIndicesAction.java index 2c63bcfb4810..63a959fcb5a4 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/cat/RestIndicesAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/cat/RestIndicesAction.java @@ -217,7 +217,7 @@ private GroupedActionListener createGroupedListener( final int size, final ActionListener listener ) { - return new GroupedActionListener<>(new ActionListener.Delegating<>(listener) { + return new GroupedActionListener<>(size, new ActionListener.Delegating<>(listener) { @Override public void onResponse(final Collection responses) { try { @@ -242,7 +242,7 @@ public void onResponse(final Collection responses) { onFailure(e); } } - }, size); + }); } @Override diff --git a/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java b/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java index dae877937fc9..408163d4acae 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java @@ -525,19 +525,22 @@ static void refreshRepositoryUuids(boolean enabled, RepositoriesService reposito "refreshing repository UUIDs for repositories [{}]", repositories.stream().map(repository -> repository.getMetadata().name()).collect(Collectors.joining(",")) ); - final ActionListener groupListener = new GroupedActionListener<>(new ActionListener>() { - @Override - public void onResponse(Collection ignored) { - logger.debug("repository UUID refresh completed"); - refreshListener.onResponse(null); - } + final ActionListener groupListener = new GroupedActionListener<>( + repositories.size(), + new ActionListener>() { + @Override + public void onResponse(Collection ignored) { + logger.debug("repository UUID refresh completed"); + refreshListener.onResponse(null); + } - @Override - public void onFailure(Exception e) { - logger.debug("repository UUID refresh failed", e); - refreshListener.onResponse(null); // this refresh is best-effort, the restore should proceed either way + @Override + public void onFailure(Exception e) { + logger.debug("repository UUID refresh failed", e); + refreshListener.onResponse(null); // this refresh is best-effort, the restore should proceed either way + } } - }, repositories.size()).map(repositoryData -> null /* don't collect the RepositoryData */); + ).map(repositoryData -> null /* don't collect the RepositoryData */); for (Repository repository : repositories) { repository.getRepositoryData(groupListener); diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java index 9d8ddd451290..e58949b8409a 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java @@ -601,8 +601,8 @@ private void startCloning(Repository repository, SnapshotsInProgress.Entry clone final StepListener>> allShardCountsListener = new StepListener<>(); final GroupedActionListener> shardCountListener = new GroupedActionListener<>( - allShardCountsListener, - indices.size() + indices.size(), + allShardCountsListener ); snapshotInfoListener.whenComplete(snapshotInfo -> { for (IndexId indexId : indices) { diff --git a/server/src/main/java/org/elasticsearch/tasks/TaskCancellationService.java b/server/src/main/java/org/elasticsearch/tasks/TaskCancellationService.java index b6742815f44a..788ae17f2bfb 100644 --- a/server/src/main/java/org/elasticsearch/tasks/TaskCancellationService.java +++ b/server/src/main/java/org/elasticsearch/tasks/TaskCancellationService.java @@ -99,7 +99,7 @@ void doCancelTaskAndDescendants(CancellableTask task, String reason, boolean wai if (task.shouldCancelChildrenOnCancellation()) { logger.trace("cancelling task [{}] and its descendants", taskId); StepListener completedListener = new StepListener<>(); - GroupedActionListener groupedListener = new GroupedActionListener<>(completedListener.map(r -> null), 3); + GroupedActionListener groupedListener = new GroupedActionListener<>(3, completedListener.map(r -> null)); Collection childConnections = taskManager.startBanOnChildTasks(task.getId(), reason, () -> { logger.trace("child tasks of parent [{}] are completed", taskId); groupedListener.onResponse(null); @@ -149,7 +149,7 @@ private void setBanOnChildConnections( } final TaskId taskId = new TaskId(localNodeId(), task.getId()); logger.trace("cancelling child tasks of [{}] on child connections {}", taskId, childConnections); - GroupedActionListener groupedListener = new GroupedActionListener<>(listener.map(r -> null), childConnections.size()); + GroupedActionListener groupedListener = new GroupedActionListener<>(childConnections.size(), listener.map(r -> null)); final BanParentTaskRequest banRequest = BanParentTaskRequest.createSetBanParentTaskRequest(taskId, reason, waitForCompletion); for (Transport.Connection connection : childConnections) { assert TransportService.unwrapConnection(connection) == connection : "Child connection must be unwrapped"; @@ -313,8 +313,8 @@ public void messageReceived(final BanParentTaskRequest request, final TransportC ); final List childTasks = taskManager.setBan(request.parentTaskId, request.reason, channel); final GroupedActionListener listener = new GroupedActionListener<>( - new ChannelActionListener<>(channel, BAN_PARENT_ACTION_NAME, request).map(r -> TransportResponse.Empty.INSTANCE), - childTasks.size() + 1 + childTasks.size() + 1, + new ChannelActionListener<>(channel, BAN_PARENT_ACTION_NAME, request).map(r -> TransportResponse.Empty.INSTANCE) ); for (CancellableTask childTask : childTasks) { cancelTaskAndDescendants(childTask, request.reason, request.waitForCompletion, listener); diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java index aff4a6952db4..696028f9dc5b 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java @@ -339,7 +339,7 @@ void initializeRemoteClusters() { return; } - GroupedActionListener listener = new GroupedActionListener<>(future, enabledClusters.size()); + GroupedActionListener listener = new GroupedActionListener<>(enabledClusters.size(), future); for (String clusterAlias : enabledClusters) { updateRemoteCluster(clusterAlias, settings, listener); } diff --git a/server/src/test/java/org/elasticsearch/action/support/GroupedActionListenerTests.java b/server/src/test/java/org/elasticsearch/action/support/GroupedActionListenerTests.java index 418c78122f17..10d53ed208ec 100644 --- a/server/src/test/java/org/elasticsearch/action/support/GroupedActionListenerTests.java +++ b/server/src/test/java/org/elasticsearch/action/support/GroupedActionListenerTests.java @@ -43,7 +43,7 @@ public void onFailure(Exception e) { }; final int groupSize = randomIntBetween(10, 1000); AtomicInteger count = new AtomicInteger(); - GroupedActionListener listener = new GroupedActionListener<>(result, groupSize); + GroupedActionListener listener = new GroupedActionListener<>(groupSize, result); int numThreads = randomIntBetween(2, 5); Thread[] threads = new Thread[numThreads]; CyclicBarrier barrier = new CyclicBarrier(numThreads); @@ -90,7 +90,7 @@ public void onFailure(Exception e) { } }; int size = randomIntBetween(3, 4); - GroupedActionListener listener = new GroupedActionListener<>(result, size); + GroupedActionListener listener = new GroupedActionListener<>(size, result); listener.onResponse(0); IOException ioException = new IOException(); RuntimeException rtException = new RuntimeException(); @@ -111,7 +111,7 @@ public void onFailure(Exception e) { public void testConcurrentFailures() throws InterruptedException { AtomicReference finalException = new AtomicReference<>(); int numGroups = randomIntBetween(10, 100); - GroupedActionListener listener = new GroupedActionListener<>(ActionListener.wrap(r -> {}, finalException::set), numGroups); + GroupedActionListener listener = new GroupedActionListener<>(numGroups, ActionListener.wrap(r -> {}, finalException::set)); ExecutorService executorService = Executors.newFixedThreadPool(numGroups); for (int i = 0; i < numGroups; i++) { executorService.submit(() -> listener.onFailure(new IOException())); @@ -133,7 +133,7 @@ public void testConcurrentFailures() throws InterruptedException { */ public void testRepeatNotificationForTheSameException() { final AtomicReference finalException = new AtomicReference<>(); - final GroupedActionListener listener = new GroupedActionListener<>(ActionListener.wrap(r -> {}, finalException::set), 2); + final GroupedActionListener listener = new GroupedActionListener<>(2, ActionListener.wrap(r -> {}, finalException::set)); final Exception e = new Exception(); // repeat notification for the same exception listener.onFailure(e); diff --git a/server/src/test/java/org/elasticsearch/repositories/blobstore/ShardSnapshotTaskRunnerTests.java b/server/src/test/java/org/elasticsearch/repositories/blobstore/ShardSnapshotTaskRunnerTests.java index 806a3bf73ab8..6cecbd540301 100644 --- a/server/src/test/java/org/elasticsearch/repositories/blobstore/ShardSnapshotTaskRunnerTests.java +++ b/server/src/test/java/org/elasticsearch/repositories/blobstore/ShardSnapshotTaskRunnerTests.java @@ -73,8 +73,8 @@ public void snapshotShard(SnapshotShardContext context) { } else { expectedFileSnapshotTasks.addAndGet(filesToUpload); ActionListener uploadListener = new GroupedActionListener<>( - ActionListener.wrap(finishedShardSnapshots::incrementAndGet), - filesToUpload + filesToUpload, + ActionListener.wrap(finishedShardSnapshots::incrementAndGet) ); for (int i = 0; i < filesToUpload; i++) { taskRunner.enqueueFileSnapshot(context, ShardSnapshotTaskRunnerTests::dummyFileInfo, uploadListener); diff --git a/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java b/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java index 3515728b69a2..5ea3fe970ec8 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java +++ b/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java @@ -665,8 +665,8 @@ public void testBulkSnapshotDeleteWithAbort() { final int inProgressSnapshots = randomIntBetween(1, 5); final StepListener> createOtherSnapshotResponseStepListener = new StepListener<>(); final ActionListener createSnapshotListener = new GroupedActionListener<>( - createOtherSnapshotResponseStepListener, - inProgressSnapshots + inProgressSnapshots, + createOtherSnapshotResponseStepListener ); continueOrDie(createSnapshotResponseStepListener, createSnapshotResponse -> { @@ -825,7 +825,7 @@ public void testConcurrentSnapshotDeleteAndDeleteIndex() throws IOException { firstIndex.set(masterNode.clusterService.state().metadata().index(index).getIndex()); // create a few more indices to make it more likely that the subsequent index delete operation happens before snapshot // finalization - final GroupedActionListener listener = new GroupedActionListener<>(createIndicesListener, indices); + final GroupedActionListener listener = new GroupedActionListener<>(indices, createIndicesListener); for (int i = 0; i < indices; ++i) { client().admin().indices().create(new CreateIndexRequest("index-" + i), listener); } @@ -1172,8 +1172,8 @@ public void testRunConcurrentSnapshots() { final StepListener> allSnapshotsListener = new StepListener<>(); final ActionListener snapshotListener = new GroupedActionListener<>( - allSnapshotsListener, - snapshotNames.size() + snapshotNames.size(), + allSnapshotsListener ); final AtomicBoolean doneIndexing = new AtomicBoolean(false); continueOrDie(createRepoAndIndex(repoName, index, shards), createIndexResponse -> { diff --git a/test/framework/src/main/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java b/test/framework/src/main/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java index 8b1ebce4ec46..2c3dee7dfd4f 100644 --- a/test/framework/src/main/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java @@ -700,7 +700,7 @@ protected List createNSnapshots(String repoName, int count) throws Excep public static List createNSnapshots(Logger logger, String repoName, int count) throws Exception { final PlainActionFuture> allSnapshotsDone = PlainActionFuture.newFuture(); - final ActionListener snapshotsListener = new GroupedActionListener<>(allSnapshotsDone, count); + final ActionListener snapshotsListener = new GroupedActionListener<>(count, allSnapshotsDone); final List snapshotNames = new ArrayList<>(count); final String prefix = RANDOM_SNAPSHOT_NAME_PREFIX + UUIDs.randomBase64UUID(random()).toLowerCase(Locale.ROOT) + "-"; for (int i = 0; i < count; i++) { diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java index 39e32f948f72..50b7d83ee164 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java @@ -594,11 +594,11 @@ private void checkAutoFollowPattern( Consumer resultHandler ) { final GroupedActionListener> groupedListener = new GroupedActionListener<>( + leaderIndicesToFollow.size(), ActionListener.wrap( rs -> resultHandler.accept(new AutoFollowResult(autoFollowPattenName, new ArrayList<>(rs))), e -> { throw new AssertionError("must never happen", e); } - ), - leaderIndicesToFollow.size() + ) ); // Loop through all the as-of-yet-unfollowed indices from the leader diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportUnfollowAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportUnfollowAction.java index 154e3538eb36..caed65fd30a7 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportUnfollowAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportUnfollowAction.java @@ -124,23 +124,26 @@ public void clusterStateProcessed(final ClusterState oldState, final ClusterStat return; } - final GroupedActionListener groupListener = new GroupedActionListener<>(new ActionListener<>() { + final GroupedActionListener groupListener = new GroupedActionListener<>( + numberOfShards, + new ActionListener<>() { - @Override - public void onResponse(final Collection responses) { - logger.trace( - "[{}] removed retention lease [{}] on all leader primary shards", - indexMetadata.getIndex(), - retentionLeaseId - ); - listener.onResponse(AcknowledgedResponse.TRUE); - } + @Override + public void onResponse(final Collection responses) { + logger.trace( + "[{}] removed retention lease [{}] on all leader primary shards", + indexMetadata.getIndex(), + retentionLeaseId + ); + listener.onResponse(AcknowledgedResponse.TRUE); + } - @Override - public void onFailure(final Exception e) { - onLeaseRemovalFailure(indexMetadata.getIndex(), retentionLeaseId, e); + @Override + public void onFailure(final Exception e) { + onLeaseRemovalFailure(indexMetadata.getIndex(), retentionLeaseId, e); + } } - }, numberOfShards); + ); for (int i = 0; i < numberOfShards; i++) { final ShardId followerShardId = new ShardId(indexMetadata.getIndex(), i); final ShardId leaderShardId = new ShardId(leaderIndex, i); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RoleReferenceIntersection.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RoleReferenceIntersection.java index 7c3f1df38c18..26f58693bc25 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RoleReferenceIntersection.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RoleReferenceIntersection.java @@ -37,15 +37,18 @@ public List getRoleReferences() { } public void buildRole(BiConsumer> singleRoleBuilder, ActionListener roleActionListener) { - final GroupedActionListener roleGroupedActionListener = new GroupedActionListener<>(ActionListener.wrap(roles -> { - assert false == roles.isEmpty(); - final Iterator iterator = roles.stream().iterator(); - Role finalRole = iterator.next(); - while (iterator.hasNext()) { - finalRole = finalRole.limitedBy(iterator.next()); - } - roleActionListener.onResponse(finalRole); - }, roleActionListener::onFailure), roleReferences.size()); + final GroupedActionListener roleGroupedActionListener = new GroupedActionListener<>( + roleReferences.size(), + ActionListener.wrap(roles -> { + assert false == roles.isEmpty(); + final Iterator iterator = roles.stream().iterator(); + Role finalRole = iterator.next(); + while (iterator.hasNext()) { + finalRole = finalRole.limitedBy(iterator.next()); + } + roleActionListener.onResponse(finalRole); + }, roleActionListener::onFailure) + ); roleReferences.forEach(roleReference -> singleRoleBuilder.accept(roleReference, roleGroupedActionListener)); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java index 1a1d31e7c16e..503585ee3a1d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java @@ -729,6 +729,7 @@ public void cleanUpFeature( List systemPlugins = filterPlugins(SystemIndexPlugin.class); GroupedActionListener allListeners = new GroupedActionListener<>( + systemPlugins.size(), ActionListener.wrap(listenerResults -> { // If the clean-up produced only one result, use that to pass along. In most // cases it should be 1-1 mapping of feature to response. Passing back success @@ -738,8 +739,7 @@ public void cleanUpFeature( } else { finalListener.onResponse(ResetFeatureStateStatus.success(getFeatureName())); } - }, finalListener::onFailure), - systemPlugins.size() + }, finalListener::onFailure) ); systemPlugins.forEach(plugin -> plugin.cleanUpFeature(clusterService, client, allListeners)); } diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/TransportDeprecationInfoAction.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/TransportDeprecationInfoAction.java index b87c4810f20c..17311571188f 100644 --- a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/TransportDeprecationInfoAction.java +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/TransportDeprecationInfoAction.java @@ -162,6 +162,7 @@ static void pluginSettingIssues( return; } GroupedActionListener groupedActionListener = new GroupedActionListener<>( + enabledCheckers.size(), ActionListener.wrap( checkResults -> listener.onResponse( checkResults.stream() @@ -170,8 +171,7 @@ static void pluginSettingIssues( ) ), listener::onFailure - ), - enabledCheckers.size() + ) ); for (DeprecationChecker checker : checkers) { checker.check(components, groupedActionListener); diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SnapshotRetentionTask.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SnapshotRetentionTask.java index a8092cd35dd5..ad5eacb07b3e 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SnapshotRetentionTask.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SnapshotRetentionTask.java @@ -329,12 +329,12 @@ void deleteSnapshots( final AtomicInteger deleted = new AtomicInteger(0); final AtomicInteger failed = new AtomicInteger(0); final GroupedActionListener allDeletesListener = new GroupedActionListener<>( + snapshotsToDelete.size(), ActionListener.runAfter(listener.map(v -> null), () -> { TimeValue totalElapsedTime = TimeValue.timeValueNanos(nowNanoSupplier.getAsLong() - startTime); logger.debug("total elapsed time for deletion of [{}] snapshots: {}", deleted, totalElapsedTime); slmStats.deletionTime(totalElapsedTime); - }), - snapshotsToDelete.size() + }) ); for (Map.Entry>> entry : snapshotsToDelete.entrySet()) { String repo = entry.getKey(); @@ -354,7 +354,7 @@ private void deleteSnapshots( ActionListener listener ) { - final ActionListener allDeletesListener = new GroupedActionListener<>(listener.map(v -> null), snapshots.size()); + final ActionListener allDeletesListener = new GroupedActionListener<>(snapshots.size(), listener.map(v -> null)); for (Tuple info : snapshots) { final SnapshotId snapshotId = info.v1(); if (runningDeletions.add(snapshotId) == false) { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCloseJobAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCloseJobAction.java index 9e82d3be6c42..4ad78fd5d5c6 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCloseJobAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCloseJobAction.java @@ -342,6 +342,7 @@ private void stopDatafeeds(List runningDatafeedIds, boolean isForce, Tim void isolateDatafeeds(List openJobs, List runningDatafeedIds, ActionListener listener) { GroupedActionListener groupedListener = new GroupedActionListener<>( + runningDatafeedIds.size(), ActionListener.wrap(c -> listener.onResponse(null), e -> { // This is deliberately NOT an error. The reasoning is as follows: // - Isolate datafeed just sets a flag on the datafeed, so cannot fail IF it reaches the running datafeed code @@ -363,8 +364,7 @@ void isolateDatafeeds(List openJobs, List runningDatafeedIds, Ac // race condition easier. logger.info("could not isolate all datafeeds while force closing jobs " + openJobs, e); listener.onResponse(null); - }), - runningDatafeedIds.size() + }) ); for (String runningDatafeedId : runningDatafeedIds) { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/cat/RestCatTrainedModelsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/cat/RestCatTrainedModelsAction.java index 235f5b825501..73701359b8d3 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/cat/RestCatTrainedModelsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/cat/RestCatTrainedModelsAction.java @@ -233,7 +233,7 @@ private GroupedActionListener createGroupedListener( final List configs, final ActionListener
listener ) { - return new GroupedActionListener<>(listener.delegateFailure((l, responses) -> { + return new GroupedActionListener<>(size, listener.delegateFailure((l, responses) -> { GetTrainedModelsStatsAction.Response statsResponse = extractResponse(responses, GetTrainedModelsStatsAction.Response.class); GetDataFrameAnalyticsAction.Response analytics = extractResponse(responses, GetDataFrameAnalyticsAction.Response.class); l.onResponse( @@ -244,7 +244,7 @@ private GroupedActionListener createGroupedListener( analytics == null ? Collections.emptyList() : analytics.getResources().results() ) ); - }), size); + })); } private Table buildTable( diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/cache/common/SparseFileTracker.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/cache/common/SparseFileTracker.java index 8e0bbbc56b2c..f3147e933387 100644 --- a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/cache/common/SparseFileTracker.java +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/cache/common/SparseFileTracker.java @@ -258,8 +258,8 @@ public List waitForRange(final ByteRange range, final ByteRange subRange, f } default -> { final GroupedActionListener groupedActionListener = new GroupedActionListener<>( - wrappedListener.map(progress -> null), - requiredRanges.size() + requiredRanges.size(), + wrappedListener.map(progress -> null) ); requiredRanges.forEach( r -> r.completionListener.addListener(groupedActionListener, Math.min(r.completionListener.end, subRange.end())) @@ -346,8 +346,8 @@ public boolean waitForRangeIfPending(final ByteRange range, final ActionListener } default -> { final GroupedActionListener groupedActionListener = new GroupedActionListener<>( - wrappedListener.map(progress -> null), - pendingRanges.size() + pendingRanges.size(), + wrappedListener.map(progress -> null) ); pendingRanges.forEach( r -> r.completionListener.addListener(groupedActionListener, Math.min(r.completionListener.end, range.end())) diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/store/SearchableSnapshotDirectory.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/store/SearchableSnapshotDirectory.java index 869622844c03..3dfe1ed53df2 100644 --- a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/store/SearchableSnapshotDirectory.java +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/store/SearchableSnapshotDirectory.java @@ -484,10 +484,13 @@ private void prewarmCache(ActionListener listener) { final BlockingQueue, CheckedRunnable>> queue = new LinkedBlockingQueue<>(); final Executor executor = prewarmExecutor(); - final GroupedActionListener completionListener = new GroupedActionListener<>(ActionListener.wrap(voids -> { - recoveryState.setPreWarmComplete(); - listener.onResponse(null); - }, listener::onFailure), snapshot().totalFileCount()); + final GroupedActionListener completionListener = new GroupedActionListener<>( + snapshot().totalFileCount(), + ActionListener.wrap(voids -> { + recoveryState.setPreWarmComplete(); + listener.onResponse(null); + }, listener::onFailure) + ); for (BlobStoreIndexShardSnapshot.FileInfo file : snapshot().indexFiles()) { boolean hashEqualsContents = file.metadata().hashEqualsContents(); @@ -517,7 +520,7 @@ private void prewarmCache(ActionListener listener) { IOUtils.closeWhileHandlingException(input); }); - final GroupedActionListener partsListener = new GroupedActionListener<>(fileCompletionListener, numberOfParts); + final GroupedActionListener partsListener = new GroupedActionListener<>(numberOfParts, fileCompletionListener); submitted = true; for (int p = 0; p < numberOfParts; p++) { final int part = p; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java index 3585fe01d70c..866a3bf1bad4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java @@ -120,6 +120,7 @@ protected void doRun() { } final String httpsCaFingerprint = fingerprint; GroupedActionListener> groupedActionListener = new GroupedActionListener<>( + 3, ActionListener.wrap(results -> { final Map allResultsMap = new HashMap<>(); for (Map result : results) { @@ -135,8 +136,7 @@ protected void doRun() { httpsCaFingerprint, console ); - }, e -> LOGGER.error("Unexpected exception during security auto-configuration", e)), - 3 + }, e -> LOGGER.error("Unexpected exception during security auto-configuration", e)) ); // we only generate the elastic user password if the node has been auto-configured in a specific way, such that the // first time a node starts it will form a cluster by itself and can hold the .security index (which we assume diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/rolemapping/ReservedRoleMappingAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/rolemapping/ReservedRoleMappingAction.java index 74e0cc081ace..86b28056a22f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/rolemapping/ReservedRoleMappingAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/rolemapping/ReservedRoleMappingAction.java @@ -104,7 +104,7 @@ private void nonStateTransform( return; } - GroupedActionListener taskListener = new GroupedActionListener<>(new ActionListener<>() { + GroupedActionListener taskListener = new GroupedActionListener<>(tasksCount, new ActionListener<>() { @Override public void onResponse(Collection booleans) { listener.onResponse(new NonStateTransformResult(ReservedRoleMappingAction.NAME, Collections.unmodifiableSet(entities))); @@ -114,7 +114,7 @@ public void onResponse(Collection booleans) { public void onFailure(Exception e) { listener.onFailure(e); } - }, tasksCount); + }); for (var request : requests) { roleMappingStore.putRoleMapping(request, taskListener); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionAction.java index 6806accc046b..d92eaf115c87 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionAction.java @@ -113,8 +113,8 @@ private void findAndInvalidateTokens(SamlRealm realm, SamlLogoutRequestHandler.R listener.onResponse(0); } else { GroupedActionListener groupedListener = new GroupedActionListener<>( - ActionListener.wrap(collection -> listener.onResponse(collection.size()), listener::onFailure), - tokens.size() + tokens.size(), + ActionListener.wrap(collection -> listener.onResponse(collection.size()), listener::onFailure) ); tokens.forEach(tuple -> invalidateTokenPair(tuple, groupedListener)); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersAction.java index ea646fcb68ce..2cfb1597f3b6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersAction.java @@ -103,7 +103,7 @@ protected void doExecute(Task task, final GetUsersRequest request, final ActionL } }, listener::onFailure); - final GroupedActionListener> groupListener = new GroupedActionListener<>(sendingListener, 2); + final GroupedActionListener> groupListener = new GroupedActionListener<>(2, sendingListener); // We have two sources for the users object, the reservedRealm and the usersStore, we query both at the same time with a // GroupedActionListener if (realmLookup.isEmpty()) { @@ -117,7 +117,7 @@ protected void doExecute(Task task, final GetUsersRequest request, final ActionL } else { // nested group listener action here - for each of the users we got and fetch it concurrently - once we are done we notify // the "global" group listener. - GroupedActionListener realmGroupListener = new GroupedActionListener<>(groupListener, realmLookup.size()); + GroupedActionListener realmGroupListener = new GroupedActionListener<>(realmLookup.size(), groupListener); for (String user : realmLookup) { reservedRealm.lookupUser(user, realmGroupListener); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/mapper/CompositeRoleMapper.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/mapper/CompositeRoleMapper.java index d8f0e574e94c..260550f9ff08 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/mapper/CompositeRoleMapper.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/mapper/CompositeRoleMapper.java @@ -44,11 +44,11 @@ private CompositeRoleMapper(UserRoleMapper... delegates) { @Override public void resolveRoles(UserData user, ActionListener> listener) { GroupedActionListener> groupListener = new GroupedActionListener<>( + delegates.size(), ActionListener.wrap( composite -> listener.onResponse(composite.stream().flatMap(Set::stream).collect(Collectors.toSet())), listener::onFailure - ), - delegates.size() + ) ); this.delegates.forEach(mapper -> mapper.resolveRoles(user, groupListener)); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java index 544809a520c0..7975badddd70 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java @@ -829,7 +829,7 @@ private void authorizeBulkItems( listener::onFailure ); final ActionListener> groupedActionListener = wrapPreservingContext( - new GroupedActionListener<>(bulkAuthzListener, actionToIndicesMap.size()), + new GroupedActionListener<>(actionToIndicesMap.size(), bulkAuthzListener), threadContext ); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index 9f4b80fc26ee..dc595935d8b9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -376,8 +376,8 @@ public void getRoleDescriptorsList(Subject subject, ActionListener { final List roleReferences = subject.getRoleReferenceIntersection(anonymousUser).getRoleReferences(); final GroupedActionListener> groupedActionListener = new GroupedActionListener<>( - listener, - roleReferences.size() + roleReferences.size(), + listener ); roleReferences.forEach(roleReference -> { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStore.java index ba2f78121550..f97bdc926f8b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/NativePrivilegeStore.java @@ -334,6 +334,7 @@ public void putPrivileges( ) { securityIndexManager.prepareIndexIfNeededThenExecute(listener::onFailure, () -> { ActionListener groupListener = new GroupedActionListener<>( + privileges.size(), ActionListener.wrap((Collection responses) -> { final Map> createdNames = responses.stream() .filter(r -> r.getResult() == DocWriteResponse.Result.CREATED) @@ -345,8 +346,7 @@ public void putPrivileges( privileges.stream().map(ApplicationPrivilegeDescriptor::getApplication).collect(Collectors.toUnmodifiableSet()), createdNames ); - }, listener::onFailure), - privileges.size() + }, listener::onFailure) ); for (ApplicationPrivilegeDescriptor privilege : privileges) { innerPutPrivilege(privilege, refreshPolicy, groupListener); @@ -392,14 +392,14 @@ public void deletePrivileges( listener.onFailure(frozenSecurityIndex.getUnavailableReason()); } else { securityIndexManager.checkIndexVersionThenExecute(listener::onFailure, () -> { - ActionListener groupListener = new GroupedActionListener<>(ActionListener.wrap(responses -> { + ActionListener groupListener = new GroupedActionListener<>(names.size(), ActionListener.wrap(responses -> { final Map> deletedNames = responses.stream() .filter(r -> r.getResult() == DocWriteResponse.Result.DELETED) .map(r -> r.getId()) .map(NativePrivilegeStore::nameFromDocId) .collect(TUPLES_TO_MAP); clearCaches(listener, Collections.singleton(application), deletedNames); - }, listener::onFailure), names.size()); + }, listener::onFailure)); for (String name : names) { ClientHelper.executeAsyncWithOrigin( client.threadPool().getThreadContext(), diff --git a/x-pack/plugin/snapshot-repo-test-kit/src/main/java/org/elasticsearch/repositories/blobstore/testkit/BlobAnalyzeAction.java b/x-pack/plugin/snapshot-repo-test-kit/src/main/java/org/elasticsearch/repositories/blobstore/testkit/BlobAnalyzeAction.java index 38726deb2b23..0046f0591907 100644 --- a/x-pack/plugin/snapshot-repo-test-kit/src/main/java/org/elasticsearch/repositories/blobstore/testkit/BlobAnalyzeAction.java +++ b/x-pack/plugin/snapshot-repo-test-kit/src/main/java/org/elasticsearch/repositories/blobstore/testkit/BlobAnalyzeAction.java @@ -254,8 +254,8 @@ static class BlobAnalysis { final StepListener> readsCompleteStep = new StepListener<>(); readNodesListener = new GroupedActionListener<>( - new ThreadedActionListener<>(logger, transportService.getThreadPool(), ThreadPool.Names.SNAPSHOT, readsCompleteStep, false), - earlyReadNodes.size() + readNodes.size() + earlyReadNodes.size() + readNodes.size(), + new ThreadedActionListener<>(logger, transportService.getThreadPool(), ThreadPool.Names.SNAPSHOT, readsCompleteStep, false) ); // The order is important in this chain: if writing fails then we may never even start all the reads, and we want to cancel diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportGetCheckpointAction.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportGetCheckpointAction.java index 579a76626d16..538779ea7659 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportGetCheckpointAction.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportGetCheckpointAction.java @@ -143,6 +143,7 @@ protected AsyncGetCheckpointsFromNodesAction( public void start() { GroupedActionListener groupedListener = new GroupedActionListener<>( + nodesAndShards.size(), ActionListener.wrap(responses -> { // the final list should be ordered by key Map checkpointsByIndexReduced = new TreeMap<>(); @@ -162,8 +163,7 @@ public void start() { } listener.onResponse(new Response(checkpointsByIndexReduced)); - }, listener::onFailure), - nodesAndShards.size() + }, listener::onFailure) ); for (Entry> oneNodeAndItsShards : nodesAndShards.entrySet()) { diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStopTransformAction.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStopTransformAction.java index a20fb4dcbe08..13301381491e 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStopTransformAction.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStopTransformAction.java @@ -483,8 +483,8 @@ private ActionListener cancelTransformTasksWithNoAssignment( ) { final ActionListener doExecuteListener = ActionListener.wrap(response -> { GroupedActionListener> groupedListener = new GroupedActionListener<>( - ActionListener.wrap(r -> { finalListener.onResponse(response); }, finalListener::onFailure), - transformNodeAssignments.getWaitingForAssignment().size() + transformNodeAssignments.getWaitingForAssignment().size(), + ActionListener.wrap(r -> { finalListener.onResponse(response); }, finalListener::onFailure) ); for (String unassignedTaskId : transformNodeAssignments.getWaitingForAssignment()) { @@ -493,8 +493,8 @@ private ActionListener cancelTransformTasksWithNoAssignment( }, e -> { GroupedActionListener> groupedListener = new GroupedActionListener<>( - ActionListener.wrap(r -> { finalListener.onFailure(e); }, finalListener::onFailure), - transformNodeAssignments.getWaitingForAssignment().size() + transformNodeAssignments.getWaitingForAssignment().size(), + ActionListener.wrap(r -> { finalListener.onFailure(e); }, finalListener::onFailure) ); for (String unassignedTaskId : transformNodeAssignments.getWaitingForAssignment()) { diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/checkpoint/DefaultCheckpointProvider.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/checkpoint/DefaultCheckpointProvider.java index 10321efbce32..0ba57764a7af 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/checkpoint/DefaultCheckpointProvider.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/checkpoint/DefaultCheckpointProvider.java @@ -118,7 +118,7 @@ protected void getIndexCheckpoints(ActionListener> listener) ); }, listener::onFailure); - groupedListener = new GroupedActionListener<>(mergeMapsListener, resolvedIndexes.numClusters()); + groupedListener = new GroupedActionListener<>(resolvedIndexes.numClusters(), mergeMapsListener); } if (resolvedIndexes.getLocalIndices().isEmpty() == false) { From 20d947fd6a0c795969c2ae29e26d39e73130339a Mon Sep 17 00:00:00 2001 From: Iraklis Psaroudakis Date: Mon, 12 Dec 2022 10:38:38 +0200 Subject: [PATCH 217/919] Secure settings from yml in Stateless (#91925) Primarily for repository settings. --- docs/changelog/91925.yaml | 5 ++ .../common/settings/ClusterSettings.java | 3 +- .../settings/StatelessSecureSettings.java | 84 +++++++++++++++++++ .../org/elasticsearch/env/Environment.java | 9 +- .../common/settings/SettingsTests.java | 39 +++++++++ 5 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 docs/changelog/91925.yaml create mode 100644 server/src/main/java/org/elasticsearch/common/settings/StatelessSecureSettings.java diff --git a/docs/changelog/91925.yaml b/docs/changelog/91925.yaml new file mode 100644 index 000000000000..731ce402d98f --- /dev/null +++ b/docs/changelog/91925.yaml @@ -0,0 +1,5 @@ +pr: 91925 +summary: Secure settings that can fall back to yml in Stateless +area: Distributed +type: feature +issues: [] diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index 53eebac01337..d47869ae65ed 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -541,7 +541,8 @@ public void apply(Settings value, Settings current, Settings previous) { SimulatePipelineTransportAction.INGEST_NODE_TRANSPORT_ACTION_TIMEOUT, WriteAckDelay.WRITE_ACK_DELAY_INTERVAL, WriteAckDelay.WRITE_ACK_DELAY_RANDOMNESS_BOUND, - TcpTransport.isUntrustedRemoteClusterEnabled() ? RemoteClusterService.REMOTE_CLUSTER_AUTHORIZATION : null + TcpTransport.isUntrustedRemoteClusterEnabled() ? RemoteClusterService.REMOTE_CLUSTER_AUTHORIZATION : null, + StatelessSecureSettings.STATELESS_SECURE_SETTINGS ).filter(Objects::nonNull).collect(Collectors.toSet()); static List> BUILT_IN_SETTING_UPGRADERS = Collections.emptyList(); diff --git a/server/src/main/java/org/elasticsearch/common/settings/StatelessSecureSettings.java b/server/src/main/java/org/elasticsearch/common/settings/StatelessSecureSettings.java new file mode 100644 index 000000000000..7d221bcb5a15 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/settings/StatelessSecureSettings.java @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.common.settings; + +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.hash.MessageDigests; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * An implementation of secure settings from YML settings. + * + * WARNING: this is a temporary class only for Stateless. It applies only to YML settings with a predetermined prefix. + */ +public class StatelessSecureSettings implements SecureSettings { + static final String PREFIX = "insecure."; + static final Setting.AffixSetting STATELESS_SECURE_SETTINGS = Setting.prefixKeySetting( + PREFIX, + (key) -> Setting.simpleString(key, Setting.Property.NodeScope) + ); + + private final Settings settings; + private final Set names; + + private StatelessSecureSettings(Settings settings) { + if (DiscoveryNode.isStateless(settings) == false) { + throw new IllegalArgumentException("StatelessSecureSettings are supported only in stateless"); + } + this.settings = Settings.builder().put(settings, false).build(); + this.names = settings.keySet() + .stream() + .filter(key -> (key.startsWith(PREFIX))) + .map(s -> s.replace(PREFIX, "")) + .collect(Collectors.toUnmodifiableSet()); + } + + public static Settings install(Settings settings) { + StatelessSecureSettings statelessSecureSettings = new StatelessSecureSettings(settings); + return Settings.builder().put(settings, false).setSecureSettings(statelessSecureSettings).build(); + } + + @Override + public boolean isLoaded() { + return true; + } + + @Override + public Set getSettingNames() { + return names; + } + + @Override + public SecureString getString(String setting) throws GeneralSecurityException { + return new SecureString(STATELESS_SECURE_SETTINGS.getConcreteSetting(PREFIX + setting).get(settings).toCharArray()); + } + + @Override + public InputStream getFile(String setting) throws GeneralSecurityException { + return new ByteArrayInputStream( + STATELESS_SECURE_SETTINGS.getConcreteSetting(PREFIX + setting).get(settings).getBytes(StandardCharsets.UTF_8) + ); + } + + @Override + public byte[] getSHA256Digest(String setting) throws GeneralSecurityException { + return MessageDigests.sha256() + .digest(STATELESS_SECURE_SETTINGS.getConcreteSetting(PREFIX + setting).get(settings).getBytes(StandardCharsets.UTF_8)); + } + + @Override + public void close() throws IOException {} +} diff --git a/server/src/main/java/org/elasticsearch/env/Environment.java b/server/src/main/java/org/elasticsearch/env/Environment.java index 90d03fe1ce94..9db7f831a186 100644 --- a/server/src/main/java/org/elasticsearch/env/Environment.java +++ b/server/src/main/java/org/elasticsearch/env/Environment.java @@ -9,9 +9,11 @@ package org.elasticsearch.env; import org.apache.lucene.util.Constants; +import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.StatelessSecureSettings; import org.elasticsearch.core.PathUtils; import org.elasticsearch.core.SuppressForbidden; @@ -156,7 +158,12 @@ public Environment(final Settings settings, final Path configPath) { assert sharedDataFile != null; finalSettings.put(Environment.PATH_SHARED_DATA_SETTING.getKey(), sharedDataFile.toString()); } - this.settings = finalSettings.build(); + + if (DiscoveryNode.isStateless(settings)) { + this.settings = StatelessSecureSettings.install(finalSettings.build()); + } else { + this.settings = finalSettings.build(); + } } /** diff --git a/server/src/test/java/org/elasticsearch/common/settings/SettingsTests.java b/server/src/test/java/org/elasticsearch/common/settings/SettingsTests.java index 2cc11e68f9c5..a415d3b8c2ef 100644 --- a/server/src/test/java/org/elasticsearch/common/settings/SettingsTests.java +++ b/server/src/test/java/org/elasticsearch/common/settings/SettingsTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.Version; import org.elasticsearch.cluster.Diff; +import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.BytesStreamOutput; @@ -26,6 +27,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -485,6 +487,43 @@ public void testSecureSettingIllegalName() { assertTrue(e.getMessage().contains("does not match the allowed setting name pattern")); } + public void testStatelessSecureSettingsWithoutStateless() { + Setting setting = SecureSetting.secureString(StatelessSecureSettings.PREFIX + "key", null); + + final Settings settings = Settings.builder() + .put(DiscoveryNode.STATELESS_ENABLED_SETTING_NAME, false) + .put(setting.getKey(), "yaml.value") + .build(); + + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> StatelessSecureSettings.install(settings)); + assertTrue(e.getMessage().contains("supported only in stateless")); + } + + public void testStatelessSecureSettings() throws Exception { + boolean testFileSettingInsteadOfStringSetting = randomBoolean(); + + Setting yamlSetting = Setting.simpleString(StatelessSecureSettings.PREFIX + "stateless.key"); + Setting secureSetting = testFileSettingInsteadOfStringSetting + ? SecureSetting.secureFile("stateless.key", null) + : SecureSetting.secureString("stateless.key", null); + + final Settings settings = Settings.builder() + .put(DiscoveryNode.STATELESS_ENABLED_SETTING_NAME, true) + .put(yamlSetting.getKey(), "stateless.yaml.value") + .build(); + + Settings newSettings = StatelessSecureSettings.install(settings); + assertTrue(secureSetting.exists(newSettings)); + assertEquals( + "stateless.yaml.value", + testFileSettingInsteadOfStringSetting + ? new String(((InputStream) secureSetting.get(newSettings)).readAllBytes(), StandardCharsets.UTF_8) + : secureSetting.get(newSettings).toString() + ); + assertTrue(yamlSetting.exists(newSettings)); + assertEquals("stateless.yaml.value", yamlSetting.get(newSettings).toString()); + } + public void testGetAsArrayFailsOnDuplicates() { final IllegalStateException e = expectThrows( IllegalStateException.class, From 7d01d768c2cd8cfef2af8d36dab3f57fb6684847 Mon Sep 17 00:00:00 2001 From: Abdon Pijpelink Date: Mon, 12 Dec 2022 09:43:46 +0100 Subject: [PATCH 218/919] [DOCS] Warn about calling vector functions repeatedly (#91864) * [DOCS] Add script score vector function clarification * [DOCS] Warn about calling vector functions repeatedly --- .../query-dsl/script-score-query.asciidoc | 7 ++- .../vectors/vector-functions.asciidoc | 47 ++++++++++++++----- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/docs/reference/query-dsl/script-score-query.asciidoc b/docs/reference/query-dsl/script-score-query.asciidoc index b2843b92309e..08522708554f 100644 --- a/docs/reference/query-dsl/script-score-query.asciidoc +++ b/docs/reference/query-dsl/script-score-query.asciidoc @@ -145,7 +145,6 @@ A good default choice might be to use the `_seq_no` field, whose only drawback is that scores will change if the document is updated since update operations also update the value of the `_seq_no` field. - [[decay-functions-numeric-fields]] ====== Decay functions for numeric fields You can read more about decay functions @@ -333,13 +332,13 @@ through a script: [[decay-functions]] ====== `decay` functions -The `script_score` query has equivalent <> -that can be used in script. +The `script_score` query has equivalent <> that can be used in scripts. include::{es-repo-dir}/vectors/vector-functions.asciidoc[] [[score-explanation]] -====== Explain request +===== Explain request Using an <> provides an explanation of how the parts of a score were computed. The `script_score` query can add its own explanation by setting the `explanation` parameter: [source,console] diff --git a/docs/reference/vectors/vector-functions.asciidoc b/docs/reference/vectors/vector-functions.asciidoc index a492d8ad6ff2..e0ed85189c97 100644 --- a/docs/reference/vectors/vector-functions.asciidoc +++ b/docs/reference/vectors/vector-functions.asciidoc @@ -9,13 +9,20 @@ to limit the number of matched documents with a `query` parameter. This is the list of available vector functions and vector access methods: -1. `cosineSimilarity` – calculates cosine similarity -2. `dotProduct` – calculates dot product -3. `l1norm` – calculates L^1^ distance -4. `l2norm` - calculates L^2^ distance -5. `doc[].vectorValue` – returns a vector's value as an array of floats -6. `doc[].magnitude` – returns a vector's magnitude +1. <> – calculates cosine similarity +2. <> – calculates dot product +3. <> – calculates L^1^ distance +4. <> - calculates L^2^ distance +5. <].vectorValue`>> – returns a vector's value as an array of floats +6. <].magnitude`>> – returns a vector's magnitude +NOTE: The recommended way to access dense vectors is through the +`cosineSimilarity`, `dotProduct`, `l1norm` or `l2norm` functions. Please note +however, that you should call these functions only once per script. For example, +don’t use these functions in a loop to calculate the similarity between a +document vector and multiple other vectors. If you need that functionality, +reimplement these functions yourself by +<>. Let's create an index with a `dense_vector` mapping and index a couple of documents into it. @@ -54,6 +61,9 @@ POST my-index-000001/_refresh -------------------------------------------------- // TESTSETUP +[[vector-functions-cosine]] +====== Cosine similarity + The `cosineSimilarity` function calculates the measure of cosine similarity between a given query vector and document vectors. @@ -90,6 +100,9 @@ GET my-index-000001/_search NOTE: If a document's dense vector field has a number of dimensions different from the query's vector, an error will be thrown. +[[vector-functions-dot-product]] +====== Dot product + The `dotProduct` function calculates the measure of dot product between a given query vector and document vectors. @@ -124,6 +137,9 @@ GET my-index-000001/_search <1> Using the standard sigmoid function prevents scores from being negative. +[[vector-functions-l1]] +====== L^1^ distance (Manhattan distance) + The `l1norm` function calculates L^1^ distance (Manhattan distance) between a given query vector and document vectors. @@ -163,6 +179,9 @@ we reversed the output from `l1norm` and `l2norm`. Also, to avoid division by 0 when a document vector matches the query exactly, we added `1` in the denominator. +[[vector-functions-l2]] +====== L^2^ distance (Euclidean distance) + The `l2norm` function calculates L^2^ distance (Euclidean distance) between a given query vector and document vectors. @@ -193,10 +212,13 @@ GET my-index-000001/_search } -------------------------------------------------- -NOTE: If a document doesn't have a value for a vector field on which -a vector function is executed, an error will be thrown. +[[vector-functions-missing-values]] +====== Checking for missing values + +If a document doesn't have a value for a vector field on which a vector function +is executed, an error will be thrown. -You can check if a document has a value for the field `my_vector` by +You can check if a document has a value for the field `my_vector` with `doc['my_vector'].size() == 0`. Your overall script can look like this: [source,js] @@ -205,9 +227,10 @@ You can check if a document has a value for the field `my_vector` by -------------------------------------------------- // NOTCONSOLE -The recommended way to access dense vectors is through `cosineSimilarity`, -`dotProduct`, `l1norm` or `l2norm` functions. But for custom use cases, -you can access dense vectors's values directly through the following functions: +[[vector-functions-accessing-vectors]] +====== Accessing vectors directly + +You can access vector values directly through the following functions: - `doc[].vectorValue` – returns a vector's value as an array of floats From fda8ae2e4188973fb46a371c635fe36fc42c3c6e Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Mon, 12 Dec 2022 09:45:02 +0100 Subject: [PATCH 219/919] Enable bloom filter for _id field in tsdb indices. (#92115) --- docs/changelog/92115.yaml | 5 ++ .../index/codec/PerFieldMapperCodec.java | 20 +++-- .../index/codec/PerFieldMapperCodecTests.java | 80 +++++++++++++++++++ 3 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 docs/changelog/92115.yaml create mode 100644 server/src/test/java/org/elasticsearch/index/codec/PerFieldMapperCodecTests.java diff --git a/docs/changelog/92115.yaml b/docs/changelog/92115.yaml new file mode 100644 index 000000000000..d8f9defebb67 --- /dev/null +++ b/docs/changelog/92115.yaml @@ -0,0 +1,5 @@ +pr: 92115 +summary: Enable bloom filter for `_id` field in tsdb indices +area: TSDB +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/index/codec/PerFieldMapperCodec.java b/server/src/main/java/org/elasticsearch/index/codec/PerFieldMapperCodec.java index 268ba302033f..f6c9794fc8ad 100644 --- a/server/src/main/java/org/elasticsearch/index/codec/PerFieldMapperCodec.java +++ b/server/src/main/java/org/elasticsearch/index/codec/PerFieldMapperCodec.java @@ -16,6 +16,7 @@ import org.apache.lucene.codecs.lucene94.Lucene94Codec; import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.index.IndexMode; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.codec.bloomfilter.ES85BloomFilterPostingsFormat; import org.elasticsearch.index.mapper.IdFieldMapper; @@ -32,8 +33,8 @@ * configured for a specific field the default postings or vector format is used. */ public class PerFieldMapperCodec extends Lucene94Codec { - private final MapperService mapperService; + private final MapperService mapperService; private final DocValuesFormat docValuesFormat = new Lucene90DocValuesFormat(); private final ES85BloomFilterPostingsFormat bloomFilterPostingsFormat; @@ -64,10 +65,19 @@ private PostingsFormat internalGetPostingsFormatForField(String field) { return super.getPostingsFormatForField(field); } - private boolean useBloomFilter(String field) { - return IdFieldMapper.NAME.equals(field) - && mapperService.mappingLookup().isDataStreamTimestampFieldEnabled() == false - && IndexSettings.BLOOM_FILTER_ID_FIELD_ENABLED_SETTING.get(mapperService.getIndexSettings().getSettings()); + boolean useBloomFilter(String field) { + IndexSettings indexSettings = mapperService.getIndexSettings(); + if (mapperService.mappingLookup().isDataStreamTimestampFieldEnabled()) { + // In case for time series indices, they _id isn't randomly generated, + // but based on dimension fields and timestamp field, so during indexing + // version/seq_no/term needs to be looked up and having a bloom filter + // can speed this up significantly. + return indexSettings.getMode() == IndexMode.TIME_SERIES + && IdFieldMapper.NAME.equals(field) + && IndexSettings.BLOOM_FILTER_ID_FIELD_ENABLED_SETTING.get(indexSettings.getSettings()); + } else { + return IdFieldMapper.NAME.equals(field) && IndexSettings.BLOOM_FILTER_ID_FIELD_ENABLED_SETTING.get(indexSettings.getSettings()); + } } @Override diff --git a/server/src/test/java/org/elasticsearch/index/codec/PerFieldMapperCodecTests.java b/server/src/test/java/org/elasticsearch/index/codec/PerFieldMapperCodecTests.java new file mode 100644 index 000000000000..75eb5d1abb52 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/codec/PerFieldMapperCodecTests.java @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.index.codec; + +import org.apache.lucene.codecs.lucene94.Lucene94Codec; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.common.compress.CompressedXContent; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.MapperTestUtils; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; + +import static org.hamcrest.Matchers.is; + +public class PerFieldMapperCodecTests extends ESTestCase { + + public void testUseBloomFilter() throws IOException { + PerFieldMapperCodec perFieldMapperCodec = createCodec(false, randomBoolean(), false); + assertThat(perFieldMapperCodec.useBloomFilter("_id"), is(true)); + assertThat(perFieldMapperCodec.useBloomFilter("another_field"), is(false)); + } + + public void testUseBloomFilterWithTimestampFieldEnabled() throws IOException { + PerFieldMapperCodec perFieldMapperCodec = createCodec(true, true, false); + assertThat(perFieldMapperCodec.useBloomFilter("_id"), is(true)); + assertThat(perFieldMapperCodec.useBloomFilter("another_field"), is(false)); + } + + public void testUseBloomFilterWithTimestampFieldEnabled_noTimeSeriesMode() throws IOException { + PerFieldMapperCodec perFieldMapperCodec = createCodec(true, false, false); + assertThat(perFieldMapperCodec.useBloomFilter("_id"), is(false)); + } + + public void testUseBloomFilterWithTimestampFieldEnabled_disableBloomFilter() throws IOException { + PerFieldMapperCodec perFieldMapperCodec = createCodec(true, true, true); + assertThat(perFieldMapperCodec.useBloomFilter("_id"), is(false)); + assertWarnings( + "[index.bloom_filter_for_id_field.enabled] setting was deprecated in Elasticsearch and will be removed in a future release." + ); + } + + private PerFieldMapperCodec createCodec(boolean timestampField, boolean timeSeries, boolean disableBloomFilter) throws IOException { + Settings.Builder settings = Settings.builder(); + if (timeSeries) { + settings.put(IndexSettings.MODE.getKey(), "time_series"); + settings.put(IndexMetadata.INDEX_ROUTING_PATH.getKey(), "field"); + } + if (disableBloomFilter) { + settings.put(IndexSettings.BLOOM_FILTER_ID_FIELD_ENABLED_SETTING.getKey(), false); + } + MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), settings.build(), "test"); + if (timestampField) { + String mapping = """ + { + "_data_stream_timestamp": { + "enabled": true + }, + "properties": { + "@timestamp": { + "type": "date" + } + } + } + """; + mapperService.merge("type", new CompressedXContent(mapping), MapperService.MergeReason.MAPPING_UPDATE); + } + return new PerFieldMapperCodec(Lucene94Codec.Mode.BEST_SPEED, mapperService, BigArrays.NON_RECYCLING_INSTANCE); + } + +} From 36e9c6ce9a546cc2c6ae3cec59b346d57c7a2966 Mon Sep 17 00:00:00 2001 From: Leaf-Lin <39002973+Leaf-Lin@users.noreply.github.com> Date: Mon, 12 Dec 2022 20:06:25 +1100 Subject: [PATCH 220/919] [DOCS] Remove the sentence on deleting document in .tasks (#87310) * Remove the sentence on deleting document in .tasks Before 8.0, one should run`DELETE .tasks/_doc/:` to reclaim the space. As the restriction on system indices are tightened since 8.0, deleting old documents from `.tasks` is no longer possible. See https://github.com/elastic/elasticsearch/issues/77383 This PR only updates the documentation to avoid confusion, the automatic mechanism is still to be implmented. * Removing sentance from update-by-query page. Co-authored-by: Amy Jonsson --- docs/reference/docs/reindex.asciidoc | 2 -- docs/reference/docs/update-by-query.asciidoc | 2 -- 2 files changed, 4 deletions(-) diff --git a/docs/reference/docs/reindex.asciidoc b/docs/reference/docs/reindex.asciidoc index 4927c84fff85..21df11cf2c15 100644 --- a/docs/reference/docs/reindex.asciidoc +++ b/docs/reference/docs/reindex.asciidoc @@ -141,8 +141,6 @@ If the request contains `wait_for_completion=false`, {es} performs some preflight checks, launches the request, and returns a <> you can use to cancel or get the status of the task. {es} creates a record of this task as a document at `_tasks/`. -When you are done with a task, you should delete the task document so -{es} can reclaim the space. [[docs-reindex-from-multiple-sources]] ===== Reindex from multiple sources diff --git a/docs/reference/docs/update-by-query.asciidoc b/docs/reference/docs/update-by-query.asciidoc index cd27ec369d25..e50d8b722c89 100644 --- a/docs/reference/docs/update-by-query.asciidoc +++ b/docs/reference/docs/update-by-query.asciidoc @@ -99,8 +99,6 @@ If the request contains `wait_for_completion=false`, {es} performs some preflight checks, launches the request, and returns a <> you can use to cancel or get the status of the task. {es} creates a record of this task as a document at `.tasks/task/${taskId}`. -When you are done with a task, you should delete the task document so -{es} can reclaim the space. ===== Waiting for active shards From c0e624b3b13269737f8d041d5dc6d6eaef01854a Mon Sep 17 00:00:00 2001 From: amyjtechwriter <61687663+amyjtechwriter@users.noreply.github.com> Date: Mon, 12 Dec 2022 09:18:27 +0000 Subject: [PATCH 221/919] Removing 'or closed' from ignore_unavalible in line with new default behviour. (#92233) --- .../snapshot-restore/apis/create-snapshot-api.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/snapshot-restore/apis/create-snapshot-api.asciidoc b/docs/reference/snapshot-restore/apis/create-snapshot-api.asciidoc index 3242fd2ee80f..649a904786f1 100644 --- a/docs/reference/snapshot-restore/apis/create-snapshot-api.asciidoc +++ b/docs/reference/snapshot-restore/apis/create-snapshot-api.asciidoc @@ -97,7 +97,7 @@ Don't expand wildcard patterns. `ignore_unavailable`:: (Optional, Boolean) If `false`, the snapshot fails if any data stream or index in `indices` is -missing or closed. If `true`, the snapshot ignores missing or closed data +missing. If `true`, the snapshot ignores missing data streams and indices. Defaults to `false`. `include_global_state`:: From 48c1447dd8990d26cb75ab31a79217cb62f83cfa Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 12 Dec 2022 09:23:15 +0000 Subject: [PATCH 222/919] Add o.e.a.a.cluster.coordination to exports from server (#92059) Fixes the red squigglies in IntelliJ. --- server/src/main/java/module-info.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java index ca9f2c078871..0ced8611f293 100644 --- a/server/src/main/java/module-info.java +++ b/server/src/main/java/module-info.java @@ -53,6 +53,7 @@ exports org.elasticsearch.action; exports org.elasticsearch.action.admin.cluster.allocation; exports org.elasticsearch.action.admin.cluster.configuration; + exports org.elasticsearch.action.admin.cluster.coordination; exports org.elasticsearch.action.admin.cluster.desirednodes; exports org.elasticsearch.action.admin.cluster.health; exports org.elasticsearch.action.admin.cluster.migration; @@ -281,7 +282,6 @@ exports org.elasticsearch.monitor.os; exports org.elasticsearch.monitor.process; exports org.elasticsearch.node; - exports org.elasticsearch.reservedstate; exports org.elasticsearch.persistent; exports org.elasticsearch.persistent.decider; exports org.elasticsearch.plugins; @@ -290,6 +290,7 @@ exports org.elasticsearch.repositories; exports org.elasticsearch.repositories.blobstore; exports org.elasticsearch.repositories.fs; + exports org.elasticsearch.reservedstate; exports org.elasticsearch.rest; exports org.elasticsearch.rest.action; exports org.elasticsearch.rest.action.admin.cluster; From b0af79dabce38c86fadb222c7980e3e234d70403 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 12 Dec 2022 09:25:03 +0000 Subject: [PATCH 223/919] Add known-issue docs for index/alias name clash bug (#92262) Relates #91456 Relates #91887 --- docs/reference/release-notes/8.5.0.asciidoc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/reference/release-notes/8.5.0.asciidoc b/docs/reference/release-notes/8.5.0.asciidoc index 1dbb44ca5f1d..9f02725e9dfa 100644 --- a/docs/reference/release-notes/8.5.0.asciidoc +++ b/docs/reference/release-notes/8.5.0.asciidoc @@ -328,3 +328,10 @@ Packaging:: * When using date range search with format that does not have all date fields (missing month or day) an incorrectly parsed date could be used. The workaround is to use date pattern with all date fields (year, month, day) (issue: {es-issue}90187[#90187]) + +* It is possible to inadvertently create an alias with the same name as an +index in version 8.5.0. This action leaves the cluster in an invalid state in +which several features will not work correctly, and it may not even be possible +to restart nodes while in this state. Upgrade to 8.5.1 as soon as possible to +avoid the risk of this occurring ({es-pull}91456[#91456]). If your cluster is +affected by this issue, upgrade to 8.5.3 to repair it ({es-pull}91887[#91887]). From 6179e9ffd8eff989267d35fe5adc2717b158ca1f Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 12 Dec 2022 22:49:40 +1100 Subject: [PATCH 224/919] JWT realm - Initial support for access tokens (#91781) This PR adds initial support for access tokens to JWT realm. Unlike ID tokens, access tokens are not well defined and rather arbitrary. Therefore the JWT realm needs to enforce a set of criteria for an access token to be supported: * An access token must be a JWT * An access token must be identifiable by (contain) three coordinates: issuer, subject and audiences * The issuer corresponds to a JWT's `iss` claim. The subject defaults to the `sub` claim but can fallback to another claim if configured. Similarly, audiences default to the `aud` claim but can fallback to another claim. * An access token must have both `iat` and `exp` claims of the same semantics as defined by ID token spec An access token meets all above requirements is processed further to run through additional checks similar to ID tokens, such as `allowed_issuer`, `allowed_audiences`, signature validation etc. A fallback claim takes effect only when the main one does _not_ exist. When it is effective, it is honored for attribute mapping as well. For example, if we have `fallback_claims.sub: email` and `claims.principal: sub`, the principal attribute will be mapped from the `email` claim if `sub` does not exist. Since we now have dedicated support for access tokens which allow fallback claims. The ID token support can be more strict and hence it now always mandates the `sub` claim (per ID token spec). NOTE: Fallback of the `aud` claim does not fully work in this PR because `JwtAuthenticationToken` currently [mandates](https://github.com/elastic/elasticsearch/blob/cf0b1af418cad79dfa9c8193c3be51e3c644d8d9/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticationToken.java#L74) it. This needs to be fixed. The plan is to remove the entire JwtRealmServices and let each JwtRealm directly extract the token. It will be done in a separate PR to keep the size under control. --- docs/changelog/91781.yaml | 5 + .../security/authc/jwt/JwtRealmSettings.java | 101 +++++++++++ .../plugin/security/qa/jwt-realm/build.gradle | 22 ++- .../xpack/security/authc/jwt/JwtRestIT.java | 73 +++++--- .../security/authc/jwt/FallbackableClaim.java | 82 +++++++++ .../authc/jwt/JwtAlgorithmValidator.java | 17 +- .../security/authc/jwt/JwtAuthenticator.java | 43 ++++- .../authc/jwt/JwtDateClaimValidator.java | 16 +- .../xpack/security/authc/jwt/JwtRealm.java | 20 ++- .../authc/jwt/JwtStringClaimValidator.java | 56 +++--- .../security/authc/jwt/JwtTypeValidator.java | 5 +- .../security/authc/support/ClaimParser.java | 49 +++-- .../authc/jwt/FallbackableClaimTests.java | 53 ++++++ .../authc/jwt/JwtAlgorithmValidatorTests.java | 5 +- .../JwtAuthenticatorAccessTokenTypeTests.java | 58 ++++++ .../jwt/JwtAuthenticatorIdTokenTypeTests.java | 42 +++++ .../authc/jwt/JwtAuthenticatorTests.java | 72 ++++++-- .../authc/jwt/JwtDateClaimValidatorTests.java | 17 +- ...RealmAuthenticateAccessTokenTypeTests.java | 167 ++++++++++++++++++ .../authc/jwt/JwtRealmAuthenticateTests.java | 114 ++++++------ .../authc/jwt/JwtRealmGenerateTests.java | 10 +- .../security/authc/jwt/JwtRealmInspector.java | 4 + .../authc/jwt/JwtRealmSettingsTests.java | 107 +++++++++++ .../security/authc/jwt/JwtRealmTestCase.java | 49 ++--- .../jwt/JwtStringClaimValidatorTests.java | 135 ++++++++++---- .../authc/jwt/JwtTypeValidatorTests.java | 5 +- .../authc/oidc/OpenIdConnectRealmTests.java | 4 +- 27 files changed, 1074 insertions(+), 257 deletions(-) create mode 100644 docs/changelog/91781.yaml create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/FallbackableClaim.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/FallbackableClaimTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorAccessTokenTypeTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorIdTokenTypeTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateAccessTokenTypeTests.java diff --git a/docs/changelog/91781.yaml b/docs/changelog/91781.yaml new file mode 100644 index 000000000000..7513ec86baf4 --- /dev/null +++ b/docs/changelog/91781.yaml @@ -0,0 +1,5 @@ +pr: 91781 +summary: JWT realm - Initial support for access tokens +area: Authentication +type: enhancement +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/jwt/JwtRealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/jwt/JwtRealmSettings.java index 42d84d8ce936..0b57987b265a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/jwt/JwtRealmSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/jwt/JwtRealmSettings.java @@ -19,7 +19,9 @@ import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; +import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -155,6 +157,9 @@ private static Set> getNonSecureSettings() { // JWT End-user settings set.addAll( List.of( + ALLOWED_SUBJECTS, + FALLBACK_SUB_CLAIM, + FALLBACK_AUD_CLAIM, CLAIMS_PRINCIPAL.getClaim(), CLAIMS_PRINCIPAL.getPattern(), CLAIMS_GROUPS.getClaim(), @@ -245,6 +250,58 @@ private static Set> getSecureSettings() { // JWT end-user settings + public static final Setting.AffixSetting> ALLOWED_SUBJECTS = Setting.affixKeySetting( + RealmSettings.realmSettingPrefix(TYPE), + "allowed_subjects", + key -> Setting.stringListSetting(key, values -> verifyNonNullNotEmpty(key, values, null), Setting.Property.NodeScope) + ); + + // Registered claim names from the JWT spec https://www.rfc-editor.org/rfc/rfc7519#section-4.1. + // Being registered means they have prescribed meanings when they present in a JWT. + public static final List REGISTERED_CLAIM_NAMES = List.of("iss", "sub", "aud", "exp", "nbf", "iat", "jti"); + + public static final Setting.AffixSetting FALLBACK_SUB_CLAIM = Setting.affixKeySetting( + RealmSettings.realmSettingPrefix(TYPE), + "fallback_claims.sub", + key -> Setting.simpleString(key, "sub", new Setting.Validator<>() { + @Override + public void validate(String value) {} + + @Override + public void validate(String value, Map, Object> settings, boolean isPresent) { + validateFallbackClaimSetting(FALLBACK_SUB_CLAIM, key, value, settings, isPresent); + } + + @Override + public Iterator> settings() { + final String namespace = FALLBACK_SUB_CLAIM.getNamespace(FALLBACK_SUB_CLAIM.getConcreteSetting(key)); + final List> settings = List.of(TOKEN_TYPE.getConcreteSettingForNamespace(namespace)); + return settings.iterator(); + } + }, Setting.Property.NodeScope) + ); + + public static final Setting.AffixSetting FALLBACK_AUD_CLAIM = Setting.affixKeySetting( + RealmSettings.realmSettingPrefix(TYPE), + "fallback_claims.aud", + key -> Setting.simpleString(key, "aud", new Setting.Validator<>() { + @Override + public void validate(String value) {} + + @Override + public void validate(String value, Map, Object> settings, boolean isPresent) { + validateFallbackClaimSetting(FALLBACK_AUD_CLAIM, key, value, settings, isPresent); + } + + @Override + public Iterator> settings() { + final String namespace = FALLBACK_AUD_CLAIM.getNamespace(FALLBACK_AUD_CLAIM.getConcreteSetting(key)); + final List> settings = List.of(TOKEN_TYPE.getConcreteSettingForNamespace(namespace)); + return settings.iterator(); + } + }, Setting.Property.NodeScope) + ); + // Note: ClaimSetting is a wrapper for two individual settings: getClaim(), getPattern() public static final ClaimSetting CLAIMS_PRINCIPAL = new ClaimSetting(TYPE, "principal"); public static final ClaimSetting CLAIMS_GROUPS = new ClaimSetting(TYPE, "groups"); @@ -357,4 +414,48 @@ private static void verifyNonNullNotEmpty(final String key, final List v } } + private static void validateFallbackClaimSetting( + Setting.AffixSetting setting, + String key, + String value, + Map, Object> settings, + boolean isPresent + ) { + if (false == isPresent) { + return; + } + final String namespace = setting.getNamespace(setting.getConcreteSetting(key)); + final TokenType tokenType = (TokenType) settings.get(TOKEN_TYPE.getConcreteSettingForNamespace(namespace)); + if (tokenType == TokenType.ID_TOKEN) { + throw new IllegalArgumentException( + Strings.format( + "fallback claim setting [%s] is not allowed when JWT realm [%s] is [%s] type", + key, + namespace, + JwtRealmSettings.TokenType.ID_TOKEN.value() + ) + ); + } + verifyFallbackClaimName(key, value); + } + + private static void verifyFallbackClaimName(String key, String fallbackClaimName) { + final String claimName = key.substring(key.lastIndexOf(".") + 1); + verifyNonNullNotEmpty(key, fallbackClaimName, null); + if (claimName.equals(fallbackClaimName)) { + return; + } + // Registered claims have prescribed meanings and should not be used for something else. + if (REGISTERED_CLAIM_NAMES.contains(fallbackClaimName)) { + throw new IllegalArgumentException( + Strings.format( + "Invalid fallback claims setting [%s]. Claim [%s] cannot fallback to a registered claim [%s]", + key, + claimName, + fallbackClaimName + ) + ); + } + } + } diff --git a/x-pack/plugin/security/qa/jwt-realm/build.gradle b/x-pack/plugin/security/qa/jwt-realm/build.gradle index 4460be5c45f1..540527d948b5 100644 --- a/x-pack/plugin/security/qa/jwt-realm/build.gradle +++ b/x-pack/plugin/security/qa/jwt-realm/build.gradle @@ -10,7 +10,13 @@ dependencies { javaRestTestImplementation project(":client:rest") } -boolean explicitIdTokenType = BuildParams.random.nextBoolean() +def random = BuildParams.random +boolean explicitIdTokenType = random.nextBoolean() +def serviceSubject = 'service_' + random.nextInt(1, 9) + '@app' + random.nextInt(1, 9) + '.example.com' + +tasks.named("javaRestTest").configure { + systemProperty 'jwt2.service_subject', serviceSubject +} testClusters.matching { it.name == 'javaRestTest' }.configureEach { testDistribution = 'DEFAULT' @@ -64,13 +70,19 @@ testClusters.matching { it.name == 'javaRestTest' }.configureEach { setting 'xpack.security.authc.realms.native.lookup_native.order', '2' setting 'xpack.security.authc.realms.jwt.jwt2.order', '3' - if (explicitIdTokenType) { - setting 'xpack.security.authc.realms.jwt.jwt2.token_type', 'id_token' - } + setting 'xpack.security.authc.realms.jwt.jwt2.token_type', 'access_token' + setting 'xpack.security.authc.realms.jwt.jwt2.fallback_claims.sub', 'email' + setting 'xpack.security.authc.realms.jwt.jwt2.fallback_claims.aud', 'scope' setting 'xpack.security.authc.realms.jwt.jwt2.allowed_issuer', 'my-issuer' + setting 'xpack.security.authc.realms.jwt.jwt2.allowed_subjects', serviceSubject setting 'xpack.security.authc.realms.jwt.jwt2.allowed_audiences', 'es01,es02,es03' setting 'xpack.security.authc.realms.jwt.jwt2.allowed_signature_algorithms', 'HS256,HS384' - setting 'xpack.security.authc.realms.jwt.jwt2.claims.principal', 'email' + // Both email or sub works because of fallback + if (random.nextBoolean()) { + setting 'xpack.security.authc.realms.jwt.jwt2.claims.principal', 'email' + } else { + setting 'xpack.security.authc.realms.jwt.jwt2.claims.principal', 'sub' + } setting 'xpack.security.authc.realms.jwt.jwt2.claim_patterns.principal', '^(.*)@[^.]*[.]example[.]com$' setting 'xpack.security.authc.realms.jwt.jwt2.authorization_realms', 'lookup_native' setting 'xpack.security.authc.realms.jwt.jwt2.client_authentication.type', 'shared_secret' diff --git a/x-pack/plugin/security/qa/jwt-realm/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/jwt/JwtRestIT.java b/x-pack/plugin/security/qa/jwt-realm/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/jwt/JwtRestIT.java index eb3567d45243..a62741620a4d 100644 --- a/x-pack/plugin/security/qa/jwt-realm/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/jwt/JwtRestIT.java +++ b/x-pack/plugin/security/qa/jwt-realm/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/jwt/JwtRestIT.java @@ -281,10 +281,11 @@ public void testFailureOnNonMatchingRsaSignature() throws Exception { * - uses a shared-secret for client authentication */ public void testAuthenticateWithHmacSignedJWTAndDelegatedAuthorization() throws Exception { - final String principal = randomPrincipal(); + final String principal = System.getProperty("jwt2.service_subject"); + final String username = getUsernameFromPrincipal(principal); final List roles = randomRoles(); final String randomMetadata = randomAlphaOfLengthBetween(6, 18); - createUser(principal, roles, Map.of("test_key", randomMetadata)); + createUser(username, roles, Map.of("test_key", randomMetadata)); try { final SignedJWT jwt = buildAndSignJwtForRealm2(principal); @@ -292,7 +293,7 @@ public void testAuthenticateWithHmacSignedJWTAndDelegatedAuthorization() throws final Map response = client.authenticate(); - assertThat(response.get(User.Fields.USERNAME.getPreferredName()), is(principal)); + assertThat(response.get(User.Fields.USERNAME.getPreferredName()), is(username)); assertThat(assertMap(response, User.Fields.AUTHENTICATION_REALM), hasEntry(User.Fields.REALM_NAME.getPreferredName(), "jwt2")); assertThat(assertList(response, User.Fields.ROLES), Matchers.containsInAnyOrder(roles.toArray(String[]::new))); assertThat(assertMap(response, User.Fields.METADATA), hasEntry("test_key", randomMetadata)); @@ -304,14 +305,15 @@ public void testAuthenticateWithHmacSignedJWTAndDelegatedAuthorization() throws ); assertThat(exception.getResponse(), hasStatusCode(RestStatus.FORBIDDEN)); } finally { - deleteUser(principal); + deleteUser(username); } } public void testFailureOnInvalidHMACSignature() throws Exception { - final String principal = randomPrincipal(); + final String principal = System.getProperty("jwt2.service_subject"); + final String username = getUsernameFromPrincipal(principal); final List roles = randomRoles(); - createUser(principal, roles, Map.of()); + createUser(username, roles, Map.of()); try { final JWTClaimsSet claimsSet = buildJwtForRealm2(principal, Instant.now()); @@ -320,7 +322,7 @@ public void testFailureOnInvalidHMACSignature() throws Exception { // This is the correct HMAC passphrase (from build.gradle) final SignedJWT jwt = signHmacJwt(claimsSet, "test-HMAC/secret passphrase-value"); final TestSecurityClient client = getSecurityClient(jwt, VALID_SHARED_SECRET); - assertThat(client.authenticate(), hasEntry(User.Fields.USERNAME.getPreferredName(), principal)); + assertThat(client.authenticate(), hasEntry(User.Fields.USERNAME.getPreferredName(), username)); } { // This is not the correct HMAC passphrase @@ -331,13 +333,14 @@ public void testFailureOnInvalidHMACSignature() throws Exception { assertThat(exception.getResponse(), hasStatusCode(RestStatus.UNAUTHORIZED)); } } finally { - deleteUser(principal); + deleteUser(username); } } public void testAuthenticationFailureIfDelegatedAuthorizationFails() throws Exception { - final String principal = randomPrincipal(); + final String principal = System.getProperty("jwt2.service_subject"); + final String username = getUsernameFromPrincipal(principal); final SignedJWT jwt = buildAndSignJwtForRealm2(principal); final TestSecurityClient client = getSecurityClient(jwt, VALID_SHARED_SECRET); @@ -345,19 +348,20 @@ public void testAuthenticationFailureIfDelegatedAuthorizationFails() throws Exce final ResponseException exception = expectThrows(ResponseException.class, client::authenticate); assertThat(exception.getResponse(), hasStatusCode(RestStatus.UNAUTHORIZED)); - createUser(principal, List.of(), Map.of()); + createUser(username, List.of(), Map.of()); try { // Now it works - assertThat(client.authenticate(), hasEntry(User.Fields.USERNAME.getPreferredName(), principal)); + assertThat(client.authenticate(), hasEntry(User.Fields.USERNAME.getPreferredName(), username)); } finally { - deleteUser(principal); + deleteUser(username); } } public void testFailureOnInvalidClientAuthentication() throws Exception { - final String principal = randomPrincipal(); + final String principal = System.getProperty("jwt2.service_subject"); + final String username = getUsernameFromPrincipal(principal); final List roles = randomRoles(); - createUser(principal, roles, Map.of()); + createUser(username, roles, Map.of()); try { final SignedJWT jwt = buildAndSignJwtForRealm2(principal); @@ -368,7 +372,7 @@ public void testFailureOnInvalidClientAuthentication() throws Exception { assertThat(exception.getResponse(), hasStatusCode(RestStatus.UNAUTHORIZED)); } finally { - deleteUser(principal); + deleteUser(username); } } @@ -499,14 +503,15 @@ private SignedJWT buildAndSignJwtForRealm2(String principal, Instant issueTime) } private JWTClaimsSet buildJwtForRealm2(String principal, Instant issueTime) { - final String emailAddress = principal + "@" + randomAlphaOfLengthBetween(3, 6) + ".example.com"; // The "jwt2" realm, supports 3 audiences (es01/02/03) final String audience = "es0" + randomIntBetween(1, 3); - final JWTClaimsSet claimsSet = buildJwt( - Map.ofEntries(Map.entry("iss", "my-issuer"), Map.entry("aud", audience), Map.entry("email", emailAddress)), - issueTime, - false - ); + final Map data = new HashMap<>(Map.of("iss", "my-issuer", "aud", audience, "email", principal)); + // scope (fallback audience) is ignored since aud exists + if (randomBoolean()) { + data.put("scope", randomAlphaOfLength(20)); + } + + final JWTClaimsSet claimsSet = buildJwt(data, issueTime, false); return claimsSet; } @@ -645,16 +650,38 @@ private void createUser(String principal, List roles, Map roles, Map metadata) throws IOException { + private void createUser(String principal, SecureString password, List roles, Map metadata) throws IOException { + final String username; + if (principal.contains("@")) { + username = principal.substring(0, principal.indexOf("@")); + } else { + username = principal; + } final String realName = randomAlphaOfLengthBetween(6, 18); final User user = new User(username, roles.toArray(String[]::new), realName, null, metadata, true); getAdminSecurityClient().putUser(user, password); } - private void deleteUser(String username) throws IOException { + private void deleteUser(String principal) throws IOException { + final String username; + if (principal.contains("@")) { + username = principal.substring(0, principal.indexOf("@")); + } else { + username = principal; + } getAdminSecurityClient().deleteUser(username); } + private String getUsernameFromPrincipal(String principal) { + final String username; + if (principal.contains("@")) { + username = principal.substring(0, principal.indexOf("@")); + } else { + username = principal; + } + return username; + } + private String createRoleMapping(List roles, String rules) throws IOException { Map mapping = new HashMap<>(); mapping.put("enabled", true); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/FallbackableClaim.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/FallbackableClaim.java new file mode 100644 index 000000000000..94bba5d03dc6 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/FallbackableClaim.java @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authc.jwt; + +import com.nimbusds.jwt.JWTClaimsSet; + +import org.elasticsearch.core.Nullable; + +import java.text.ParseException; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.elasticsearch.core.Strings.format; + +/** + * A JWT claim that can optionally fallback to another claim (if configured) for retrieving the associated value + * from a {@link JWTClaimsSet}. The fallback behaviour happens only when: + * 1. The fallback is configured (it can be null) + * 2. The original claim does not exist in the {@link JWTClaimsSet} + * In any other cases, the original claim will be used for retrieving the value. + */ +public class FallbackableClaim { + private final String name; + private final JWTClaimsSet claimsSet; + private final String actualName; + + public FallbackableClaim(String name, @Nullable Map fallbackClaimNames, JWTClaimsSet claimsSet) { + this.name = Objects.requireNonNull(name); + this.claimsSet = Objects.requireNonNull(claimsSet); + final String fallbackName; + if (fallbackClaimNames != null) { + fallbackName = fallbackClaimNames.getOrDefault(name, name); + } else { + fallbackName = null; + } + if (fallbackName == null) { + this.actualName = name; + } else { + this.actualName = claimsSet.getClaim(name) != null ? name : fallbackName; + } + } + + public String getActualName() { + return actualName; + } + + public String getStringClaimValue() { + try { + return claimsSet.getStringClaim(actualName); + } catch (ParseException e) { + throw new IllegalArgumentException(format("cannot parse string claim [%s] as string", this), e); + } + } + + public List getStringListClaimValue() { + final Object claimValue = claimsSet.getClaim(actualName); + if (claimValue instanceof String) { + return List.of((String) claimValue); + } else { + try { + return claimsSet.getStringListClaim(actualName); + } catch (ParseException e) { + throw new IllegalArgumentException(format("cannot parse string claim [%s] as string array", this), e); + } + } + } + + @Override + public String toString() { + if (name.equals(actualName)) { + return name; + } else { + return format("%s (fallback of %s)", actualName, name); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAlgorithmValidator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAlgorithmValidator.java index 5f2fe02c263b..79082d140bfb 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAlgorithmValidator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAlgorithmValidator.java @@ -11,12 +11,12 @@ import com.nimbusds.jose.JWSHeader; import com.nimbusds.jwt.JWTClaimsSet; -import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.common.Strings; -import org.elasticsearch.rest.RestStatus; import java.util.List; +import static org.elasticsearch.core.Strings.format; + public class JwtAlgorithmValidator implements JwtFieldValidator { private final List allowedAlgorithms; @@ -28,15 +28,16 @@ public JwtAlgorithmValidator(List allowedAlgorithms) { public void validate(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet) { final JWSAlgorithm algorithm = jwsHeader.getAlgorithm(); if (algorithm == null) { - throw new ElasticsearchSecurityException("missing JWT algorithm header", RestStatus.BAD_REQUEST); + throw new IllegalArgumentException("missing JWT algorithm header"); } if (false == allowedAlgorithms.contains(algorithm.getName())) { - throw new ElasticsearchSecurityException( - "invalid JWT algorithm [{}], allowed algorithms are [{}]", - RestStatus.BAD_REQUEST, - algorithm, - Strings.collectionToCommaDelimitedString(allowedAlgorithms) + throw new IllegalArgumentException( + format( + "invalid JWT algorithm [%s], allowed algorithms are [%s]", + algorithm, + Strings.collectionToCommaDelimitedString(allowedAlgorithms) + ) ); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticator.java index 7b769f15fe72..3f2ff4a39543 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticator.java @@ -24,6 +24,7 @@ import java.text.ParseException; import java.time.Clock; import java.util.List; +import java.util.Map; /** * This class performs validations of header, claims and signatures against the incoming {@link JwtAuthenticationToken}. @@ -37,6 +38,7 @@ public class JwtAuthenticator implements Releasable { private final List jwtFieldValidators; private final JwtSignatureValidator jwtSignatureValidator; private final JwtRealmSettings.TokenType tokenType; + private final Map fallbackClaimNames; public JwtAuthenticator( final RealmConfig realmConfig, @@ -46,9 +48,14 @@ public JwtAuthenticator( this.realmConfig = realmConfig; this.tokenType = realmConfig.getSetting(JwtRealmSettings.TOKEN_TYPE); if (tokenType == JwtRealmSettings.TokenType.ID_TOKEN) { + this.fallbackClaimNames = Map.of(); this.jwtFieldValidators = configureFieldValidatorsForIdToken(realmConfig); } else { - this.jwtFieldValidators = configureFieldValidatorsForAccessToken(realmConfig); + this.fallbackClaimNames = Map.ofEntries( + Map.entry("sub", realmConfig.getSetting(JwtRealmSettings.FALLBACK_SUB_CLAIM)), + Map.entry("aud", realmConfig.getSetting(JwtRealmSettings.FALLBACK_AUD_CLAIM)) + ); + this.jwtFieldValidators = configureFieldValidatorsForAccessToken(realmConfig, fallbackClaimNames); } this.jwtSignatureValidator = new JwtSignatureValidator.DelegatingJwtSignatureValidator(realmConfig, sslService, reloadNotifier); } @@ -112,6 +119,10 @@ public JwtRealmSettings.TokenType getTokenType() { return tokenType; } + public Map getFallbackClaimNames() { + return fallbackClaimNames; + } + // Package private for testing JwtSignatureValidator.DelegatingJwtSignatureValidator getJwtSignatureValidator() { assert jwtSignatureValidator instanceof JwtSignatureValidator.DelegatingJwtSignatureValidator; @@ -122,10 +133,19 @@ private static List configureFieldValidatorsForIdToken(RealmC assert realmConfig.getSetting(JwtRealmSettings.TOKEN_TYPE) == JwtRealmSettings.TokenType.ID_TOKEN; final TimeValue allowedClockSkew = realmConfig.getSetting(JwtRealmSettings.ALLOWED_CLOCK_SKEW); final Clock clock = Clock.systemUTC(); + + final JwtStringClaimValidator subjectClaimValidator; + if (realmConfig.hasSetting(JwtRealmSettings.ALLOWED_SUBJECTS)) { + subjectClaimValidator = new JwtStringClaimValidator("sub", realmConfig.getSetting(JwtRealmSettings.ALLOWED_SUBJECTS), true); + } else { + // Allow any value for the sub claim as long as there is a non-null value + subjectClaimValidator = JwtStringClaimValidator.ALLOW_ALL_SUBJECTS; + } + return List.of( JwtTypeValidator.INSTANCE, - // TODO: mandate "sub" claim once access token support is in place new JwtStringClaimValidator("iss", List.of(realmConfig.getSetting(JwtRealmSettings.ALLOWED_ISSUER)), true), + subjectClaimValidator, new JwtStringClaimValidator("aud", realmConfig.getSetting(JwtRealmSettings.ALLOWED_AUDIENCES), false), new JwtAlgorithmValidator(realmConfig.getSetting(JwtRealmSettings.ALLOWED_SIGNATURE_ALGORITHMS)), new JwtDateClaimValidator(clock, "iat", allowedClockSkew, JwtDateClaimValidator.Relationship.BEFORE_NOW, false), @@ -135,8 +155,23 @@ private static List configureFieldValidatorsForIdToken(RealmC ); } - private static List configureFieldValidatorsForAccessToken(RealmConfig realmConfig) { + private static List configureFieldValidatorsForAccessToken( + RealmConfig realmConfig, + Map fallbackClaimLookup + ) { assert realmConfig.getSetting(JwtRealmSettings.TOKEN_TYPE) == JwtRealmSettings.TokenType.ACCESS_TOKEN; - throw new UnsupportedOperationException("NYI"); + final TimeValue allowedClockSkew = realmConfig.getSetting(JwtRealmSettings.ALLOWED_CLOCK_SKEW); + final Clock clock = Clock.systemUTC(); + + return List.of( + JwtTypeValidator.INSTANCE, + new JwtStringClaimValidator("iss", List.of(realmConfig.getSetting(JwtRealmSettings.ALLOWED_ISSUER)), true), + new JwtStringClaimValidator("sub", fallbackClaimLookup, realmConfig.getSetting(JwtRealmSettings.ALLOWED_SUBJECTS), true), + new JwtStringClaimValidator("aud", fallbackClaimLookup, realmConfig.getSetting(JwtRealmSettings.ALLOWED_AUDIENCES), false), + new JwtAlgorithmValidator(realmConfig.getSetting(JwtRealmSettings.ALLOWED_SIGNATURE_ALGORITHMS)), + new JwtDateClaimValidator(clock, "iat", allowedClockSkew, JwtDateClaimValidator.Relationship.BEFORE_NOW, false), + new JwtDateClaimValidator(clock, "exp", allowedClockSkew, JwtDateClaimValidator.Relationship.AFTER_NOW, false) + ); + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtDateClaimValidator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtDateClaimValidator.java index 07f63c5ee435..8aabededde50 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtDateClaimValidator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtDateClaimValidator.java @@ -10,10 +10,8 @@ import com.nimbusds.jose.JWSHeader; import com.nimbusds.jwt.JWTClaimsSet; -import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.core.Strings; import org.elasticsearch.core.TimeValue; -import org.elasticsearch.rest.RestStatus; import java.text.ParseException; import java.time.Clock; @@ -47,14 +45,14 @@ public void validate(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet) { try { claimValue = jwtClaimsSet.getDateClaim(claimName); } catch (ParseException e) { - throw new ElasticsearchSecurityException("cannot parse date claim [" + claimName + "]", RestStatus.BAD_REQUEST, e); + throw new IllegalArgumentException("cannot parse date claim [" + claimName + "]", e); } if (claimValue == null) { if (allowNull) { return; } else { - throw new ElasticsearchSecurityException("missing required date claim [" + claimName + "]"); + throw new IllegalArgumentException("missing required date claim [" + claimName + "]"); } } @@ -64,27 +62,25 @@ public void validate(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet) { switch (relationship) { case BEFORE_NOW: if (false == claimInstant.isBefore(now.plusSeconds(allowedClockSkewSeconds))) { - throw new ElasticsearchSecurityException( + throw new IllegalArgumentException( Strings.format( "date claim [%s] value [%s] must be before now [%s]", claimName, claimInstant.toEpochMilli(), now.toEpochMilli() - ), - RestStatus.BAD_REQUEST + ) ); } break; case AFTER_NOW: if (false == claimInstant.isAfter(now.minusSeconds(allowedClockSkewSeconds))) { - throw new ElasticsearchSecurityException( + throw new IllegalArgumentException( Strings.format( "date claim [%s] value [%s] must be after now [%s]", claimName, claimInstant.toEpochMilli(), now.toEpochMilli() - ), - RestStatus.BAD_REQUEST + ) ); } break; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealm.java index 847dc7ffaf6d..495fd23ac23f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealm.java @@ -83,11 +83,7 @@ public class JwtRealm extends Realm implements CachingRealm, Releasable { this.userRoleMapper = userRoleMapper; this.userRoleMapper.refreshRealmOnChange(this); this.allowedClockSkew = realmConfig.getSetting(JwtRealmSettings.ALLOWED_CLOCK_SKEW); - this.claimParserPrincipal = ClaimParser.forSetting(logger, JwtRealmSettings.CLAIMS_PRINCIPAL, realmConfig, true); - this.claimParserGroups = ClaimParser.forSetting(logger, JwtRealmSettings.CLAIMS_GROUPS, realmConfig, false); - this.claimParserDn = ClaimParser.forSetting(logger, JwtRealmSettings.CLAIMS_DN, realmConfig, false); - this.claimParserMail = ClaimParser.forSetting(logger, JwtRealmSettings.CLAIMS_MAIL, realmConfig, false); - this.claimParserName = ClaimParser.forSetting(logger, JwtRealmSettings.CLAIMS_NAME, realmConfig, false); + this.populateUserMetadata = realmConfig.getSetting(JwtRealmSettings.POPULATE_USER_METADATA); this.clientAuthenticationType = realmConfig.getSetting(JwtRealmSettings.CLIENT_AUTHENTICATION_TYPE); final SecureString sharedSecret = realmConfig.getSetting(JwtRealmSettings.CLIENT_AUTHENTICATION_SHARED_SECRET); @@ -115,6 +111,20 @@ public class JwtRealm extends Realm implements CachingRealm, Releasable { this.jwtCacheHelper = null; } jwtAuthenticator = new JwtAuthenticator(realmConfig, sslService, this::expireAll); + + final Map fallbackClaimNames = jwtAuthenticator.getFallbackClaimNames(); + + this.claimParserPrincipal = ClaimParser.forSetting( + logger, + JwtRealmSettings.CLAIMS_PRINCIPAL, + fallbackClaimNames, + realmConfig, + true + ); + this.claimParserGroups = ClaimParser.forSetting(logger, JwtRealmSettings.CLAIMS_GROUPS, fallbackClaimNames, realmConfig, false); + this.claimParserDn = ClaimParser.forSetting(logger, JwtRealmSettings.CLAIMS_DN, fallbackClaimNames, realmConfig, false); + this.claimParserMail = ClaimParser.forSetting(logger, JwtRealmSettings.CLAIMS_MAIL, fallbackClaimNames, realmConfig, false); + this.claimParserName = ClaimParser.forSetting(logger, JwtRealmSettings.CLAIMS_NAME, fallbackClaimNames, realmConfig, false); } /** diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidator.java index 7afeff7022f2..30ea0979a624 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidator.java @@ -10,12 +10,11 @@ import com.nimbusds.jose.JWSHeader; import com.nimbusds.jwt.JWTClaimsSet; -import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.common.Strings; -import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.core.Nullable; -import java.text.ParseException; import java.util.List; +import java.util.Map; /** * Validates a string claim against a list of allowed values. The validation is successful @@ -25,60 +24,65 @@ * values. * Whether a claim's value can be an array of strings is customised with the {@link #singleValuedClaim} * field, which enforces the claim's value to be a single string if it is configured to {@code true}. + * + * NOTE the allowed values can be null which means skipping the actual value check, i.e. the validator + * succeeds as long as there is a (non-null) value. */ public class JwtStringClaimValidator implements JwtFieldValidator { + public static JwtStringClaimValidator ALLOW_ALL_SUBJECTS = new JwtStringClaimValidator("sub", null, true); + private final String claimName; + @Nullable + private final Map fallbackClaimNames; + @Nullable private final List allowedClaimValues; // Whether the claim should be a single string private final boolean singleValuedClaim; public JwtStringClaimValidator(String claimName, List allowedClaimValues, boolean singleValuedClaim) { + this(claimName, null, allowedClaimValues, singleValuedClaim); + } + + public JwtStringClaimValidator( + String claimName, + Map fallbackClaimNames, + List allowedClaimValues, + boolean singleValuedClaim + ) { this.claimName = claimName; + this.fallbackClaimNames = fallbackClaimNames; this.allowedClaimValues = allowedClaimValues; this.singleValuedClaim = singleValuedClaim; } @Override public void validate(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet) { - final List claimValues; - try { - claimValues = getStringClaimValues(jwtClaimsSet); - } catch (ParseException e) { - throw new ElasticsearchSecurityException("cannot parse string claim [" + claimName + "]", RestStatus.BAD_REQUEST, e); - } + final FallbackableClaim fallbackableClaim = new FallbackableClaim(claimName, fallbackClaimNames, jwtClaimsSet); + final List claimValues = getStringClaimValues(fallbackableClaim); if (claimValues == null) { - throw new ElasticsearchSecurityException("missing required string claim [" + claimName + "]", RestStatus.BAD_REQUEST); + throw new IllegalArgumentException("missing required string claim [" + fallbackableClaim + "]"); } - if (false == claimValues.stream().anyMatch(allowedClaimValues::contains)) { - throw new ElasticsearchSecurityException( + if (allowedClaimValues != null && false == claimValues.stream().anyMatch(allowedClaimValues::contains)) { + throw new IllegalArgumentException( "string claim [" - + claimName + + fallbackableClaim + "] has value [" + Strings.collectionToCommaDelimitedString(claimValues) + "] which does not match allowed claim values [" + Strings.collectionToCommaDelimitedString(allowedClaimValues) - + "]", - RestStatus.BAD_REQUEST + + "]" ); } } - private List getStringClaimValues(JWTClaimsSet claimsSet) throws ParseException { - // TODO: fallback claims - final String actualClaimName = claimName; - + private List getStringClaimValues(FallbackableClaim fallbackableClaim) { if (singleValuedClaim) { - final String claimValue = claimsSet.getStringClaim(actualClaimName); + final String claimValue = fallbackableClaim.getStringClaimValue(); return claimValue != null ? List.of(claimValue) : null; } else { - final Object claimValue = claimsSet.getClaim(actualClaimName); - if (claimValue instanceof String) { - return List.of((String) claimValue); - } else { - return claimsSet.getStringListClaim(actualClaimName); - } + return fallbackableClaim.getStringListClaimValue(); } } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtTypeValidator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtTypeValidator.java index 5a163c7c566e..8d0dd06d59c2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtTypeValidator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtTypeValidator.java @@ -15,9 +15,6 @@ import com.nimbusds.jose.proc.SecurityContext; import com.nimbusds.jwt.JWTClaimsSet; -import org.elasticsearch.ElasticsearchSecurityException; -import org.elasticsearch.rest.RestStatus; - public class JwtTypeValidator implements JwtFieldValidator { private static final JOSEObjectTypeVerifier JWT_HEADER_TYPE_VERIFIER = new DefaultJOSEObjectTypeVerifier<>( @@ -34,7 +31,7 @@ public void validate(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet) { try { JWT_HEADER_TYPE_VERIFIER.verify(jwtHeaderType, null); } catch (BadJOSEException e) { - throw new ElasticsearchSecurityException("invalid jwt typ header", RestStatus.BAD_REQUEST, e); + throw new IllegalArgumentException("invalid jwt typ header", e); } } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/ClaimParser.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/ClaimParser.java index 25a42b215d23..0b9ea1d29033 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/ClaimParser.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/ClaimParser.java @@ -15,9 +15,11 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.support.ClaimSetting; +import org.elasticsearch.xpack.security.authc.jwt.FallbackableClaim; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.function.Function; import java.util.regex.Matcher; @@ -80,9 +82,9 @@ public String toString() { } @SuppressWarnings("unchecked") - private static Collection parseClaimValues(JWTClaimsSet claimsSet, String claimName, String settingKey) { + private static Collection parseClaimValues(JWTClaimsSet claimsSet, FallbackableClaim fallbackableClaim, String settingKey) { Collection values; - final Object claimValueObject = claimsSet.getClaim(claimName); + final Object claimValueObject = claimsSet.getClaim(fallbackableClaim.getActualName()); if (claimValueObject == null) { values = List.of(); } else if (claimValueObject instanceof String) { @@ -91,50 +93,67 @@ private static Collection parseClaimValues(JWTClaimsSet claimsSet, Strin && ((Collection) claimValueObject).stream().allMatch(c -> c instanceof String)) { values = (Collection) claimValueObject; } else { - throw new SettingsException("Setting [ " + settingKey + " expects a claim with String or a String Array value"); + throw new SettingsException( + "Setting [ " + settingKey + "] expects claim [" + fallbackableClaim + "] with String or a String Array value" + ); } return values; } public static ClaimParser forSetting(Logger logger, ClaimSetting setting, RealmConfig realmConfig, boolean required) { + return forSetting(logger, setting, Map.of(), realmConfig, required); + } + + public static ClaimParser forSetting( + Logger logger, + ClaimSetting setting, + Map fallbackClaimNames, + RealmConfig realmConfig, + boolean required + ) { if (realmConfig.hasSetting(setting.getClaim())) { - String claimName = realmConfig.getSetting(setting.getClaim()); + final String claimName = realmConfig.getSetting(setting.getClaim()); if (realmConfig.hasSetting(setting.getPattern())) { Pattern regex = Pattern.compile(realmConfig.getSetting(setting.getPattern())); return new ClaimParser(setting.name(realmConfig), claimName, regex.pattern(), claims -> { + final FallbackableClaim fallbackableClaim = new FallbackableClaim(claimName, fallbackClaimNames, claims); Collection values = parseClaimValues( claims, - claimName, + fallbackableClaim, RealmSettings.getFullSettingKey(realmConfig, setting.getClaim()) ); return values.stream().map(s -> { if (s == null) { - logger.debug("Claim [{}] is null", claimName); + logger.debug("Claim [{}] is null", fallbackableClaim); return null; } final Matcher matcher = regex.matcher(s); if (matcher.find() == false) { - logger.debug("Claim [{}] is [{}], which does not match [{}]", claimName, s, regex.pattern()); + logger.debug("Claim [{}] is [{}], which does not match [{}]", fallbackableClaim, s, regex.pattern()); return null; } final String value = matcher.group(1); if (Strings.isNullOrEmpty(value)) { - logger.debug("Claim [{}] is [{}], which does match [{}] but group(1) is empty", claimName, s, regex.pattern()); + logger.debug( + "Claim [{}] is [{}], which does match [{}] but group(1) is empty", + fallbackableClaim, + s, + regex.pattern() + ); return null; } return value; }).filter(Objects::nonNull).toList(); }); } else { - return new ClaimParser( - setting.name(realmConfig), - claimName, - null, - claims -> parseClaimValues(claims, claimName, RealmSettings.getFullSettingKey(realmConfig, setting.getClaim())).stream() + return new ClaimParser(setting.name(realmConfig), claimName, null, claims -> { + final FallbackableClaim fallbackableClaim = new FallbackableClaim(claimName, fallbackClaimNames, claims); + return parseClaimValues(claims, fallbackableClaim, RealmSettings.getFullSettingKey(realmConfig, setting.getClaim())) + .stream() .filter(Objects::nonNull) - .toList() - ); + .toList(); + }); } } else if (required) { throw new SettingsException("Setting [" + RealmSettings.getFullSettingKey(realmConfig, setting.getClaim()) + "] is required"); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/FallbackableClaimTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/FallbackableClaimTests.java new file mode 100644 index 000000000000..3fccbb4e5458 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/FallbackableClaimTests.java @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authc.jwt; + +import com.nimbusds.jwt.JWTClaimsSet; + +import org.elasticsearch.test.ESTestCase; + +import java.text.ParseException; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; + +public class FallbackableClaimTests extends ESTestCase { + + public void testNoFallback() throws ParseException { + final String name = randomAlphaOfLength(10); + final String value = randomAlphaOfLength(10); + final FallbackableClaim fallbackableClaim = new FallbackableClaim(name, null, JWTClaimsSet.parse(Map.of(name, value))); + assertThat(fallbackableClaim.getActualName(), equalTo(name)); + assertThat(fallbackableClaim.toString(), equalTo(name)); + assertThat(fallbackableClaim.getStringClaimValue(), equalTo(value)); + assertThat(fallbackableClaim.getStringListClaimValue(), equalTo(List.of(value))); + } + + public void testFallback() throws ParseException { + final String name = randomAlphaOfLength(10); + final String fallbackName = randomAlphaOfLength(12); + final String value = randomAlphaOfLength(10); + + // fallback ignored + final JWTClaimsSet claimSet1 = JWTClaimsSet.parse(Map.of(name, value, fallbackName, randomAlphaOfLength(16))); + final FallbackableClaim fallbackableClaim1 = new FallbackableClaim(name, Map.of(name, fallbackName), claimSet1); + assertThat(fallbackableClaim1.getActualName(), equalTo(name)); + assertThat(fallbackableClaim1.toString(), equalTo(name)); + assertThat(fallbackableClaim1.getStringClaimValue(), equalTo(value)); + assertThat(fallbackableClaim1.getStringListClaimValue(), equalTo(List.of(value))); + + // fallback active + final JWTClaimsSet claimSet2 = JWTClaimsSet.parse(Map.of(fallbackName, value)); + final FallbackableClaim fallbackableClaim2 = new FallbackableClaim(name, Map.of(name, fallbackName), claimSet2); + assertThat(fallbackableClaim2.getActualName(), equalTo(fallbackName)); + assertThat(fallbackableClaim2.toString(), equalTo(fallbackName + " (fallback of " + name + ")")); + assertThat(fallbackableClaim2.getStringClaimValue(), equalTo(value)); + assertThat(fallbackableClaim2.getStringListClaimValue(), equalTo(List.of(value))); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAlgorithmValidatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAlgorithmValidatorTests.java index 355d39b2d9d8..516f608dad95 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAlgorithmValidatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAlgorithmValidatorTests.java @@ -10,7 +10,6 @@ import com.nimbusds.jose.JWSHeader; import com.nimbusds.jwt.JWTClaimsSet; -import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.test.ESTestCase; import java.text.ParseException; @@ -39,8 +38,8 @@ public void testMismatchingAlgorithm() throws ParseException { final JwtAlgorithmValidator validator = new JwtAlgorithmValidator(randomList(1, 5, () -> randomAlphaOfLength(8))); final JWSHeader jwsHeader = JWSHeader.parse(Map.of("alg", algorithm)); - final ElasticsearchSecurityException e = expectThrows( - ElasticsearchSecurityException.class, + final IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, () -> validator.validate(jwsHeader, JWTClaimsSet.parse(Map.of())) ); assertThat(e.getMessage(), containsString("invalid JWT algorithm")); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorAccessTokenTypeTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorAccessTokenTypeTests.java new file mode 100644 index 000000000000..23dd9d52f933 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorAccessTokenTypeTests.java @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authc.jwt; + +import org.elasticsearch.xpack.core.security.authc.RealmSettings; +import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings; +import org.junit.Before; + +import java.text.ParseException; + +import static org.hamcrest.Matchers.containsString; + +public class JwtAuthenticatorAccessTokenTypeTests extends JwtAuthenticatorTests { + + private String fallbackSub; + private String fallbackAud; + + @Before + public void beforeTest() { + doBeforeTest(); + fallbackSub = randomBoolean() ? "_" + randomAlphaOfLength(5) : null; + fallbackAud = randomBoolean() ? "_" + randomAlphaOfLength(8) : null; + } + + @Override + protected JwtRealmSettings.TokenType getTokenType() { + return JwtRealmSettings.TokenType.ACCESS_TOKEN; + } + + public void testSubjectIsRequired() throws ParseException { + final IllegalArgumentException e = doTestSubjectIsRequired(buildJwtAuthenticator(fallbackSub, fallbackAud)); + if (fallbackSub != null) { + assertThat(e.getMessage(), containsString("missing required string claim [" + fallbackSub + " (fallback of sub)]")); + } + } + + public void testAccessTokenTypeMandatesAllowedSubjects() { + allowedSubject = null; + final IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, + () -> buildJwtAuthenticator(fallbackSub, fallbackAud) + ); + + assertThat( + e.getMessage(), + containsString("Invalid empty list for [" + RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.ALLOWED_SUBJECTS) + "]") + ); + } + + public void testInvalidIssuerIsCheckedBeforeAlgorithm() throws ParseException { + doTestInvalidIssuerIsCheckedBeforeAlgorithm(buildJwtAuthenticator(fallbackSub, fallbackAud)); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorIdTokenTypeTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorIdTokenTypeTests.java new file mode 100644 index 000000000000..ad7e50109d56 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorIdTokenTypeTests.java @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authc.jwt; + +import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings; +import org.junit.Before; + +import java.text.ParseException; + +import static org.hamcrest.Matchers.containsString; + +public class JwtAuthenticatorIdTokenTypeTests extends JwtAuthenticatorTests { + + private String fallbackSub; + private String fallbackAud; + + @Before + public void beforeTest() { + doBeforeTest(); + fallbackSub = null; + fallbackAud = null; + } + + @Override + protected JwtRealmSettings.TokenType getTokenType() { + return JwtRealmSettings.TokenType.ID_TOKEN; + } + + public void testSubjectIsRequired() throws ParseException { + final IllegalArgumentException e = doTestSubjectIsRequired(buildJwtAuthenticator(fallbackSub, fallbackAud)); + assertThat(e.getMessage(), containsString("missing required string claim [sub]")); + } + + public void testInvalidIssuerIsCheckedBeforeAlgorithm() throws ParseException { + doTestInvalidIssuerIsCheckedBeforeAlgorithm(buildJwtAuthenticator(fallbackSub, fallbackAud)); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorTests.java index 0f22070458c5..bd92534c05fc 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorTests.java @@ -12,12 +12,12 @@ import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; -import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.settings.MockSecureSettings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.core.Nullable; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.authc.RealmConfig; @@ -31,20 +31,50 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class JwtAuthenticatorTests extends ESTestCase { +public abstract class JwtAuthenticatorTests extends ESTestCase { - public void testInvalidIssuerIsCheckedBeforeAlgorithm() throws ParseException { - final String realmName = randomAlphaOfLengthBetween(3, 8); - final String allowedIssuer = randomAlphaOfLength(6); - final String allowedAlgorithm = randomFrom(JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS_HMAC); - final JwtAuthenticator jwtAuthenticator = buildJwtAuthenticator(realmName, allowedAlgorithm, allowedIssuer); + protected String realmName; + protected String allowedAlgorithm; + protected String allowedIssuer; + @Nullable + protected String allowedSubject; + protected String allowedAudience; + protected abstract JwtRealmSettings.TokenType getTokenType(); + + protected void doBeforeTest() { + realmName = randomAlphaOfLengthBetween(3, 8); + allowedIssuer = randomAlphaOfLength(6); + allowedAlgorithm = randomFrom(JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS_HMAC); + if (getTokenType() == JwtRealmSettings.TokenType.ID_TOKEN) { + allowedSubject = randomBoolean() ? randomAlphaOfLength(8) : null; + } else { + allowedSubject = randomAlphaOfLength(8); + } + allowedAudience = randomAlphaOfLength(10); + } + + protected IllegalArgumentException doTestSubjectIsRequired(JwtAuthenticator jwtAuthenticator) throws ParseException { + final SignedJWT signedJWT = new SignedJWT( + JWSHeader.parse(Map.of("alg", allowedAlgorithm)).toBase64URL(), + JWTClaimsSet.parse(Map.of("iss", allowedIssuer)).toPayload().toBase64URL(), + Base64URL.encode("signature") + ); + final JwtAuthenticationToken jwtAuthenticationToken = mock(JwtAuthenticationToken.class); + when(jwtAuthenticationToken.getEndUserSignedJwt()).thenReturn(new SecureString(signedJWT.serialize().toCharArray())); + + final PlainActionFuture future = new PlainActionFuture<>(); + jwtAuthenticator.authenticate(jwtAuthenticationToken, future); + return expectThrows(IllegalArgumentException.class, future::actionGet); + } + + protected void doTestInvalidIssuerIsCheckedBeforeAlgorithm(JwtAuthenticator jwtAuthenticator) throws ParseException { // A JWT token that has mismatch for both algorithm and issuer final String invalidAlgorithm = randomValueOtherThan(allowedAlgorithm, () -> randomAlphaOfLengthBetween(3, 8)); final String invalidIssuer = randomValueOtherThan(allowedIssuer, () -> randomAlphaOfLengthBetween(3, 8)); final SignedJWT signedJWT = new SignedJWT( JWSHeader.parse(Map.of("alg", invalidAlgorithm)).toBase64URL(), - JWTClaimsSet.parse(Map.of("iss", invalidIssuer)).toPayload().toBase64URL(), + JWTClaimsSet.parse(Map.of("iss", invalidIssuer, "sub", randomAlphaOfLengthBetween(3, 8))).toPayload().toBase64URL(), Base64URL.encode("signature") ); final JwtAuthenticationToken jwtAuthenticationToken = mock(JwtAuthenticationToken.class); @@ -53,8 +83,7 @@ public void testInvalidIssuerIsCheckedBeforeAlgorithm() throws ParseException { final PlainActionFuture future = new PlainActionFuture<>(); jwtAuthenticator.authenticate(jwtAuthenticationToken, future); - final ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, future::actionGet); - + final IllegalArgumentException e = expectThrows(IllegalArgumentException.class, future::actionGet); assertThat( e, throwableWithMessage( @@ -63,7 +92,7 @@ public void testInvalidIssuerIsCheckedBeforeAlgorithm() throws ParseException { ); } - private JwtAuthenticator buildJwtAuthenticator(String realmName, String allowedAlgorithm, String allowedIssuer) { + protected JwtAuthenticator buildJwtAuthenticator(String fallbackSub, String fallbackAud) { final RealmConfig.RealmIdentifier realmIdentifier = new RealmConfig.RealmIdentifier(JwtRealmSettings.TYPE, realmName); final MockSecureSettings secureSettings = new MockSecureSettings(); secureSettings.setString(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.HMAC_KEY), randomAlphaOfLength(40)); @@ -74,9 +103,26 @@ private JwtAuthenticator buildJwtAuthenticator(String realmName, String allowedA .put(RealmSettings.getFullSettingKey(realmIdentifier, RealmSettings.ORDER_SETTING), randomIntBetween(0, 99)) .put("path.home", randomAlphaOfLength(10)) .setSecureSettings(secureSettings); - if (randomBoolean()) { - builder.put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.TOKEN_TYPE), "id_token"); + + if (allowedSubject != null) { + builder.put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.ALLOWED_SUBJECTS), allowedSubject); } + + if (getTokenType() == JwtRealmSettings.TokenType.ID_TOKEN) { + if (randomBoolean()) { + builder.put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.TOKEN_TYPE), "id_token"); + } + } else { + builder.put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.TOKEN_TYPE), "access_token"); + } + + if (fallbackSub != null) { + builder.put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.FALLBACK_SUB_CLAIM), fallbackSub); + } + if (fallbackAud != null) { + builder.put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.FALLBACK_AUD_CLAIM), fallbackAud); + } + final Settings settings = builder.build(); final RealmConfig realmConfig = new RealmConfig( diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtDateClaimValidatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtDateClaimValidatorTests.java index e27e9e1ab027..7bb60fca9c23 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtDateClaimValidatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtDateClaimValidatorTests.java @@ -10,7 +10,6 @@ import com.nimbusds.jose.JWSHeader; import com.nimbusds.jwt.JWTClaimsSet; -import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.core.TimeValue; import org.elasticsearch.test.ESTestCase; import org.junit.Before; @@ -44,8 +43,8 @@ public void testClaimIsNotDate() throws ParseException { ); final JWTClaimsSet jwtClaimsSet = JWTClaimsSet.parse(Map.of(claimName, randomAlphaOfLengthBetween(3, 8))); - final ElasticsearchSecurityException e = expectThrows( - ElasticsearchSecurityException.class, + final IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, () -> validator.validate(getJwsHeader(), jwtClaimsSet) ); assertThat(e.getMessage(), containsString("cannot parse date claim")); @@ -64,8 +63,8 @@ public void testClaimDoesNotExist() throws ParseException { ); final JWTClaimsSet jwtClaimsSet = JWTClaimsSet.parse(Map.of()); - final ElasticsearchSecurityException e = expectThrows( - ElasticsearchSecurityException.class, + final IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, () -> validator.validate(getJwsHeader(), jwtClaimsSet) ); assertThat(e.getMessage(), containsString("missing required date claim")); @@ -112,8 +111,8 @@ public void testBeforeNow() throws ParseException { } final Instant after = now.plusSeconds(randomLongBetween(1 + allowedSkewInSeconds, 600)); - final ElasticsearchSecurityException e = expectThrows( - ElasticsearchSecurityException.class, + final IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, () -> validator.validate(getJwsHeader(), JWTClaimsSet.parse(Map.of(claimName, after.getEpochSecond()))) ); assertThat( @@ -145,8 +144,8 @@ public void testAfterNow() throws ParseException { when(clock.instant()).thenReturn(now); final Instant before = now.minusSeconds(randomLongBetween(1 + allowedSkewInSeconds, 600)); - final ElasticsearchSecurityException e = expectThrows( - ElasticsearchSecurityException.class, + final IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, () -> validator.validate(getJwsHeader(), JWTClaimsSet.parse(Map.of(claimName, before.getEpochSecond()))) ); assertThat( diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateAccessTokenTypeTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateAccessTokenTypeTests.java new file mode 100644 index 000000000000..b0dd07c67fbb --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateAccessTokenTypeTests.java @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authc.jwt; + +import com.nimbusds.jose.JOSEObjectType; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.openid.connect.sdk.Nonce; + +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.xpack.core.security.authc.RealmSettings; +import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings; +import org.elasticsearch.xpack.core.security.user.User; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +public class JwtRealmAuthenticateAccessTokenTypeTests extends JwtRealmTestCase { + + private String fallbackSub; + private String fallbackAud; + private SignedJWT unsignedJwt; + + public void testAccessTokenTypeWorksWithNoFallback() throws Exception { + noFallback(); + + jwtIssuerAndRealms = generateJwtIssuerRealmPairs( + createJwtRealmsSettingsBuilder(), + randomIntBetween(1, 1), // realms + randomIntBetween(0, 1), // authz + randomIntBetween(1, JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS.size()), // algorithms + randomIntBetween(1, 3), // audiences + randomIntBetween(1, 3), // users + randomIntBetween(0, 3), // roles + randomIntBetween(0, 1), // jwtCacheSize + randomBoolean() // createHttpsServer + ); + final JwtIssuerAndRealm jwtIssuerAndRealm = randomJwtIssuerRealmPair(); + final User user = randomUser(jwtIssuerAndRealm.issuer()); + + final SecureString jwt = randomJwt(jwtIssuerAndRealm, user); + final SecureString clientSecret = JwtRealmInspector.getClientAuthenticationSharedSecret(jwtIssuerAndRealm.realm()); + doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, clientSecret, randomIntBetween(1, 3)); + } + + public void testAccessTokenTypeWorksWithFallbacks() throws Exception { + randomFallbacks(); + + jwtIssuerAndRealms = generateJwtIssuerRealmPairs( + createJwtRealmsSettingsBuilder(), + randomIntBetween(1, 1), // realms + randomIntBetween(0, 1), // authz + randomIntBetween(1, JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS.size()), // algorithms + randomIntBetween(1, 3), // audiences + randomIntBetween(1, 3), // users + randomIntBetween(0, 3), // roles + randomIntBetween(0, 1), // jwtCacheSize + randomBoolean() // createHttpsServer + ); + final JwtIssuerAndRealm jwtIssuerAndRealm = randomJwtIssuerRealmPair(); + final User user = randomUser(jwtIssuerAndRealm.issuer()); + + final SecureString jwt2 = randomJwt(jwtIssuerAndRealm, user); + final SecureString clientSecret = JwtRealmInspector.getClientAuthenticationSharedSecret(jwtIssuerAndRealm.realm()); + doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt2, clientSecret, randomIntBetween(1, 3)); + } + + @Override + protected JwtRealmSettingsBuilder createJwtRealmSettingsBuilder(JwtIssuer jwtIssuer, int authzCount, int jwtCacheSize) + throws Exception { + final JwtRealmSettingsBuilder jwtRealmSettingsBuilder = super.createJwtRealmSettingsBuilder(jwtIssuer, authzCount, jwtCacheSize); + final String realmName = jwtRealmSettingsBuilder.name(); + final Settings.Builder settingsBuilder = jwtRealmSettingsBuilder.settingsBuilder(); + settingsBuilder.put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.TOKEN_TYPE), "access_token") + .putList( + RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.ALLOWED_SUBJECTS), + jwtIssuer.principals.keySet().stream().toList() + ); + + if (fallbackSub != null) { + settingsBuilder.put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.FALLBACK_SUB_CLAIM), fallbackSub); + } + if (fallbackAud != null) { + settingsBuilder.put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.FALLBACK_AUD_CLAIM), fallbackAud); + } + + return jwtRealmSettingsBuilder; + } + + @Override + protected SecureString randomJwt(JwtIssuerAndRealm jwtIssuerAndRealm, User user) throws Exception { + final JwtIssuer.AlgJwkPair algJwkPair = randomFrom(jwtIssuerAndRealm.issuer().algAndJwksAll); + final JWK jwk = algJwkPair.jwk(); + + final HashMap otherClaims = new HashMap<>(); + if (randomBoolean()) { + otherClaims.putAll(Map.of("other1", randomAlphaOfLength(10), "other2", randomAlphaOfLength(10))); + } + + // Randomly set the fallback claims, it can co-exist with the original one in which case it is ignored + String subClaimValue = user.principal(); + if (fallbackSub != null) { + if (randomBoolean()) { + // original claim does not exist, so it's the effective fallback + otherClaims.put(fallbackSub, subClaimValue); + subClaimValue = null; + } else { + // original claim still exist, in this case, the fallback can be anything and it does not matter + otherClaims.put(fallbackSub, randomValueOtherThan(subClaimValue, () -> randomAlphaOfLength(15))); + } + } + // TODO: fallback aud + List audClaimValue = JwtRealmInspector.getAllowedAudiences(jwtIssuerAndRealm.realm()); + + // A bogus auth_time but access_token type does not check it + if (randomBoolean()) { + otherClaims.put("auth_time", randomAlphaOfLengthBetween(6, 18)); + } + + final Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); + unsignedJwt = JwtTestCase.buildUnsignedJwt( + randomBoolean() ? null : JOSEObjectType.JWT.toString(), // kty + randomBoolean() ? null : jwk.getKeyID(), // kid + algJwkPair.alg(), // alg + randomAlphaOfLengthBetween(10, 20), // jwtID + JwtRealmInspector.getAllowedIssuer(jwtIssuerAndRealm.realm()), // iss + audClaimValue, + subClaimValue, + JwtRealmInspector.getPrincipalClaimName(jwtIssuerAndRealm.realm()), // principal claim name + user.principal(), // principal claim value + JwtRealmInspector.getGroupsClaimName(jwtIssuerAndRealm.realm()), // group claim name + List.of(user.roles()), // group claim value + null, + Date.from(now.minusSeconds(randomBoolean() ? 0 : 60 * randomLongBetween(5, 10))), // iat + Date.from(now), // nbf + Date.from(now.plusSeconds(60 * randomLongBetween(3600, 7200))), // exp + randomBoolean() ? null : new Nonce(32).toString(), + otherClaims + ); + final SecureString signedJWT = JwtValidateUtil.signJwt(jwk, unsignedJwt); + assertThat(JwtValidateUtil.verifyJwt(jwk, SignedJWT.parse(signedJWT.toString())), is(equalTo(true))); + return signedJWT; + } + + private void noFallback() { + fallbackSub = null; + fallbackAud = null; + } + + private void randomFallbacks() { + fallbackSub = randomBoolean() ? "_" + randomAlphaOfLength(5) : null; + fallbackAud = randomBoolean() || fallbackSub == null ? "_" + randomAlphaOfLength(8) : null; + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateTests.java index 32c2ce32083a..ad898fa3fc51 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateTests.java @@ -50,21 +50,21 @@ public class JwtRealmAuthenticateTests extends JwtRealmTestCase { public void testJwtAuthcRealmAuthcAuthzWithEmptyRoles() throws Exception { this.jwtIssuerAndRealms = this.generateJwtIssuerRealmPairs( this.createJwtRealmsSettingsBuilder(), - new MinMax(1, 1), // realmsRange - new MinMax(0, 1), // authzRange - new MinMax(1, JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS.size()), // algsRange - new MinMax(1, 3), // audiencesRange - new MinMax(1, 3), // usersRange - new MinMax(0, 0), // rolesRange - new MinMax(0, 1), // jwtCacheSizeRange + randomIntBetween(1, 1), // realmsRange + randomIntBetween(0, 1), // authzRange + randomIntBetween(1, JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS.size()), // algsRange + randomIntBetween(1, 3), // audiencesRange + randomIntBetween(1, 3), // usersRange + randomIntBetween(0, 0), // rolesRange + randomIntBetween(0, 1), // jwtCacheSizeRange randomBoolean() // createHttpsServer ); final JwtIssuerAndRealm jwtIssuerAndRealm = this.randomJwtIssuerRealmPair(); final User user = this.randomUser(jwtIssuerAndRealm.issuer()); final SecureString jwt = this.randomJwt(jwtIssuerAndRealm, user); final SecureString clientSecret = JwtRealmInspector.getClientAuthenticationSharedSecret(jwtIssuerAndRealm.realm()); - final MinMax jwtAuthcRange = new MinMax(2, 3); - this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, clientSecret, jwtAuthcRange); + final int jwtAuthcCount = randomIntBetween(2, 3); + this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, clientSecret, jwtAuthcCount); } /** @@ -74,13 +74,13 @@ public void testJwtAuthcRealmAuthcAuthzWithEmptyRoles() throws Exception { public void testJwtAuthcRealmAuthcAuthzWithoutAuthzRealms() throws Exception { this.jwtIssuerAndRealms = this.generateJwtIssuerRealmPairs( this.createJwtRealmsSettingsBuilder(), - new MinMax(1, 3), // realmsRange - new MinMax(0, 0), // authzRange - new MinMax(1, JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS.size()), // algsRange - new MinMax(1, 3), // audiencesRange - new MinMax(1, 3), // usersRange - new MinMax(0, 3), // rolesRange - new MinMax(0, 1), // jwtCacheSizeRange + randomIntBetween(1, 3), // realmsRange + randomIntBetween(0, 0), // authzRange + randomIntBetween(1, JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS.size()), // algsRange + randomIntBetween(1, 3), // audiencesRange + randomIntBetween(1, 3), // usersRange + randomIntBetween(0, 3), // rolesRange + randomIntBetween(0, 1), // jwtCacheSizeRange randomBoolean() // createHttpsServer ); final JwtIssuerAndRealm jwtIssuerAndRealm = this.randomJwtIssuerRealmPair(); @@ -89,8 +89,8 @@ public void testJwtAuthcRealmAuthcAuthzWithoutAuthzRealms() throws Exception { final User user = this.randomUser(jwtIssuerAndRealm.issuer()); final SecureString jwt = this.randomJwt(jwtIssuerAndRealm, user); final SecureString clientSecret = JwtRealmInspector.getClientAuthenticationSharedSecret(jwtIssuerAndRealm.realm()); - final MinMax jwtAuthcRange = new MinMax(2, 3); - this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, clientSecret, jwtAuthcRange); + final int jwtAuthcCount = randomIntBetween(2, 3); + this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, clientSecret, jwtAuthcCount); } /** @@ -100,13 +100,13 @@ public void testJwtAuthcRealmAuthcAuthzWithoutAuthzRealms() throws Exception { public void testJwkSetUpdates() throws Exception { this.jwtIssuerAndRealms = this.generateJwtIssuerRealmPairs( this.createJwtRealmsSettingsBuilder(), - new MinMax(1, 3), // realmsRange - new MinMax(0, 0), // authzRange - new MinMax(1, JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS.size()), // algsRange - new MinMax(1, 3), // audiencesRange - new MinMax(1, 3), // usersRange - new MinMax(0, 3), // rolesRange - new MinMax(0, 1), // jwtCacheSizeRange + randomIntBetween(1, 3), // realmsRange + randomIntBetween(0, 0), // authzRange + randomIntBetween(1, JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS.size()), // algsRange + randomIntBetween(1, 3), // audiencesRange + randomIntBetween(1, 3), // usersRange + randomIntBetween(0, 3), // rolesRange + randomIntBetween(0, 1), // jwtCacheSizeRange randomBoolean() // createHttpsServer ); final JwtIssuerAndRealm jwtIssuerAndRealm = this.randomJwtIssuerRealmPair(); @@ -115,8 +115,8 @@ public void testJwkSetUpdates() throws Exception { final User user = this.randomUser(jwtIssuerAndRealm.issuer()); final SecureString jwtJwks1 = this.randomJwt(jwtIssuerAndRealm, user); final SecureString clientSecret = JwtRealmInspector.getClientAuthenticationSharedSecret(jwtIssuerAndRealm.realm()); - final MinMax jwtAuthcRange = new MinMax(2, 3); - this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks1, clientSecret, jwtAuthcRange); + final int jwtAuthcCount = randomIntBetween(2, 3); + this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks1, clientSecret, jwtAuthcCount); // Details about first JWT using the JWT issuer original JWKs final String jwt1JwksAlg = SignedJWT.parse(jwtJwks1.toString()).getHeader().getAlgorithm().getName(); @@ -138,7 +138,7 @@ public void testJwkSetUpdates() throws Exception { LOGGER.debug("JWKs 1 emptied, algs=[{}]", String.join(",", jwtIssuerAndRealm.issuer().algorithmsAll)); // Original JWT continues working, because JWT realm cached old JWKs in memory. - this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks1, clientSecret, jwtAuthcRange); + this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks1, clientSecret, jwtAuthcCount); LOGGER.debug("JWT 1 still worked, because JWT realm has old JWKs cached in memory"); // Restore original JWKs 1 into the JWT issuer. @@ -148,7 +148,7 @@ public void testJwkSetUpdates() throws Exception { LOGGER.debug("JWKs 1 restored, algs=[{}]", String.join(",", jwtIssuerAndRealm.issuer().algorithmsAll)); // Original JWT continues working, because JWT realm cached old JWKs in memory. - this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks1, clientSecret, jwtAuthcRange); + this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks1, clientSecret, jwtAuthcCount); LOGGER.debug("JWT 1 still worked, because JWT realm has old JWKs cached in memory"); // Generate a replacement set of JWKs 2 for the JWT issuer. @@ -164,7 +164,7 @@ public void testJwkSetUpdates() throws Exception { // Original JWT continues working, because JWT realm still has original JWKs cached in memory. // - jwtJwks1(PKC): Pass (Original PKC JWKs are still in the realm) // - jwtJwks1(HMAC): Pass (Original HMAC JWKs are still in the realm) - this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks1, clientSecret, jwtAuthcRange); + this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks1, clientSecret, jwtAuthcCount); LOGGER.debug("JWT 1 still worked, because JWT realm has old JWKs cached in memory"); // Create a JWT using the new JWKs. @@ -177,7 +177,7 @@ public void testJwkSetUpdates() throws Exception { // - jwtJwks2(PKC): PKC reload triggered and loaded new JWKs, so PASS // - jwtJwks2(HMAC): HMAC reload triggered but it is a no-op, so FAIL if (isPkcJwtJwks2) { - this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks2, clientSecret, jwtAuthcRange); + this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks2, clientSecret, jwtAuthcCount); LOGGER.debug("PKC JWT 2 worked with JWKs 2"); } else { this.verifyAuthenticateFailureHelper(jwtIssuerAndRealm, jwtJwks2, clientSecret); @@ -190,7 +190,7 @@ public void testJwkSetUpdates() throws Exception { // - jwtJwks2(HMAC): HMAC reload triggered but it is a no-op, jwtJwks1(PKC): PKC reload not triggered, so PASS // - jwtJwks2(HMAC): HMAC reload triggered but it is a no-op, jwtJwks1(HMAC): HMAC reload not triggered, so PASS if (isPkcJwtJwks1 == false || isPkcJwtJwks2 == false) { - this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks1, clientSecret, jwtAuthcRange); + this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks1, clientSecret, jwtAuthcCount); } else { this.verifyAuthenticateFailureHelper(jwtIssuerAndRealm, jwtJwks1, clientSecret); } @@ -202,7 +202,7 @@ public void testJwkSetUpdates() throws Exception { // New JWT continues working because JWT realm will end up with PKC JWKs 2 and HMAC JWKs 1 in memory if (isPkcJwtJwks2) { - this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks2, clientSecret, jwtAuthcRange); + this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks2, clientSecret, jwtAuthcCount); } else { this.verifyAuthenticateFailureHelper(jwtIssuerAndRealm, jwtJwks2, clientSecret); } @@ -211,7 +211,7 @@ public void testJwkSetUpdates() throws Exception { // - jwtJwks1(HMAC): HMAC reload not triggered, so PASS // - jwtJwks1(PKC): PKC reload triggered and loaded new JWKs, so FAIL if (isPkcJwtJwks1 == false || isPkcJwtJwks2 == false) { - this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks1, clientSecret, jwtAuthcRange); + this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks1, clientSecret, jwtAuthcCount); } else { this.verifyAuthenticateFailureHelper(jwtIssuerAndRealm, jwtJwks1, clientSecret); } @@ -222,7 +222,7 @@ public void testJwkSetUpdates() throws Exception { // - jwtJwks1(PKC) + jwtJwks2(HMAC): If second JWT is HMAC, it always fails because HMAC reload not supported. // - jwtJwks1(HMAC) + jwtJwks2(HMAC): If second JWT is HMAC, it always fails because HMAC reload not supported. if (isPkcJwtJwks1 == false && isPkcJwtJwks2) { - this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks2, clientSecret, jwtAuthcRange); + this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks2, clientSecret, jwtAuthcCount); } else { this.verifyAuthenticateFailureHelper(jwtIssuerAndRealm, jwtJwks2, clientSecret); } @@ -238,12 +238,12 @@ public void testJwkSetUpdates() throws Exception { // - jwtJwks2(HMAC): Fail (Triggers HMAC reload, but it is a no-op), jwtJwks1(PKC): Fail (Triggers PKC reload, gets new PKC JWKs) // - jwtJwks2(HMAC): Fail (Triggers HMAC reload, but it is a no-op), jwtJwks1(HMAC): Pass (HMAC reload was a no-op) if (isPkcJwtJwks2) { - this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks2, clientSecret, jwtAuthcRange); + this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks2, clientSecret, jwtAuthcCount); } else { this.verifyAuthenticateFailureHelper(jwtIssuerAndRealm, jwtJwks2, clientSecret); } if (isPkcJwtJwks1 == false || isPkcJwtJwks2 == false) { - this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks1, clientSecret, jwtAuthcRange); + this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwtJwks1, clientSecret, jwtAuthcCount); } else { this.verifyAuthenticateFailureHelper(jwtIssuerAndRealm, jwtJwks1, clientSecret); } @@ -256,13 +256,13 @@ public void testJwkSetUpdates() throws Exception { public void testJwtAuthcRealmAuthcAuthzWithAuthzRealms() throws Exception { this.jwtIssuerAndRealms = this.generateJwtIssuerRealmPairs( this.createJwtRealmsSettingsBuilder(), - new MinMax(1, 3), // realmsRange - new MinMax(1, 3), // authzRange - new MinMax(1, JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS.size()), // algsRange - new MinMax(1, 3), // audiencesRange - new MinMax(1, 3), // usersRange - new MinMax(0, 3), // rolesRange - new MinMax(0, 1), // jwtCacheSizeRange + randomIntBetween(1, 3), // realmsRange + randomIntBetween(1, 3), // authzRange + randomIntBetween(1, JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS.size()), // algsRange + randomIntBetween(1, 3), // audiencesRange + randomIntBetween(1, 3), // usersRange + randomIntBetween(0, 3), // rolesRange + randomIntBetween(0, 1), // jwtCacheSizeRange randomBoolean() // createHttpsServer ); final JwtIssuerAndRealm jwtIssuerAndRealm = this.randomJwtIssuerRealmPair(); @@ -271,8 +271,8 @@ public void testJwtAuthcRealmAuthcAuthzWithAuthzRealms() throws Exception { final User user = this.randomUser(jwtIssuerAndRealm.issuer()); final SecureString jwt = this.randomJwt(jwtIssuerAndRealm, user); final SecureString clientSecret = JwtRealmInspector.getClientAuthenticationSharedSecret(jwtIssuerAndRealm.realm()); - final MinMax jwtAuthcRange = new MinMax(2, 3); - this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, clientSecret, jwtAuthcRange); + final int jwtAuthcCount = randomIntBetween(2, 3); + this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, clientSecret, jwtAuthcCount); // After the above success path test, do a negative path test for an authc user that does not exist in any authz realm. // In other words, above the `user` was found in an authz realm, but below `otherUser` will not be found in any authz realm. @@ -336,23 +336,23 @@ public void testPkcJwkSetUrlNotFound() throws Exception { public void testJwtValidationFailures() throws Exception { this.jwtIssuerAndRealms = this.generateJwtIssuerRealmPairs( this.createJwtRealmsSettingsBuilder(), - new MinMax(1, 1), // realmsRange - new MinMax(0, 0), // authzRange - new MinMax(1, JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS.size()), // algsRange - new MinMax(1, 1), // audiencesRange - new MinMax(1, 1), // usersRange - new MinMax(1, 1), // rolesRange - new MinMax(0, 1), // jwtCacheSizeRange + randomIntBetween(1, 1), // realmsRange + randomIntBetween(0, 0), // authzRange + randomIntBetween(1, JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS.size()), // algsRange + randomIntBetween(1, 1), // audiencesRange + randomIntBetween(1, 1), // usersRange + randomIntBetween(1, 1), // rolesRange + randomIntBetween(0, 1), // jwtCacheSizeRange randomBoolean() // createHttpsServer ); final JwtIssuerAndRealm jwtIssuerAndRealm = this.randomJwtIssuerRealmPair(); final User user = this.randomUser(jwtIssuerAndRealm.issuer()); final SecureString jwt = this.randomJwt(jwtIssuerAndRealm, user); final SecureString clientSecret = JwtRealmInspector.getClientAuthenticationSharedSecret(jwtIssuerAndRealm.realm()); - final MinMax jwtAuthcRange = new MinMax(2, 3); + final int jwtAuthcCount = randomIntBetween(2, 3); // Indirectly verify authentication works before performing any failure scenarios - this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, clientSecret, jwtAuthcRange); + this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, clientSecret, jwtAuthcCount); // The above confirmed JWT realm authc/authz is working. // Now perform negative path tests to confirm JWT validation rejects invalid JWTs for different scenarios. @@ -544,7 +544,7 @@ public void testSameIssuerTwoRealmsDifferentClientSecrets() throws Exception { final User user = this.randomUser(jwtIssuerAndRealm.issuer()); final SecureString jwt = this.randomJwt(jwtIssuerAndRealm, user); final SecureString clientSecret = JwtRealmInspector.getClientAuthenticationSharedSecret(jwtIssuerAndRealm.realm()); - final MinMax jwtAuthcRange = new MinMax(2, 3); - this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, clientSecret, jwtAuthcRange); + final int jwtAuthcCount = randomIntBetween(2, 3); + this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, clientSecret, jwtAuthcCount); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmGenerateTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmGenerateTests.java index a6f4586f2588..7907d34301b2 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmGenerateTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmGenerateTests.java @@ -50,7 +50,7 @@ public class JwtRealmGenerateTests extends JwtRealmTestCase { private static final Logger LOGGER = LogManager.getLogger(JwtRealmGenerateTests.class); - private static final MinMax JWT_AUTHC_RANGE_1 = new MinMax(1, 1); + private static final int JWT_AUTHC_REPEATS_1 = 1; private static final Date DATE_2000_1_1 = Date.from(ZonedDateTime.of(2000, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC).toInstant()); private static final Date DATE_2099_1_1 = Date.from(ZonedDateTime.of(2099, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC).toInstant()); @@ -147,7 +147,7 @@ public void testCreateJwtSmokeTestRealm() throws Exception { assertThat(JwtValidateUtil.verifyJwt(algJwkPairHmac.jwk(), SignedJWT.parse(jwt.toString())), is(equalTo(true))); // Verify authc+authz, then print all artifacts - super.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, clientSecret, JWT_AUTHC_RANGE_1); + super.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, clientSecret, JWT_AUTHC_REPEATS_1); this.printArtifacts(jwtIssuer, config, clientSecret, jwt); } @@ -239,7 +239,7 @@ public void testCreateJwtIntegrationTestRealm1() throws Exception { assertThat(JwtValidateUtil.verifyJwt(algJwkPairPkc.jwk(), SignedJWT.parse(jwt.toString())), is(equalTo(true))); // Verify authc+authz, then print all artifacts - super.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, null, JWT_AUTHC_RANGE_1); + super.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, null, JWT_AUTHC_REPEATS_1); this.printArtifacts(jwtIssuer, config, null, jwt); } @@ -345,7 +345,7 @@ public void testCreateJwtIntegrationTestRealm2() throws Exception { assertThat(JwtValidateUtil.verifyJwt(algJwkPairHmac.jwk(), SignedJWT.parse(jwt.toString())), is(equalTo(true))); // Verify authc+authz, then print all artifacts - super.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, clientSecret, JWT_AUTHC_RANGE_1); + super.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, clientSecret, JWT_AUTHC_REPEATS_1); this.printArtifacts(jwtIssuer, config, clientSecret, jwt); } @@ -442,7 +442,7 @@ public void testCreateJwtIntegrationTestRealm3() throws Exception { assertThat(JwtValidateUtil.verifyJwt(selectedHmac.jwk(), SignedJWT.parse(jwt.toString())), is(equalTo(true))); // Verify authc+authz, then print all artifacts - super.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, clientSecret, JWT_AUTHC_RANGE_1); + super.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, clientSecret, JWT_AUTHC_REPEATS_1); this.printArtifacts(jwtIssuer, config, clientSecret, jwt); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmInspector.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmInspector.java index 3e0cccad7b9c..7697849179ac 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmInspector.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmInspector.java @@ -20,6 +20,10 @@ class JwtRealmInspector { private JwtRealmInspector() {} + public static JwtRealmSettings.TokenType getTokenType(JwtRealm realm) { + return realm.getConfig().getSetting(JwtRealmSettings.TOKEN_TYPE); + } + public static String getJwkSetPath(JwtRealm realm) { return realm.getConfig().getSetting(JwtRealmSettings.PKC_JWKSET_PATH); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmSettingsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmSettingsTests.java index 49d38038d24c..4a32ba78ef6b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmSettingsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmSettingsTests.java @@ -10,6 +10,7 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.Strings; import org.elasticsearch.core.TimeValue; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; @@ -428,4 +429,110 @@ public void testTokenTypeSetting() { ); assertThat(e.getMessage(), containsString("Invalid value")); } + + public void testFallbackClaimSettingsNotAllowedForIdTokenType() { + final String realmName = randomAlphaOfLengthBetween(3, 8); + final Settings.Builder settingsBuilder = Settings.builder(); + if (randomBoolean()) { + settingsBuilder.put( + RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.TOKEN_TYPE), + JwtRealmSettings.TokenType.ID_TOKEN.value() + ); + } + settingsBuilder.put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.FALLBACK_SUB_CLAIM), randomAlphaOfLength(8)) + .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.FALLBACK_AUD_CLAIM), randomAlphaOfLength(8)); + + final RealmConfig realmConfig = buildRealmConfig(JwtRealmSettings.TYPE, realmName, settingsBuilder.build(), randomInt()); + + final IllegalArgumentException e1 = expectThrows( + IllegalArgumentException.class, + () -> realmConfig.getSetting(JwtRealmSettings.FALLBACK_SUB_CLAIM) + ); + assertThat( + e1.getMessage(), + containsString( + "fallback claim setting [" + + RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.FALLBACK_SUB_CLAIM) + + "] is not allowed when JWT realm [" + + realmName + + "] is [id_token] type" + ) + ); + + final IllegalArgumentException e2 = expectThrows( + IllegalArgumentException.class, + () -> realmConfig.getSetting(JwtRealmSettings.FALLBACK_AUD_CLAIM) + ); + assertThat( + e2.getMessage(), + containsString( + "fallback claim setting [" + + RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.FALLBACK_AUD_CLAIM) + + "] is not allowed when JWT realm [" + + realmName + + "] is [id_token] type" + ) + ); + } + + public void testFallbackSettingsForAccessTokenType() { + final String realmName = randomAlphaOfLengthBetween(3, 8); + final String fallbackSub = randomAlphaOfLength(8); + final String fallbackAud = randomAlphaOfLength(8); + final Settings settings = Settings.builder() + .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.TOKEN_TYPE), JwtRealmSettings.TokenType.ACCESS_TOKEN.value()) + .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.FALLBACK_SUB_CLAIM), fallbackSub) + .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.FALLBACK_AUD_CLAIM), fallbackAud) + .build(); + + final RealmConfig realmConfig = buildRealmConfig(JwtRealmSettings.TYPE, realmName, settings, randomInt()); + assertThat(realmConfig.getSetting(JwtRealmSettings.FALLBACK_SUB_CLAIM), equalTo(fallbackSub)); + assertThat(realmConfig.getSetting(JwtRealmSettings.FALLBACK_AUD_CLAIM), equalTo(fallbackAud)); + } + + public void testRegisteredClaimsCannotBeUsedForFallbackSettings() { + final String realmName = randomAlphaOfLengthBetween(3, 8); + final String fallbackSub = randomValueOtherThan("sub", () -> randomFrom(JwtRealmSettings.REGISTERED_CLAIM_NAMES)); + final String fallbackAud = randomValueOtherThan("aud", () -> randomFrom(JwtRealmSettings.REGISTERED_CLAIM_NAMES)); + final Settings settings = Settings.builder() + .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.TOKEN_TYPE), JwtRealmSettings.TokenType.ACCESS_TOKEN.value()) + .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.FALLBACK_SUB_CLAIM), fallbackSub) + .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.FALLBACK_AUD_CLAIM), fallbackAud) + .build(); + + final RealmConfig realmConfig = buildRealmConfig(JwtRealmSettings.TYPE, realmName, settings, randomInt()); + + final IllegalArgumentException e1 = expectThrows( + IllegalArgumentException.class, + () -> realmConfig.getSetting(JwtRealmSettings.FALLBACK_SUB_CLAIM) + ); + assertThat( + e1.getMessage(), + containsString( + Strings.format( + "Invalid fallback claims setting [%s]. Claim [%s] cannot fallback to a registered claim [%s]", + RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.FALLBACK_SUB_CLAIM), + "sub", + fallbackSub + ) + ) + ); + + final IllegalArgumentException e2 = expectThrows( + IllegalArgumentException.class, + () -> realmConfig.getSetting(JwtRealmSettings.FALLBACK_AUD_CLAIM) + ); + assertThat( + e2.getMessage(), + containsString( + Strings.format( + "Invalid fallback claims setting [%s]. Claim [%s] cannot fallback to a registered claim [%s]", + RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.FALLBACK_AUD_CLAIM), + "aud", + fallbackAud + ) + ) + ); + } + } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmTestCase.java index d6f170861483..4f7e7b57d9b9 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmTestCase.java @@ -59,7 +59,6 @@ import static org.hamcrest.Matchers.anEmptyMap; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.is; @@ -80,12 +79,6 @@ record JwtRealmSettingsBuilder(String name, Settings.Builder settingsBuilder) {} record JwtIssuerAndRealm(JwtIssuer issuer, JwtRealm realm, JwtRealmSettingsBuilder realmSettingsBuilder) {} - record MinMax(int min, int max) { - MinMax { - assert min >= 0 && max >= min : "Invalid min=" + min + " max=" + max; - } - } - protected ThreadPool threadPool; protected ResourceWatcherService resourceWatcherService; protected MockLicenseState licenseState; @@ -130,35 +123,20 @@ protected JwtRealmsService generateJwtRealmsService(final JwtRealmsServiceSettin protected List generateJwtIssuerRealmPairs( final JwtRealmsServiceSettingsBuilder jwtRealmsServiceSettingsBuilder, - final MinMax realmsRange, - final MinMax authzRange, - final MinMax algsRange, - final MinMax audiencesRange, - final MinMax usersRange, - final MinMax rolesRange, - final MinMax jwtCacheSizeRange, + final int realmsCount, + final int authzCount, + final int algsCount, + final int audiencesCount, + final int usersCount, + final int rolesCount, + final int jwtCacheSize, final boolean createHttpsServer ) throws Exception { - assertThat(realmsRange.min(), is(greaterThanOrEqualTo(1))); - assertThat(authzRange.min(), is(greaterThanOrEqualTo(0))); - assertThat(algsRange.min(), is(greaterThanOrEqualTo(1))); - assertThat(audiencesRange.min(), is(greaterThanOrEqualTo(1))); - assertThat(usersRange.min(), is(greaterThanOrEqualTo(1))); - assertThat(rolesRange.min(), is(greaterThanOrEqualTo(0))); - assertThat(jwtCacheSizeRange.min(), is(greaterThanOrEqualTo(0))); - // Create JWT authc realms and mocked authz realms. Initialize each JWT realm, and test ensureInitialized() before and after. final JwtRealmsService jwtRealmsService = this.generateJwtRealmsService(jwtRealmsServiceSettingsBuilder); - final int realmsCount = randomIntBetween(realmsRange.min(), realmsRange.max()); final List allRealms = new ArrayList<>(); // authc and authz realms this.jwtIssuerAndRealms = new ArrayList<>(realmsCount); for (int i = 0; i < realmsCount; i++) { - final int authzCount = randomIntBetween(authzRange.min(), authzRange.max()); - final int algsCount = randomIntBetween(algsRange.min(), algsRange.max()); - final int audiencesCount = randomIntBetween(audiencesRange.min(), audiencesRange.max()); - final int usersCount = randomIntBetween(usersRange.min(), usersRange.max()); - final int rolesCount = randomIntBetween(rolesRange.min(), rolesRange.max()); - final int jwtCacheSize = randomIntBetween(jwtCacheSizeRange.min(), jwtCacheSizeRange.max()); final JwtIssuer jwtIssuer = this.createJwtIssuer( i, @@ -444,15 +422,13 @@ protected void doMultipleAuthcAuthzAndVerifySuccess( final User user, final SecureString jwt, final SecureString sharedSecret, - final MinMax jwtAuthcRange + final int jwtAuthcRepeats ) throws Exception { - assertThat(jwtAuthcRange.min(), is(greaterThanOrEqualTo(1))); // Select one JWT authc Issuer/Realm pair. Select one test user, to use inside the authc test loop. final List jwtRealmsList = this.jwtIssuerAndRealms.stream().map(p -> p.realm).toList(); // Select different test JWKs from the JWT realm, and generate test JWTs for the test user. Run the JWT through the chain. - final int jwtAuthcRepeats = randomIntBetween(jwtAuthcRange.min(), jwtAuthcRange.max()); for (int authcRun = 1; authcRun <= jwtAuthcRepeats; authcRun++) { // Create request with headers set final ThreadContext requestThreadContext = super.createThreadContext(jwt, sharedSecret); @@ -571,10 +547,13 @@ protected void doMultipleAuthcAuthzAndVerifySuccess( if (jwtRealm.delegatedAuthorizationSupport.hasDelegation()) { assertThat(user.metadata(), is(equalTo(authenticatedUser.metadata()))); // delegated authz returns user's metadata } else if (JwtRealmInspector.shouldPopulateUserMetadata(jwtRealm)) { - assertThat(authenticatedUser.metadata(), hasEntry("jwt_token_type", "id_token")); + assertThat(authenticatedUser.metadata(), hasEntry("jwt_token_type", JwtRealmInspector.getTokenType(jwtRealm).value())); assertThat(authenticatedUser.metadata(), hasKey(startsWith("jwt_claim_"))); } else { - assertThat(authenticatedUser.metadata(), equalTo(Map.of("jwt_token_type", "id_token"))); + assertThat( + authenticatedUser.metadata(), + equalTo(Map.of("jwt_token_type", JwtRealmInspector.getTokenType(jwtRealm).value())) + ); } } catch (Throwable t) { realmFailureExceptions.forEach(t::addSuppressed); // all previous realm exceptions @@ -625,7 +604,7 @@ protected SecureString randomJwt(final JwtIssuerAndRealm jwtIssuerAndRealm, User randomAlphaOfLengthBetween(10, 20), // jwtID JwtRealmInspector.getAllowedIssuer(jwtIssuerAndRealm.realm), // iss JwtRealmInspector.getAllowedAudiences(jwtIssuerAndRealm.realm), // aud - randomBoolean() ? null : randomBoolean() ? user.principal() : user.principal() + "_" + randomInt(9), // sub claim value + randomBoolean() ? user.principal() : user.principal() + "_" + randomInt(9), // sub claim value JwtRealmInspector.getPrincipalClaimName(jwtIssuerAndRealm.realm), // principal claim name user.principal(), // principal claim value JwtRealmInspector.getGroupsClaimName(jwtIssuerAndRealm.realm), // group claim name diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidatorTests.java index 33383cf6f6b1..fe49a6196be7 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidatorTests.java @@ -10,7 +10,6 @@ import com.nimbusds.jose.JWSHeader; import com.nimbusds.jwt.JWTClaimsSet; -import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.test.ESTestCase; import java.text.ParseException; @@ -23,12 +22,22 @@ public class JwtStringClaimValidatorTests extends ESTestCase { public void testClaimIsNotString() throws ParseException { - final String claimName = randomAlphaOfLengthBetween(10, 18); - final JwtStringClaimValidator validator = new JwtStringClaimValidator(claimName, List.of(), randomBoolean()); + final String claimName = randomAlphaOfLength(10); + final String fallbackClaimName = randomAlphaOfLength(12); + + final JwtStringClaimValidator validator; + final JWTClaimsSet jwtClaimsSet; + if (randomBoolean()) { + validator = new JwtStringClaimValidator(claimName, List.of(), randomBoolean()); + // fallback claim is ignored + jwtClaimsSet = JWTClaimsSet.parse(Map.of(claimName, List.of(42), fallbackClaimName, randomAlphaOfLength(8))); + } else { + validator = new JwtStringClaimValidator(claimName, Map.of(claimName, fallbackClaimName), List.of(), randomBoolean()); + jwtClaimsSet = JWTClaimsSet.parse(Map.of(fallbackClaimName, List.of(42))); + } - final JWTClaimsSet jwtClaimsSet = JWTClaimsSet.parse(Map.of(claimName, List.of(42))); - final ElasticsearchSecurityException e = expectThrows( - ElasticsearchSecurityException.class, + final IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, () -> validator.validate(getJwsHeader(), jwtClaimsSet) ); assertThat(e.getMessage(), containsString("cannot parse string claim")); @@ -37,11 +46,21 @@ public void testClaimIsNotString() throws ParseException { public void testClaimIsNotSingleValued() throws ParseException { final String claimName = randomAlphaOfLengthBetween(10, 18); - final JwtStringClaimValidator validator = new JwtStringClaimValidator(claimName, List.of(), true); + final String fallbackClaimName = randomAlphaOfLength(12); + + final JwtStringClaimValidator validator; + final JWTClaimsSet jwtClaimsSet; + if (randomBoolean()) { + validator = new JwtStringClaimValidator(claimName, List.of(), true); + // fallback claim is ignored + jwtClaimsSet = JWTClaimsSet.parse(Map.of(claimName, List.of("foo", "bar"), fallbackClaimName, randomAlphaOfLength(8))); + } else { + validator = new JwtStringClaimValidator(claimName, Map.of(claimName, fallbackClaimName), List.of(), true); + jwtClaimsSet = JWTClaimsSet.parse(Map.of(fallbackClaimName, List.of("foo", "bar"))); + } - final JWTClaimsSet jwtClaimsSet = JWTClaimsSet.parse(Map.of(claimName, List.of("foo", "bar"))); - final ElasticsearchSecurityException e = expectThrows( - ElasticsearchSecurityException.class, + final IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, () -> validator.validate(getJwsHeader(), jwtClaimsSet) ); assertThat(e.getMessage(), containsString("cannot parse string claim")); @@ -50,11 +69,19 @@ public void testClaimIsNotSingleValued() throws ParseException { public void testClaimDoesNotExist() throws ParseException { final String claimName = randomAlphaOfLengthBetween(10, 18); - final JwtStringClaimValidator validator = new JwtStringClaimValidator(claimName, List.of(), randomBoolean()); + final String fallbackClaimName = randomAlphaOfLength(12); + + final JwtStringClaimValidator validator; + final JWTClaimsSet jwtClaimsSet; + if (randomBoolean()) { + validator = new JwtStringClaimValidator(claimName, List.of(), randomBoolean()); + } else { + validator = new JwtStringClaimValidator(claimName, Map.of(claimName, fallbackClaimName), List.of(), randomBoolean()); + } + jwtClaimsSet = JWTClaimsSet.parse(Map.of()); - final JWTClaimsSet jwtClaimsSet = JWTClaimsSet.parse(Map.of()); - final ElasticsearchSecurityException e = expectThrows( - ElasticsearchSecurityException.class, + final IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, () -> validator.validate(getJwsHeader(), jwtClaimsSet) ); assertThat(e.getMessage(), containsString("missing required string claim")); @@ -62,26 +89,40 @@ public void testClaimDoesNotExist() throws ParseException { public void testMatchingClaimValues() throws ParseException { final String claimName = randomAlphaOfLengthBetween(10, 18); + final String fallbackClaimName = randomAlphaOfLength(12); final String claimValue = randomAlphaOfLength(10); final boolean singleValuedClaim = randomBoolean(); - final JwtStringClaimValidator validator = new JwtStringClaimValidator( - claimName, - List.of(claimValue, randomAlphaOfLengthBetween(11, 20)), - singleValuedClaim - ); + final List allowedClaimValues = List.of(claimValue, randomAlphaOfLengthBetween(11, 20)); + final Object incomingClaimValue = singleValuedClaim ? claimValue : randomFrom(claimValue, List.of(claimValue, "other-stuff")); + + final JwtStringClaimValidator validator; + final JWTClaimsSet validJwtClaimsSet; + final boolean noFallback = randomBoolean(); + if (noFallback) { + validator = new JwtStringClaimValidator(claimName, allowedClaimValues, singleValuedClaim); + // fallback claim is ignored + validJwtClaimsSet = JWTClaimsSet.parse(Map.of(claimName, incomingClaimValue, fallbackClaimName, List.of(42))); + } else { + validator = new JwtStringClaimValidator(claimName, Map.of(claimName, fallbackClaimName), allowedClaimValues, randomBoolean()); + validJwtClaimsSet = JWTClaimsSet.parse(Map.of(fallbackClaimName, incomingClaimValue)); + } - final JWTClaimsSet validJwtClaimsSet = JWTClaimsSet.parse( - Map.of(claimName, singleValuedClaim ? claimValue : randomFrom(claimValue, List.of(claimValue, "other-stuff"))) - ); try { validator.validate(getJwsHeader(), validJwtClaimsSet); } catch (Exception e) { throw new AssertionError("validation should have passed without exception", e); } - final JWTClaimsSet invalidJwtClaimsSet = JWTClaimsSet.parse(Map.of(claimName, "not-" + claimValue)); - final ElasticsearchSecurityException e = expectThrows( - ElasticsearchSecurityException.class, + final JWTClaimsSet invalidJwtClaimsSet; + if (noFallback) { + // fallback is ignored (even when it has a valid value) since the main claim exists + invalidJwtClaimsSet = JWTClaimsSet.parse(Map.of(claimName, "not-" + claimValue, fallbackClaimName, claimValue)); + } else { + invalidJwtClaimsSet = JWTClaimsSet.parse(Map.of(fallbackClaimName, "not-" + claimValue)); + } + + final IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, () -> validator.validate(getJwsHeader(), invalidJwtClaimsSet) ); assertThat(e.getMessage(), containsString("does not match allowed claim values")); @@ -89,19 +130,36 @@ public void testMatchingClaimValues() throws ParseException { public void testDoesNotSupportWildcardOrRegex() throws ParseException { final String claimName = randomAlphaOfLengthBetween(10, 18); + final String fallbackClaimName = randomAlphaOfLength(12); final String claimValue = randomFrom("*", "/.*/"); - final JwtStringClaimValidator validator = new JwtStringClaimValidator(claimName, List.of(claimValue), randomBoolean()); + + final JwtStringClaimValidator validator; + final JWTClaimsSet invalidJwtClaimsSet; + final boolean noFallback = randomBoolean(); + if (noFallback) { + validator = new JwtStringClaimValidator(claimName, List.of(claimValue), randomBoolean()); + // fallback is ignored (even when it has a valid value) since the main claim exists + invalidJwtClaimsSet = JWTClaimsSet.parse(Map.of(claimName, randomAlphaOfLengthBetween(1, 10), fallbackClaimName, claimValue)); + } else { + validator = new JwtStringClaimValidator(claimName, Map.of(claimName, fallbackClaimName), List.of(claimValue), randomBoolean()); + invalidJwtClaimsSet = JWTClaimsSet.parse(Map.of(fallbackClaimName, randomAlphaOfLengthBetween(1, 10))); + } // It should not match arbitrary claim value because wildcard or regex is not supported - final JWTClaimsSet invalidJwtClaimsSet = JWTClaimsSet.parse(Map.of(claimName, randomAlphaOfLengthBetween(1, 10))); - final ElasticsearchSecurityException e = expectThrows( - ElasticsearchSecurityException.class, + final IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, () -> validator.validate(getJwsHeader(), invalidJwtClaimsSet) ); assertThat(e.getMessage(), containsString("does not match allowed claim values")); // It should support literal matching - final JWTClaimsSet validJwtClaimsSet = JWTClaimsSet.parse(Map.of(claimName, claimValue)); + final JWTClaimsSet validJwtClaimsSet; + if (noFallback) { + // fallback claim is ignored + validJwtClaimsSet = JWTClaimsSet.parse(Map.of(claimName, claimValue, fallbackClaimName, randomAlphaOfLength(10))); + } else { + validJwtClaimsSet = JWTClaimsSet.parse(Map.of(fallbackClaimName, claimValue)); + } try { validator.validate(getJwsHeader(), validJwtClaimsSet); } catch (Exception e2) { @@ -109,6 +167,23 @@ public void testDoesNotSupportWildcardOrRegex() throws ParseException { } } + public void testAllowAllSubjects() { + try { + JwtStringClaimValidator.ALLOW_ALL_SUBJECTS.validate( + getJwsHeader(), + JWTClaimsSet.parse(Map.of("sub", randomAlphaOfLengthBetween(1, 10))) + ); + } catch (Exception e) { + throw new AssertionError("validation should have passed without exception", e); + } + + final IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, + () -> JwtStringClaimValidator.ALLOW_ALL_SUBJECTS.validate(getJwsHeader(), JWTClaimsSet.parse(Map.of())) + ); + assertThat(e.getMessage(), containsString("missing required string claim")); + } + private JWSHeader getJwsHeader() throws ParseException { return JWSHeader.parse(Map.of("alg", randomAlphaOfLengthBetween(3, 8))); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtTypeValidatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtTypeValidatorTests.java index 2b491a84c8cc..7de72577970d 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtTypeValidatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtTypeValidatorTests.java @@ -10,7 +10,6 @@ import com.nimbusds.jose.JWSHeader; import com.nimbusds.jwt.JWTClaimsSet; -import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.test.ESTestCase; import java.text.ParseException; @@ -41,8 +40,8 @@ public void testInvalidType() throws ParseException { Map.of("typ", randomAlphaOfLengthBetween(4, 8), "alg", randomAlphaOfLengthBetween(3, 8)) ); - final ElasticsearchSecurityException e = expectThrows( - ElasticsearchSecurityException.class, + final IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, () -> JwtTypeValidator.INSTANCE.validate(jwsHeader, JWTClaimsSet.parse(Map.of())) ); assertThat(e.getMessage(), containsString("invalid jwt typ header")); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmTests.java index e644cedd2361..273f8e4c111e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectRealmTests.java @@ -133,8 +133,8 @@ public void testClaimPropertyMapping() throws Exception { Exception.class, () -> authenticateWithOidc(principal, roleMapper, false, false, REALM_NAME, claimsWithNumber) ); - assertThat(e.getCause().getMessage(), containsString("expects a claim with String or a String Array value")); - assertThat(e2.getCause().getMessage(), containsString("expects a claim with String or a String Array value")); + assertThat(e.getCause().getMessage(), containsString("expects claim [groups] with String or a String Array value")); + assertThat(e2.getCause().getMessage(), containsString("expects claim [groups] with String or a String Array value")); } public void testClaimMetadataMapping() throws Exception { From fb756a9ca5bcd30c435bde1bfb82f5b82278b6bb Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 12 Dec 2022 11:56:07 +0000 Subject: [PATCH 225/919] Include last-committed data in publication (#92259) The cluster coordination consistency layer relies on a couple of fields within `Metadata` which record the last _committed_ values on each node. In contrast, the rest of the cluster state can only be changed at _accept_ time. In the past we would copy these fields over from the master on every publication, but since #90101 we don't copy anything at all if the `Metadata` is unchanged on the master. However, the master computes the diff against the last _committed_ state whereas the receiving nodes apply the diff to the last _accepted_ state, and this means if the master sends a no-op `Metadata` diff then the receiving node will revert its last-committed values to the ones included in the state it last accepted. With this commit we include the last-committed values alongside the cluster state diff so that they are always copied properly. Closes #90158 --- docs/changelog/92259.yaml | 6 + .../PublicationTransportHandler.java | 31 +++- .../cluster/metadata/Metadata.java | 43 ++++- .../coordination/CoordinatorTests.java | 1 - .../PublicationTransportHandlerTests.java | 174 ++++++++++++++++++ 5 files changed, 251 insertions(+), 4 deletions(-) create mode 100644 docs/changelog/92259.yaml diff --git a/docs/changelog/92259.yaml b/docs/changelog/92259.yaml new file mode 100644 index 000000000000..826d63dedf7f --- /dev/null +++ b/docs/changelog/92259.yaml @@ -0,0 +1,6 @@ +pr: 92259 +summary: Include last-committed data in publication +area: Cluster Coordination +type: bug +issues: + - 90158 diff --git a/server/src/main/java/org/elasticsearch/cluster/coordination/PublicationTransportHandler.java b/server/src/main/java/org/elasticsearch/cluster/coordination/PublicationTransportHandler.java index 5dd308640cc9..11d64f162241 100644 --- a/server/src/main/java/org/elasticsearch/cluster/coordination/PublicationTransportHandler.java +++ b/server/src/main/java/org/elasticsearch/cluster/coordination/PublicationTransportHandler.java @@ -86,6 +86,8 @@ public class PublicationTransportHandler { TransportRequestOptions.Type.STATE ); + public static final Version INCLUDES_LAST_COMMITTED_DATA_VERSION = Version.V_8_7_0; + private final SerializationStatsTracker serializationStatsTracker = new SerializationStatsTracker(); public PublicationTransportHandler( @@ -131,6 +133,7 @@ private PublishWithJoinResponse handleIncomingPublishRequest(BytesTransportReque // Close early to release resources used by the de-compression as early as possible try (StreamInput input = in) { incomingState = ClusterState.readFrom(input, transportService.getLocalNode()); + assert input.read() == -1; } catch (Exception e) { logger.warn("unexpected error while deserializing an incoming cluster state", e); assert false : e; @@ -151,11 +154,30 @@ private PublishWithJoinResponse handleIncomingPublishRequest(BytesTransportReque ClusterState incomingState; try { final Diff diff; + final boolean includesLastCommittedData = request.version().onOrAfter(INCLUDES_LAST_COMMITTED_DATA_VERSION); + final boolean clusterUuidCommitted; + final CoordinationMetadata.VotingConfiguration lastCommittedConfiguration; + // Close stream early to release resources used by the de-compression as early as possible try (StreamInput input = in) { diff = ClusterState.readDiffFrom(input, lastSeen.nodes().getLocalNode()); + if (includesLastCommittedData) { + clusterUuidCommitted = in.readBoolean(); + lastCommittedConfiguration = new CoordinationMetadata.VotingConfiguration(in); + } else { + clusterUuidCommitted = false; + lastCommittedConfiguration = null; + } + assert input.read() == -1; } incomingState = diff.apply(lastSeen); // might throw IncompatibleClusterStateVersionException + if (includesLastCommittedData) { + final var adjustedMetadata = incomingState.metadata() + .withLastCommittedValues(clusterUuidCommitted, lastCommittedConfiguration); + if (adjustedMetadata != incomingState.metadata()) { + incomingState = ClusterState.builder(incomingState).metadata(adjustedMetadata).build(); + } + } } catch (IncompatibleClusterStateVersionException e) { incompatibleClusterStateDiffReceivedCount.incrementAndGet(); throw e; @@ -239,7 +261,8 @@ private ReleasableBytesReference serializeFullClusterState(ClusterState clusterS } } - private ReleasableBytesReference serializeDiffClusterState(long clusterStateVersion, Diff diff, DiscoveryNode node) { + private ReleasableBytesReference serializeDiffClusterState(ClusterState newState, Diff diff, DiscoveryNode node) { + final long clusterStateVersion = newState.version(); final Version nodeVersion = node.getVersion(); final RecyclerBytesStreamOutput bytesStream = transportService.newNetworkBytesStream(); boolean success = false; @@ -253,6 +276,10 @@ private ReleasableBytesReference serializeDiffClusterState(long clusterStateVers stream.setVersion(nodeVersion); stream.writeBoolean(false); diff.writeTo(stream); + if (nodeVersion.onOrAfter(INCLUDES_LAST_COMMITTED_DATA_VERSION)) { + stream.writeBoolean(newState.metadata().clusterUUIDCommitted()); + newState.getLastCommittedConfiguration().writeTo(stream); + } uncompressedBytes = stream.position(); } catch (IOException e) { throw new ElasticsearchException("failed to serialize cluster state diff for publishing to node {}", e, node); @@ -316,7 +343,7 @@ void buildDiffAndSerializeStates() { } else { serializedDiffs.computeIfAbsent( node.getVersion(), - v -> serializeDiffClusterState(newState.version(), diffSupplier.getOrCompute(), node) + v -> serializeDiffClusterState(newState, diffSupplier.getOrCompute(), node) ); } } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java index 7f450bf73923..f45a12b95da9 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java @@ -22,6 +22,7 @@ import org.elasticsearch.cluster.block.ClusterBlock; import org.elasticsearch.cluster.block.ClusterBlockLevel; import org.elasticsearch.cluster.coordination.CoordinationMetadata; +import org.elasticsearch.cluster.coordination.PublicationTransportHandler; import org.elasticsearch.cluster.metadata.IndexAbstraction.ConcreteIndex; import org.elasticsearch.cluster.routing.RoutingTable; import org.elasticsearch.common.Strings; @@ -442,6 +443,42 @@ public Metadata withCoordinationMetadata(CoordinationMetadata coordinationMetada ); } + public Metadata withLastCommittedValues( + boolean clusterUUIDCommitted, + CoordinationMetadata.VotingConfiguration lastCommittedConfiguration + ) { + if (clusterUUIDCommitted == this.clusterUUIDCommitted + && lastCommittedConfiguration.equals(this.coordinationMetadata.getLastCommittedConfiguration())) { + return this; + } + return new Metadata( + clusterUUID, + clusterUUIDCommitted, + version, + CoordinationMetadata.builder(coordinationMetadata).lastCommittedConfiguration(lastCommittedConfiguration).build(), + transientSettings, + persistentSettings, + settings, + hashesOfConsistentSettings, + totalNumberOfShards, + totalOpenIndexShards, + indices, + aliasedIndices, + templates, + customs, + allIndices, + visibleIndices, + allOpenIndices, + visibleOpenIndices, + allClosedIndices, + visibleClosedIndices, + indicesLookup, + mappingsByHash, + oldestIndexVersion, + reservedStateMetadata + ); + } + /** * Creates a copy of this instance updated with the given {@link IndexMetadata} that must only contain changes to primary terms * and in-sync allocation ids relative to the existing entries. This method is only used by @@ -1380,6 +1417,7 @@ public Map getMappingsByHash() { private static class MetadataDiff implements Diff { private static final Version NOOP_METADATA_DIFF_VERSION = Version.V_8_5_0; + private static final Version NOOP_METADATA_DIFF_SAFE_VERSION = PublicationTransportHandler.INCLUDES_LAST_COMMITTED_DATA_VERSION; private final long version; private final String clusterUUID; @@ -1466,12 +1504,15 @@ private MetadataDiff(StreamInput in) throws IOException { @Override public void writeTo(StreamOutput out) throws IOException { - if (out.getVersion().onOrAfter(NOOP_METADATA_DIFF_VERSION)) { + if (out.getVersion().onOrAfter(NOOP_METADATA_DIFF_SAFE_VERSION)) { out.writeBoolean(empty); if (empty) { // noop diff return; } + } else if (out.getVersion().onOrAfter(NOOP_METADATA_DIFF_VERSION)) { + // noops are not safe with these versions, see #92259 + out.writeBoolean(false); } out.writeString(clusterUUID); out.writeBoolean(clusterUUIDCommitted); diff --git a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java index e24be3b79d5a..e2dc48e6bd6e 100644 --- a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java @@ -568,7 +568,6 @@ public void testUnresponsiveLeaderDetectedEventually() { } } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/90158") public void testUnhealthyLeaderIsReplaced() { final AtomicReference nodeHealthServiceStatus = new AtomicReference<>(new StatusInfo(HEALTHY, "healthy-info")); final int initialClusterSize = between(1, 3); diff --git a/server/src/test/java/org/elasticsearch/cluster/coordination/PublicationTransportHandlerTests.java b/server/src/test/java/org/elasticsearch/cluster/coordination/PublicationTransportHandlerTests.java index 52cc92f73c74..ccd34e74c610 100644 --- a/server/src/test/java/org/elasticsearch/cluster/coordination/PublicationTransportHandlerTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/coordination/PublicationTransportHandlerTests.java @@ -16,6 +16,7 @@ import org.elasticsearch.cluster.Diff; import org.elasticsearch.cluster.IncompatibleClusterStateVersionException; import org.elasticsearch.cluster.coordination.CoordinationMetadata.VotingConfiguration; +import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodeRole; import org.elasticsearch.cluster.node.DiscoveryNodes; @@ -30,6 +31,7 @@ import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.MockPageCacheRecycler; +import org.elasticsearch.common.util.concurrent.DeterministicTaskQueue; import org.elasticsearch.core.IOUtils; import org.elasticsearch.core.Nullable; import org.elasticsearch.tasks.Task; @@ -42,17 +44,23 @@ import org.elasticsearch.transport.BytesRefRecycler; import org.elasticsearch.transport.BytesTransportRequest; import org.elasticsearch.transport.RemoteTransportException; +import org.elasticsearch.transport.TestTransportChannel; import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.transport.TransportResponse; import org.elasticsearch.transport.TransportService; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import static java.util.Collections.emptyMap; import static org.elasticsearch.cluster.service.MasterService.STATE_UPDATE_ACTION_NAME; @@ -330,4 +338,170 @@ public void writeTo(StreamOutput out) throws IOException { } } + public void testIncludesLastCommittedFieldsInDiffSerialization() { + final var deterministicTaskQueue = new DeterministicTaskQueue(); + final var threadPool = deterministicTaskQueue.getThreadPool(); + + final var transportsByNode = new HashMap(); + final var transportHandlersByNode = new HashMap(); + final var transportServicesByNode = new HashMap(); + final var receivedStateRef = new AtomicReference(); + final var completed = new AtomicBoolean(); + + final var localNode = new DiscoveryNode("localNode", buildNewFakeTransportAddress(), Version.CURRENT); + final var otherNode = new DiscoveryNode( + "otherNode", + buildNewFakeTransportAddress(), + VersionUtils.randomCompatibleVersion(random(), Version.CURRENT) + ); + for (final var discoveryNode : List.of(localNode, otherNode)) { + final var transport = new MockTransport() { + @Override + protected void onSendRequest(long requestId, String action, TransportRequest request, DiscoveryNode node) { + @SuppressWarnings("unchecked") + final var context = (ResponseContext) getResponseHandlers().remove(requestId); + try { + transportsByNode.get(node) + .getRequestHandlers() + .getHandler(action) + .getHandler() + .messageReceived(request, new TestTransportChannel(new ActionListener<>() { + @Override + public void onResponse(TransportResponse transportResponse) { + context.handler().handleResponse(transportResponse); + } + + @Override + public void onFailure(Exception e) { + throw new AssertionError("unexpected", e); + } + }), new Task(randomNonNegativeLong(), "test", "test", "", TaskId.EMPTY_TASK_ID, Map.of())); + } catch (IncompatibleClusterStateVersionException e) { + context.handler().handleException(new RemoteTransportException("wrapped", e)); + } catch (Exception e) { + throw new AssertionError("unexpected", e); + } + } + }; + transportsByNode.put(discoveryNode, transport); + + final var transportService = transport.createTransportService( + Settings.EMPTY, + threadPool, + TransportService.NOOP_TRANSPORT_INTERCEPTOR, + ignored -> discoveryNode, + null, + Set.of() + ); + transportServicesByNode.put(discoveryNode, transportService); + + final var publicationTransportHandler = new PublicationTransportHandler( + transportService, + writableRegistry(), + publishRequest -> { + assertTrue(receivedStateRef.compareAndSet(null, publishRequest.getAcceptedState())); + return new PublishWithJoinResponse( + new PublishResponse(publishRequest.getAcceptedState().term(), publishRequest.getAcceptedState().version()), + Optional.empty() + ); + } + ); + transportHandlersByNode.put(discoveryNode, publicationTransportHandler); + } + + for (final var transportService : transportServicesByNode.values()) { + transportService.start(); + transportService.acceptIncomingRequests(); + } + + threadPool.getThreadContext().markAsSystemContext(); + + final var clusterState0 = ClusterState.builder(ClusterState.EMPTY_STATE) + .nodes(DiscoveryNodes.builder().add(localNode).add(otherNode).localNodeId(localNode.getId()).masterNodeId(localNode.getId())) + .metadata( + Metadata.builder() + .coordinationMetadata( + CoordinationMetadata.builder().lastAcceptedConfiguration(VotingConfiguration.of(localNode)).build() + ) + .generateClusterUuidIfNeeded() + ) + .build(); + + final ClusterState receivedState0; + var context0 = transportHandlersByNode.get(localNode) + .newPublicationContext( + new ClusterStatePublicationEvent( + new BatchSummary("test"), + clusterState0, + clusterState0, + new Task(randomNonNegativeLong(), "test", "test", "", TaskId.EMPTY_TASK_ID, Map.of()), + 0L, + 0L + ) + ); + try { + context0.sendPublishRequest( + otherNode, + new PublishRequest(clusterState0), + ActionListener.wrap(() -> assertTrue(completed.compareAndSet(false, true))) + ); + assertTrue(completed.getAndSet(false)); + receivedState0 = receivedStateRef.getAndSet(null); + assertEquals(clusterState0.stateUUID(), receivedState0.stateUUID()); + assertEquals(otherNode, receivedState0.nodes().getLocalNode()); + assertFalse(receivedState0.metadata().clusterUUIDCommitted()); + assertEquals(VotingConfiguration.of(), receivedState0.getLastCommittedConfiguration()); + final var receivedStateStats = transportHandlersByNode.get(otherNode).stats(); + assertEquals(0, receivedStateStats.getCompatibleClusterStateDiffReceivedCount()); + assertEquals(1, receivedStateStats.getIncompatibleClusterStateDiffReceivedCount()); + assertEquals(1, receivedStateStats.getFullClusterStateReceivedCount()); + } finally { + context0.decRef(); + } + + final var committedClusterState0 = ClusterState.builder(clusterState0) + .metadata(clusterState0.metadata().withLastCommittedValues(true, clusterState0.getLastAcceptedConfiguration())) + .build(); + assertEquals(clusterState0.stateUUID(), committedClusterState0.stateUUID()); + assertEquals(clusterState0.term(), committedClusterState0.term()); + assertEquals(clusterState0.version(), committedClusterState0.version()); + + final var clusterState1 = ClusterState.builder(committedClusterState0).incrementVersion().build(); + assertSame(committedClusterState0.metadata(), clusterState1.metadata()); + + var context1 = transportHandlersByNode.get(localNode) + .newPublicationContext( + new ClusterStatePublicationEvent( + new BatchSummary("test"), + committedClusterState0, + clusterState1, + new Task(randomNonNegativeLong(), "test", "test", "", TaskId.EMPTY_TASK_ID, Map.of()), + 0L, + 0L + ) + ); + try { + context1.sendPublishRequest( + otherNode, + new PublishRequest(clusterState1), + ActionListener.wrap(() -> assertTrue(completed.compareAndSet(false, true))) + ); + assertTrue(completed.getAndSet(false)); + var receivedState1 = receivedStateRef.getAndSet(null); + assertEquals(clusterState1.stateUUID(), receivedState1.stateUUID()); + assertEquals(otherNode, receivedState1.nodes().getLocalNode()); + assertSame(receivedState0.nodes(), receivedState1.nodes()); // it was a diff + assertTrue(receivedState1.metadata().clusterUUIDCommitted()); + assertEquals(VotingConfiguration.of(localNode), receivedState1.getLastCommittedConfiguration()); + final var receivedStateStats = transportHandlersByNode.get(otherNode).stats(); + assertEquals(1, receivedStateStats.getCompatibleClusterStateDiffReceivedCount()); + assertEquals(1, receivedStateStats.getIncompatibleClusterStateDiffReceivedCount()); + assertEquals(1, receivedStateStats.getFullClusterStateReceivedCount()); + } finally { + context1.decRef(); + } + + assertFalse(deterministicTaskQueue.hasRunnableTasks()); + assertFalse(deterministicTaskQueue.hasDeferredTasks()); + } } From eddfae38c28e9ec64add3dcb71bcbe815e189378 Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Mon, 12 Dec 2022 13:28:05 +0100 Subject: [PATCH 226/919] Minor TSDB parsing speedup. (#92276) This changes addresses a few minor inefficiences on the TSDB parsing path: - The UTF-8 representation of keyword fields that are also dimensions is computed twice. - Extraction of the routing path evaluates every pattern in sequence, while it is possible to do better by converting the list of patterns to a set when there are no wildcards, or to an automaton otherwise. --- docs/changelog/92276.yaml | 5 +++ .../cluster/routing/IndexRouting.java | 10 +++-- .../org/elasticsearch/common/regex/Regex.java | 38 +++++++++++++++++++ .../index/mapper/DocumentDimensions.java | 20 +++++++++- .../index/mapper/KeywordFieldMapper.java | 7 ++-- .../index/mapper/TimeSeriesIdFieldMapper.java | 11 +++--- .../common/regex/RegexTests.java | 23 +++++++++++ 7 files changed, 100 insertions(+), 14 deletions(-) create mode 100644 docs/changelog/92276.yaml diff --git a/docs/changelog/92276.yaml b/docs/changelog/92276.yaml new file mode 100644 index 000000000000..0727c512496a --- /dev/null +++ b/docs/changelog/92276.yaml @@ -0,0 +1,5 @@ +pr: 92276 +summary: Minor TSDB parsing speedup +area: TSDB +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/IndexRouting.java b/server/src/main/java/org/elasticsearch/cluster/routing/IndexRouting.java index 11440ffeca90..ceaa696f6e55 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/IndexRouting.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/IndexRouting.java @@ -36,6 +36,7 @@ import java.util.Set; import java.util.function.IntConsumer; import java.util.function.IntSupplier; +import java.util.function.Predicate; import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; @@ -233,7 +234,7 @@ public void collectSearchShards(String routing, IntConsumer consumer) { } public static class ExtractFromSource extends IndexRouting { - private final List routingPaths; + private final Predicate isRoutingPath; private final XContentParserConfiguration parserConfig; ExtractFromSource(IndexMetadata metadata) { @@ -241,7 +242,8 @@ public static class ExtractFromSource extends IndexRouting { if (metadata.isRoutingPartitionedIndex()) { throw new IllegalArgumentException("routing_partition_size is incompatible with routing_path"); } - this.routingPaths = metadata.getRoutingPaths(); + List routingPaths = metadata.getRoutingPaths(); + isRoutingPath = Regex.simpleMatcher(routingPaths.toArray(String[]::new)); this.parserConfig = XContentParserConfiguration.EMPTY.withFiltering(Set.copyOf(routingPaths), null, true); } @@ -262,7 +264,7 @@ public String createId(XContentType sourceType, BytesReference source, byte[] su public String createId(Map flat, byte[] suffix) { Builder b = builder(); for (Map.Entry e : flat.entrySet()) { - if (Regex.simpleMatch(routingPaths, e.getKey())) { + if (isRoutingPath.test(e.getKey())) { b.hashes.add(new NameAndHash(new BytesRef(e.getKey()), hash(new BytesRef(e.getValue().toString())))); } } @@ -299,7 +301,7 @@ public class Builder { private final List hashes = new ArrayList<>(); public void addMatching(String fieldName, BytesRef string) { - if (Regex.simpleMatch(routingPaths, fieldName)) { + if (isRoutingPath.test(fieldName)) { hashes.add(new NameAndHash(new BytesRef(fieldName), hash(string))); } } diff --git a/server/src/main/java/org/elasticsearch/common/regex/Regex.java b/server/src/main/java/org/elasticsearch/common/regex/Regex.java index ff2ac47ad209..73975e6c91e6 100644 --- a/server/src/main/java/org/elasticsearch/common/regex/Regex.java +++ b/server/src/main/java/org/elasticsearch/common/regex/Regex.java @@ -11,13 +11,17 @@ import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.automaton.Automata; import org.apache.lucene.util.automaton.Automaton; +import org.apache.lucene.util.automaton.CharacterRunAutomaton; import org.apache.lucene.util.automaton.Operations; import org.elasticsearch.common.Strings; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Set; +import java.util.function.Predicate; import java.util.regex.Pattern; public class Regex { @@ -90,6 +94,40 @@ public static Automaton simpleMatchToAutomaton(String... patterns) { return Operations.union(automata); } + /** + * Create a {@link Predicate} that matches the given patterns. Evaluating + * the returned predicate against a {@link String} yields the same result as + * running {@link #simpleMatch(String[], String)} but may run faster, + * especially in the case when there are multiple patterns. + */ + public static Predicate simpleMatcher(String... patterns) { + if (patterns == null || patterns.length == 0) { + return str -> false; + } + boolean hasWildcard = false; + for (String pattern : patterns) { + if (isMatchAllPattern(pattern)) { + return str -> true; + } + if (isSimpleMatchPattern(pattern)) { + hasWildcard = true; + break; + } + } + if (patterns.length == 1) { + if (hasWildcard) { + return str -> simpleMatch(patterns[0], str); + } else { + return patterns[0]::equals; + } + } else if (hasWildcard == false) { + return Set.copyOf(Arrays.asList(patterns))::contains; + } else { + Automaton automaton = simpleMatchToAutomaton(patterns); + return new CharacterRunAutomaton(automaton)::run; + } + } + /** * Match a String against the given pattern, supporting the following simple * pattern styles: "xxx*", "*xxx", "*xxx*" and "xxx*yyy" matches (with an diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DocumentDimensions.java b/server/src/main/java/org/elasticsearch/index/mapper/DocumentDimensions.java index 6f5f0c336633..0b0cc5673a31 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DocumentDimensions.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DocumentDimensions.java @@ -8,6 +8,8 @@ package org.elasticsearch.index.mapper; +import org.apache.lucene.util.BytesRef; + import java.net.InetAddress; import java.util.HashSet; import java.util.Set; @@ -16,7 +18,17 @@ * Collects dimensions from documents. */ public interface DocumentDimensions { - void addString(String fieldName, String value); + + /** + * This overloaded method tries to take advantage of the fact that the UTF-8 + * value is already computed in some cases when we want to collect + * dimensions, so we can save re-computing the UTF-8 encoding. + */ + void addString(String fieldName, BytesRef utf8Value); + + default void addString(String fieldName, String value) { + addString(fieldName, new BytesRef(value)); + } void addIp(String fieldName, InetAddress value); @@ -30,6 +42,12 @@ public interface DocumentDimensions { class OnlySingleValueAllowed implements DocumentDimensions { private final Set names = new HashSet<>(); + @Override + public void addString(String fieldName, BytesRef value) { + add(fieldName); + } + + // Override to skip the UTF-8 conversion that happens in the default implementation @Override public void addString(String fieldName, String value) { add(fieldName); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java index 8ebf6c5c5fc6..f6739c436131 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java @@ -995,13 +995,14 @@ private void indexValue(DocumentParserContext context, String value) { } value = normalizeValue(fieldType().normalizer(), name(), value); - if (fieldType().isDimension()) { - context.getDimensions().addString(fieldType().name(), value); - } // convert to utf8 only once before feeding postings/dv/stored fields final BytesRef binaryValue = new BytesRef(value); + if (fieldType().isDimension()) { + context.getDimensions().addString(fieldType().name(), binaryValue); + } + // If the UTF8 encoding of the field value is bigger than the max length 32766, Lucene fill fail the indexing request and, to roll // back the changes, will mark the (possibly partially indexed) document as deleted. This results in deletes, even in an append-only // workload, which in turn leads to slower merges, as these will potentially have to fall back to MergeStrategy.DOC instead of diff --git a/server/src/main/java/org/elasticsearch/index/mapper/TimeSeriesIdFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/TimeSeriesIdFieldMapper.java index 3032c4e696cd..a85ba6d0e9a4 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/TimeSeriesIdFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/TimeSeriesIdFieldMapper.java @@ -239,7 +239,7 @@ public BytesReference build() throws IOException { } @Override - public void addString(String fieldName, String value) { + public void addString(String fieldName, BytesRef utf8Value) { try (BytesStreamOutput out = new BytesStreamOutput()) { out.write((byte) 's'); /* @@ -247,17 +247,16 @@ public void addString(String fieldName, String value) { * so it's easier for folks to reason about the space taken up. Mostly * it'll be smaller too. */ - BytesRef bytes = new BytesRef(value); - if (bytes.length > DIMENSION_VALUE_LIMIT) { + if (utf8Value.length > DIMENSION_VALUE_LIMIT) { throw new IllegalArgumentException( - "Dimension fields must be less than [" + DIMENSION_VALUE_LIMIT + "] bytes but was [" + bytes.length + "]." + "Dimension fields must be less than [" + DIMENSION_VALUE_LIMIT + "] bytes but was [" + utf8Value.length + "]." ); } - out.writeBytesRef(bytes); + out.writeBytesRef(utf8Value); add(fieldName, out.bytes()); if (routingBuilder != null) { - routingBuilder.addMatching(fieldName, bytes); + routingBuilder.addMatching(fieldName, utf8Value); } } catch (IOException e) { throw new IllegalArgumentException("Dimension field cannot be serialized.", e); diff --git a/server/src/test/java/org/elasticsearch/common/regex/RegexTests.java b/server/src/test/java/org/elasticsearch/common/regex/RegexTests.java index 09fb8a410653..ccff89491fbf 100644 --- a/server/src/test/java/org/elasticsearch/common/regex/RegexTests.java +++ b/server/src/test/java/org/elasticsearch/common/regex/RegexTests.java @@ -189,4 +189,27 @@ private void assertMatchesNone(Automaton automaton, String... strings) { assertFalse(run.run(s)); } } + + public void testSimpleMatcher() { + assertFalse(Regex.simpleMatcher((String[]) null).test("abc")); + assertFalse(Regex.simpleMatcher().test("abc")); + assertTrue(Regex.simpleMatcher("abc").test("abc")); + assertFalse(Regex.simpleMatcher("abc").test("abd")); + + assertTrue(Regex.simpleMatcher("abc", "xyz").test("abc")); + assertTrue(Regex.simpleMatcher("abc", "xyz").test("xyz")); + assertFalse(Regex.simpleMatcher("abc", "xyz").test("abd")); + assertFalse(Regex.simpleMatcher("abc", "xyz").test("xyy")); + + assertTrue(Regex.simpleMatcher("abc", "*").test("abc")); + assertTrue(Regex.simpleMatcher("abc", "*").test("abd")); + + assertTrue(Regex.simpleMatcher("a*c").test("abc")); + assertFalse(Regex.simpleMatcher("a*c").test("abd")); + + assertTrue(Regex.simpleMatcher("a*c", "x*z").test("abc")); + assertTrue(Regex.simpleMatcher("a*c", "x*z").test("xyz")); + assertFalse(Regex.simpleMatcher("a*c", "x*z").test("abd")); + assertFalse(Regex.simpleMatcher("a*c", "x*z").test("xyy")); + } } From 296eacd1a3b2c41275dddafc3469f60d309d7344 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 12 Dec 2022 12:45:46 +0000 Subject: [PATCH 227/919] Disable BWC tests for #92277 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index e1e11e60e110..5aa3bbd3bd5b 100644 --- a/build.gradle +++ b/build.gradle @@ -137,9 +137,9 @@ tasks.register("verifyVersions") { * after the backport of the backcompat code is complete. */ -boolean bwc_tests_enabled = true +boolean bwc_tests_enabled = false // place a PR link here when committing bwc changes: -String bwc_tests_disabled_issue = "" +String bwc_tests_disabled_issue = "https://github.com/elastic/elasticsearch/pull/92277" if (bwc_tests_enabled == false) { if (bwc_tests_disabled_issue.isEmpty()) { throw new GradleException("bwc_tests_disabled_issue must be set when bwc_tests_enabled == false") From beb89873f67dd98ea9a897c0b78cfc3ff582f879 Mon Sep 17 00:00:00 2001 From: Alan Woodward Date: Mon, 12 Dec 2022 13:18:27 +0000 Subject: [PATCH 228/919] Remove old out-dated TODO (#92275) --- .../java/org/elasticsearch/index/mapper/MapperTestCase.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperTestCase.java index 4ef132026574..5a66cf243eab 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperTestCase.java @@ -388,8 +388,7 @@ public final void testMinimalSerializesToItself() throws IOException { assertParseMinimalWarnings(); } - // TODO make this final once we remove FieldMapperTestCase2 - public void testMinimalToMaximal() throws IOException { + public final void testMinimalToMaximal() throws IOException { XContentBuilder orig = JsonXContent.contentBuilder().startObject(); createMapperService(fieldMapping(this::minimalMapping)).documentMapper().mapping().toXContent(orig, INCLUDE_DEFAULTS); orig.endObject(); From 1581e8c7b7015eff5f71446b1e5e1a3ca7abaeb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Mon, 12 Dec 2022 14:20:44 +0100 Subject: [PATCH 229/919] Revert "Mark empty `_terms_enum` results due to DLS as incomplete (#91720)" (#92250) This reverts commit 635a4fe2bd6bd88ea0122602f6fe68e6f72fbb3b. --- docs/changelog/91720.yaml | 6 ---- .../action/TransportTermsEnumAction.java | 15 +-------- .../test/terms_enum/10_basic.yml | 31 ++++++------------- 3 files changed, 10 insertions(+), 42 deletions(-) delete mode 100644 docs/changelog/91720.yaml diff --git a/docs/changelog/91720.yaml b/docs/changelog/91720.yaml deleted file mode 100644 index 4994330c8aaa..000000000000 --- a/docs/changelog/91720.yaml +++ /dev/null @@ -1,6 +0,0 @@ -pr: 91720 -summary: Mark empty `_terms_enum` results due to DLS as incomplete -area: Search -type: enhancement -issues: - - 88321 diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/termsenum/action/TransportTermsEnumAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/termsenum/action/TransportTermsEnumAction.java index c4a587814a54..42729bebec94 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/termsenum/action/TransportTermsEnumAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/termsenum/action/TransportTermsEnumAction.java @@ -715,20 +715,7 @@ private void asyncNodeOperation(NodeTermsEnumRequest request, Task task, ActionL ThreadContext threadContext = transportService.getThreadPool().getThreadContext(); final XPackLicenseState frozenLicenseState = licenseState.copyCurrentLicenseState(); for (ShardId shardId : request.shardIds().toArray(new ShardId[0])) { - if (canAccess(shardId, request, frozenLicenseState, threadContext) == false) { - listener.onResponse( - new NodeTermsEnumResponse( - request.nodeId(), - Collections.emptyList(), - "cannot execute [_terms_enum] request on index [" - + shardId.getIndexName() - + "] due to " - + "DLS/FLS security restrictions.", - false - ) - ); - } - if (canMatchShard(shardId, request) == false) { + if (canAccess(shardId, request, frozenLicenseState, threadContext) == false || canMatchShard(shardId, request) == false) { // Permission denied or can't match, remove shardID from request request.remove(shardId); } diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/terms_enum/10_basic.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/terms_enum/10_basic.yml index a592768d8336..359e14b935f8 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/terms_enum/10_basic.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/terms_enum/10_basic.yml @@ -549,25 +549,18 @@ teardown: - length: { terms: 1 } - do: - headers: { Authorization: "Basic ZGxzX3NvbWVfdXNlcjp4LXBhY2stdGVzdC1wYXNzd29yZA==" } # dls_some_user doesn't see all docs + headers: { Authorization: "Basic ZGxzX3NvbWVfdXNlcjp4LXBhY2stdGVzdC1wYXNzd29yZA==" } # dls_some_user sees selected docs terms_enum: - index: test_security - body: { "field": "foo", "string": "b" } - - - length: { terms: 0 } - - match: { complete: false } - - match: { _shards.failed: 1 } - - match: { _shards.failures.0.reason.type: "broadcast_shard_operation_failed_exception" } - - match: { _shards.failures.0.reason.reason: "cannot execute [_terms_enum] request on index [test_security] due to DLS/FLS security restrictions." } + index: test_security + body: {"field": "foo", "string":"b"} + - length: {terms: 0} - do: headers: { Authorization: "Basic ZmxzX3VzZXI6eC1wYWNrLXRlc3QtcGFzc3dvcmQ=" } # fls_user can't see field terms_enum: - index: test_security - body: { "field": "foo", "string": "b" } - - length: { terms: 0 } - - match: { complete: true } - - match: { _shards.failed: 0 } + index: test_security + body: {"field": "foo", "string":"b"} + - length: {terms: 0} --- "Test security with API keys": @@ -619,7 +612,7 @@ teardown: } } - match: { name: "dls_all_user_bad_key" } - - set: { encoded: login_creds } + - set: { encoded: login_creds} - do: headers: Authorization: ApiKey ${login_creds} # dls_all_user bad API key sees selected docs @@ -627,9 +620,6 @@ teardown: index: test_security body: { "field": "foo", "string": "b" } - length: { terms: 0 } - - match: { complete: false } - - match: { _shards.failed: 1 } - - match: { _shards.failures.0.reason.type: "broadcast_shard_operation_failed_exception" } - do: headers: { Authorization: "Basic ZGxzX3NvbWVfdXNlcjp4LXBhY2stdGVzdC1wYXNzd29yZA==" } # dls_some_user @@ -651,7 +641,7 @@ teardown: } } - match: { name: "dls_some_user_key" } - - set: { encoded: login_creds } + - set: { encoded: login_creds} - do: headers: Authorization: ApiKey ${login_creds} # dls_some_user's API key sees selected user regardless of the key's role descriptor @@ -659,6 +649,3 @@ teardown: index: test_security body: { "field": "foo", "string": "b" } - length: { terms: 0 } - - match: { complete: false } - - match: { _shards.failed: 1 } - - match: { _shards.failures.0.reason.type: "broadcast_shard_operation_failed_exception" } From 7a5af1305e04a1938a340f4793e6362d6a4a6d22 Mon Sep 17 00:00:00 2001 From: Przemyslaw Gomulka Date: Mon, 12 Dec 2022 14:24:19 +0100 Subject: [PATCH 230/919] Remove known issue note in 8.5.0 for date range bug (#92270) it was fixed in 8.5.0 - so can be removed relates #90721 --- docs/reference/release-notes/8.5.0.asciidoc | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/reference/release-notes/8.5.0.asciidoc b/docs/reference/release-notes/8.5.0.asciidoc index 9f02725e9dfa..e5e3bfc8655f 100644 --- a/docs/reference/release-notes/8.5.0.asciidoc +++ b/docs/reference/release-notes/8.5.0.asciidoc @@ -325,10 +325,6 @@ Packaging:: [float] === Known issues -* When using date range search with format that does not have all date fields (missing month or day) -an incorrectly parsed date could be used. The workaround is to use date pattern with all date fields (year, month, day) -(issue: {es-issue}90187[#90187]) - * It is possible to inadvertently create an alias with the same name as an index in version 8.5.0. This action leaves the cluster in an invalid state in which several features will not work correctly, and it may not even be possible From bdf634f9f5d106eadd89b88c1dadd8fd16b2d602 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 12 Dec 2022 13:36:27 +0000 Subject: [PATCH 231/919] Fixup BWC after backport of #92259 (#92280) Adjusts the BWC version for the wire protocol and re-enables the BWC tests. --- build.gradle | 4 ++-- .../cluster/coordination/PublicationTransportHandler.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 5aa3bbd3bd5b..e1e11e60e110 100644 --- a/build.gradle +++ b/build.gradle @@ -137,9 +137,9 @@ tasks.register("verifyVersions") { * after the backport of the backcompat code is complete. */ -boolean bwc_tests_enabled = false +boolean bwc_tests_enabled = true // place a PR link here when committing bwc changes: -String bwc_tests_disabled_issue = "https://github.com/elastic/elasticsearch/pull/92277" +String bwc_tests_disabled_issue = "" if (bwc_tests_enabled == false) { if (bwc_tests_disabled_issue.isEmpty()) { throw new GradleException("bwc_tests_disabled_issue must be set when bwc_tests_enabled == false") diff --git a/server/src/main/java/org/elasticsearch/cluster/coordination/PublicationTransportHandler.java b/server/src/main/java/org/elasticsearch/cluster/coordination/PublicationTransportHandler.java index 11d64f162241..ce92c98d6f8f 100644 --- a/server/src/main/java/org/elasticsearch/cluster/coordination/PublicationTransportHandler.java +++ b/server/src/main/java/org/elasticsearch/cluster/coordination/PublicationTransportHandler.java @@ -86,7 +86,7 @@ public class PublicationTransportHandler { TransportRequestOptions.Type.STATE ); - public static final Version INCLUDES_LAST_COMMITTED_DATA_VERSION = Version.V_8_7_0; + public static final Version INCLUDES_LAST_COMMITTED_DATA_VERSION = Version.V_8_6_0; private final SerializationStatsTracker serializationStatsTracker = new SerializationStatsTracker(); From ebc09dd8d603bf11bcd9d0cfd6754d9d8d60e304 Mon Sep 17 00:00:00 2001 From: Abdon Pijpelink Date: Mon, 12 Dec 2022 14:54:14 +0100 Subject: [PATCH 232/919] [DOCS] Remove 'coming' from 8.5.3 release notes (#92283) --- docs/reference/release-notes/8.5.3.asciidoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/reference/release-notes/8.5.3.asciidoc b/docs/reference/release-notes/8.5.3.asciidoc index ebc429faeda4..41c1b4090b88 100644 --- a/docs/reference/release-notes/8.5.3.asciidoc +++ b/docs/reference/release-notes/8.5.3.asciidoc @@ -1,8 +1,6 @@ [[release-notes-8.5.3]] == {es} version 8.5.3 -coming[8.5.3] - Also see <>. [[bug-8.5.3]] From 6561c4731742f2d1967466f31dee6324785f6d1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Mon, 12 Dec 2022 15:18:49 +0100 Subject: [PATCH 233/919] [DOCS] Adds example of better index resolve to transforms at scale (#92234) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Przemysław Witek --- .../transform/transforms-at-scale.asciidoc | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/reference/transform/transforms-at-scale.asciidoc b/docs/reference/transform/transforms-at-scale.asciidoc index f4ad4fb112dc..f1d47c994324 100644 --- a/docs/reference/transform/transforms-at-scale.asciidoc +++ b/docs/reference/transform/transforms-at-scale.asciidoc @@ -116,6 +116,23 @@ example, greater than `2020-01-01T00:00:00`) to limit which historical indices are accessed. If you use a relative time value (for example, `now-30d`) then this date range is re-evaluated at the point of each checkpoint execution. +Consider using <> in your index names to +reduce the number of indices to resolve in your queries. Add a date pattern +- for example, `yyyy-MM-dd` - to your index names and use it to limit your query +to a specific date. The example below queries indices only from yesterday and +today: + +[source,js] +---------------------------------- + "source": { + "index": [ + "", + "" + ] + }, +---------------------------------- +// NOTCONSOLE + [discrete] [[optimize-shading-strategy]] From 265f32ae969ed07bb67be2eae6d12d72fe48c074 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 13 Dec 2022 01:23:29 +1100 Subject: [PATCH 234/919] Support stored authentication headers prior to version 6.7 (#92221) Officially Elasticsearch is compatible with last major at data level. Therefore v8 is not compatible with v6. However we don't have a guided migration path for stored authentication headers, e.g. upgrade assistant does not do anything for them. Therefore it is more helpful and user friendly for v8 to support v6 stored authentication headers. This PR adds back the version conditional logic removed in #41185 along with tests. --- docs/changelog/92221.yaml | 5 ++++ .../core/security/authc/Authentication.java | 28 ++++++++++++++--- .../security/authc/AuthenticationTests.java | 30 +++++++++++++++++++ 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 docs/changelog/92221.yaml diff --git a/docs/changelog/92221.yaml b/docs/changelog/92221.yaml new file mode 100644 index 000000000000..87d77fe01951 --- /dev/null +++ b/docs/changelog/92221.yaml @@ -0,0 +1,5 @@ +pr: 92221 +summary: Support stored authentication headers prior to version 6.7 +area: Authentication +type: bug +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java index d643ea99ab63..5ef7b99308e0 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java @@ -19,6 +19,7 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Strings; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.ToXContentObject; @@ -90,6 +91,7 @@ public final class Authentication implements ToXContentObject { private static final Logger logger = LogManager.getLogger(Authentication.class); + private static final Version VERSION_AUTHENTICATION_TYPE = Version.fromString("6.7.0"); public static final Version VERSION_API_KEY_ROLES_AS_BYTES = Version.V_7_9_0; public static final Version VERSION_REALM_DOMAINS = Version.V_8_2_0; @@ -141,8 +143,14 @@ public Authentication(StreamInput in) throws IOException { assert innerUser != null || lookedUpBy == null : "Authentication has no inner-user, but looked-up-by is [" + lookedUpBy + "]"; final Version version = in.getVersion(); - type = AuthenticationType.values()[in.readVInt()]; - final Map metadata = in.readMap(); + final Map metadata; + if (version.onOrAfter(VERSION_AUTHENTICATION_TYPE)) { + type = AuthenticationType.values()[in.readVInt()]; + metadata = in.readMap(); + } else { + type = AuthenticationType.REALM; + metadata = Map.of(); + } if (innerUser != null) { authenticatingSubject = new Subject(innerUser, authenticatedBy, version, metadata); // The lookup user for run-as currently doesn't have authentication metadata associated with them because @@ -469,8 +477,20 @@ public void writeTo(StreamOutput out) throws IOException { } else { out.writeBoolean(false); } - out.writeVInt(type.ordinal()); - out.writeGenericMap(getAuthenticatingSubject().getMetadata()); + final Map metadata = getAuthenticatingSubject().getMetadata(); + if (out.getVersion().onOrAfter(VERSION_AUTHENTICATION_TYPE)) { + out.writeVInt(type.ordinal()); + out.writeGenericMap(metadata); + } else { + assert type == AuthenticationType.REALM && metadata.isEmpty() + : Strings.format( + "authentication with version [%s] must have authentication type %s and empty metadata, but got [%s] and [%s]", + out.getVersion(), + AuthenticationType.REALM, + type, + metadata + ); + } } /** diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTests.java index 7ddc0f91c8b8..d04f8c231787 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTests.java @@ -22,6 +22,7 @@ import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings; +import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.User; @@ -32,6 +33,7 @@ import java.util.function.Consumer; import java.util.stream.Collectors; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.is; @@ -474,6 +476,34 @@ public void testToXContentWithServiceAccount() throws IOException { ); } + public void testBwcWithStoredAuthenticationHeaders() throws IOException { + // Version 6.6.1 + final String headerV6 = "p/HxAgANZWxhc3RpYy1hZG1pbgEJc3VwZXJ1c2VyCgAAAAEABG5vZGUFZmlsZTEEZmlsZQA="; + final Authentication authenticationV6 = AuthenticationContextSerializer.decode(headerV6); + assertThat(authenticationV6.getEffectiveSubject().getVersion(), equalTo(Version.fromString("6.6.1"))); + assertThat(authenticationV6.getEffectiveSubject().getUser(), equalTo(new User("elastic-admin", "superuser"))); + assertThat(authenticationV6.getAuthenticationType(), equalTo(Authentication.AuthenticationType.REALM)); + assertThat(authenticationV6.isRunAs(), is(false)); + assertThat(authenticationV6.encode(), equalTo(headerV6)); + + // Rewrite for a different version + final Version nodeVersion = VersionUtils.randomIndexCompatibleVersion(random()); + final Authentication rewrittenAuthentication = authenticationV6.maybeRewriteForOlderVersion(nodeVersion); + assertThat(rewrittenAuthentication.getEffectiveSubject().getVersion(), equalTo(nodeVersion)); + assertThat(rewrittenAuthentication.getEffectiveSubject().getUser(), equalTo(authenticationV6.getEffectiveSubject().getUser())); + assertThat(rewrittenAuthentication.getAuthenticationType(), equalTo(Authentication.AuthenticationType.REALM)); + assertThat(rewrittenAuthentication.isRunAs(), is(false)); + + // Version 7.2.1 + final String headerV7 = "p72sAwANZWxhc3RpYy1hZG1pbgENX2VzX3Rlc3Rfcm9vdAoAAAABAARub2RlBWZpbGUxBGZpbGUAAAoA"; + final Authentication authenticationV7 = AuthenticationContextSerializer.decode(headerV7); + assertThat(authenticationV7.getEffectiveSubject().getVersion(), equalTo(Version.fromString("7.2.1"))); + assertThat(authenticationV7.getEffectiveSubject().getUser(), equalTo(new User("elastic-admin", "_es_test_root"))); + assertThat(authenticationV7.getAuthenticationType(), equalTo(Authentication.AuthenticationType.REALM)); + assertThat(authenticationV7.isRunAs(), is(false)); + assertThat(authenticationV7.encode(), equalTo(headerV7)); + } + private void runWithAuthenticationToXContent(Authentication authentication, Consumer> consumer) throws IOException { try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { authentication.toXContent(builder, ToXContent.EMPTY_PARAMS); From b3515ddbb3df447f613a5b504deb5ef683607415 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Mon, 12 Dec 2022 14:37:29 +0000 Subject: [PATCH 235/919] Short-circuit painless def equality (#92102) Add short-circuits for basic equality conditions on defs --- docs/changelog/92102.yaml | 5 + .../org/elasticsearch/painless/DefMath.java | 16 +- .../painless/ComparisonTests.java | 337 ++++++++++++++++++ 3 files changed, 352 insertions(+), 6 deletions(-) create mode 100644 docs/changelog/92102.yaml diff --git a/docs/changelog/92102.yaml b/docs/changelog/92102.yaml new file mode 100644 index 000000000000..99a1cbcc4b38 --- /dev/null +++ b/docs/changelog/92102.yaml @@ -0,0 +1,5 @@ +pr: 92102 +summary: Short-circuit painless def equality +area: Infra/Scripting +type: enhancement +issues: [] diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/DefMath.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/DefMath.java index 0a29ff80a45b..1132b1163565 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/DefMath.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/DefMath.java @@ -526,8 +526,12 @@ private static boolean eq(boolean a, boolean b) { } private static boolean eq(Object left, Object right) { - if (left != null && right != null) { - if (left instanceof Double) { + if (left == right) { + return true; + } else if (left != null && right != null) { + if (left.getClass() == right.getClass()) { + return left.equals(right); + } else if (left instanceof Double) { if (right instanceof Number) { return (double) left == ((Number) right).doubleValue(); } else if (right instanceof Character) { @@ -537,7 +541,7 @@ private static boolean eq(Object left, Object right) { if (left instanceof Number) { return ((Number) left).doubleValue() == (double) right; } else if (left instanceof Character) { - return (char) left == ((Number) right).doubleValue(); + return (char) left == (double) right; } } else if (left instanceof Float) { if (right instanceof Number) { @@ -549,7 +553,7 @@ private static boolean eq(Object left, Object right) { if (left instanceof Number) { return ((Number) left).floatValue() == (float) right; } else if (left instanceof Character) { - return (char) left == ((Number) right).floatValue(); + return (char) left == (float) right; } } else if (left instanceof Long) { if (right instanceof Number) { @@ -561,7 +565,7 @@ private static boolean eq(Object left, Object right) { if (left instanceof Number) { return ((Number) left).longValue() == (long) right; } else if (left instanceof Character) { - return (char) left == ((Number) right).longValue(); + return (char) left == (long) right; } } else if (left instanceof Number) { if (right instanceof Number) { @@ -578,7 +582,7 @@ private static boolean eq(Object left, Object right) { return left.equals(right); } - return left == null && right == null; + return false; } // comparison operators: applicable for any numeric type diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/ComparisonTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/ComparisonTests.java index 89d1872e1ccc..b7bd562a3ee0 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/ComparisonTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/ComparisonTests.java @@ -11,6 +11,14 @@ public class ComparisonTests extends ScriptTestCase { public void testDefEq() { + assertEquals(true, exec("def x = (byte)7; def y = (char)7; return x == y")); + assertEquals(true, exec("def x = (short)6; def y = (char)6; return x == y")); + assertEquals(true, exec("def x = (char)5; def y = (char)5; return x == y")); + assertEquals(true, exec("def x = (int)4; def y = (char)4; return x == y")); + assertEquals(false, exec("def x = (long)5; def y = (char)3; return x == y")); + assertEquals(false, exec("def x = (float)6; def y = (char)2; return x == y")); + assertEquals(false, exec("def x = (double)7; def y = (char)1; return x == y")); + assertEquals(true, exec("def x = (byte)7; def y = (int)7; return x == y")); assertEquals(true, exec("def x = (short)6; def y = (int)6; return x == y")); assertEquals(true, exec("def x = (char)5; def y = (int)5; return x == y")); @@ -19,6 +27,22 @@ public void testDefEq() { assertEquals(false, exec("def x = (float)6; def y = (int)2; return x == y")); assertEquals(false, exec("def x = (double)7; def y = (int)1; return x == y")); + assertEquals(true, exec("def x = (byte)7; def y = (long)7; return x == y")); + assertEquals(true, exec("def x = (short)6; def y = (long)6; return x == y")); + assertEquals(true, exec("def x = (char)5; def y = (long)5; return x == y")); + assertEquals(true, exec("def x = (int)4; def y = (long)4; return x == y")); + assertEquals(false, exec("def x = (long)5; def y = (long)3; return x == y")); + assertEquals(false, exec("def x = (float)6; def y = (long)2; return x == y")); + assertEquals(false, exec("def x = (double)7; def y = (long)1; return x == y")); + + assertEquals(true, exec("def x = (byte)7; def y = (float)7; return x == y")); + assertEquals(true, exec("def x = (short)6; def y = (float)6; return x == y")); + assertEquals(true, exec("def x = (char)5; def y = (float)5; return x == y")); + assertEquals(true, exec("def x = (int)4; def y = (float)4; return x == y")); + assertEquals(false, exec("def x = (long)5; def y = (float)3; return x == y")); + assertEquals(false, exec("def x = (float)6; def y = (float)2; return x == y")); + assertEquals(false, exec("def x = (double)7; def y = (float)1; return x == y")); + assertEquals(true, exec("def x = (byte)7; def y = (double)7; return x == y")); assertEquals(true, exec("def x = (short)6; def y = (double)6; return x == y")); assertEquals(true, exec("def x = (char)5; def y = (double)5; return x == y")); @@ -41,6 +65,14 @@ public void testDefEq() { } public void testDefEqTypedLHS() { + assertEquals(true, exec("byte x = (byte)7; def y = (char)7; return x == y")); + assertEquals(true, exec("short x = (short)6; def y = (char)6; return x == y")); + assertEquals(true, exec("char x = (char)5; def y = (char)5; return x == y")); + assertEquals(true, exec("int x = (int)4; def y = (char)4; return x == y")); + assertEquals(false, exec("long x = (long)5; def y = (char)3; return x == y")); + assertEquals(false, exec("float x = (float)6; def y = (char)2; return x == y")); + assertEquals(false, exec("double x = (double)7; def y = (char)1; return x == y")); + assertEquals(true, exec("byte x = (byte)7; def y = (int)7; return x == y")); assertEquals(true, exec("short x = (short)6; def y = (int)6; return x == y")); assertEquals(true, exec("char x = (char)5; def y = (int)5; return x == y")); @@ -49,6 +81,22 @@ public void testDefEqTypedLHS() { assertEquals(false, exec("float x = (float)6; def y = (int)2; return x == y")); assertEquals(false, exec("double x = (double)7; def y = (int)1; return x == y")); + assertEquals(true, exec("byte x = (byte)7; def y = (long)7; return x == y")); + assertEquals(true, exec("short x = (short)6; def y = (long)6; return x == y")); + assertEquals(true, exec("char x = (char)5; def y = (long)5; return x == y")); + assertEquals(true, exec("int x = (int)4; def y = (long)4; return x == y")); + assertEquals(false, exec("long x = (long)5; def y = (long)3; return x == y")); + assertEquals(false, exec("float x = (float)6; def y = (long)2; return x == y")); + assertEquals(false, exec("double x = (double)7; def y = (long)1; return x == y")); + + assertEquals(true, exec("byte x = (byte)7; def y = (float)7; return x == y")); + assertEquals(true, exec("short x = (short)6; def y = (float)6; return x == y")); + assertEquals(true, exec("char x = (char)5; def y = (float)5; return x == y")); + assertEquals(true, exec("int x = (int)4; def y = (float)4; return x == y")); + assertEquals(false, exec("long x = (long)5; def y = (float)3; return x == y")); + assertEquals(false, exec("float x = (float)6; def y = (float)2; return x == y")); + assertEquals(false, exec("double x = (double)7; def y = (float)1; return x == y")); + assertEquals(true, exec("byte x = (byte)7; def y = (double)7; return x == y")); assertEquals(true, exec("short x = (short)6; def y = (double)6; return x == y")); assertEquals(true, exec("char x = (char)5; def y = (double)5; return x == y")); @@ -70,6 +118,14 @@ public void testDefEqTypedLHS() { } public void testDefEqTypedRHS() { + assertEquals(true, exec("def x = (byte)7; char y = (char)7; return x == y")); + assertEquals(true, exec("def x = (short)6; char y = (char)6; return x == y")); + assertEquals(true, exec("def x = (char)5; char y = (char)5; return x == y")); + assertEquals(true, exec("def x = (int)4; char y = (char)4; return x == y")); + assertEquals(false, exec("def x = (long)5; char y = (char)3; return x == y")); + assertEquals(false, exec("def x = (float)6; char y = (char)2; return x == y")); + assertEquals(false, exec("def x = (double)7; char y = (char)1; return x == y")); + assertEquals(true, exec("def x = (byte)7; int y = (int)7; return x == y")); assertEquals(true, exec("def x = (short)6; int y = (int)6; return x == y")); assertEquals(true, exec("def x = (char)5; int y = (int)5; return x == y")); @@ -78,6 +134,22 @@ public void testDefEqTypedRHS() { assertEquals(false, exec("def x = (float)6; int y = (int)2; return x == y")); assertEquals(false, exec("def x = (double)7; int y = (int)1; return x == y")); + assertEquals(true, exec("def x = (byte)7; long y = (long)7; return x == y")); + assertEquals(true, exec("def x = (short)6; long y = (long)6; return x == y")); + assertEquals(true, exec("def x = (char)5; long y = (long)5; return x == y")); + assertEquals(true, exec("def x = (int)4; long y = (long)4; return x == y")); + assertEquals(false, exec("def x = (long)5; long y = (long)3; return x == y")); + assertEquals(false, exec("def x = (float)6; long y = (long)2; return x == y")); + assertEquals(false, exec("def x = (double)7; long y = (long)1; return x == y")); + + assertEquals(true, exec("def x = (byte)7; float y = (float)7; return x == y")); + assertEquals(true, exec("def x = (short)6; float y = (float)6; return x == y")); + assertEquals(true, exec("def x = (char)5; float y = (float)5; return x == y")); + assertEquals(true, exec("def x = (int)4; float y = (float)4; return x == y")); + assertEquals(false, exec("def x = (long)5; float y = (float)3; return x == y")); + assertEquals(false, exec("def x = (float)6; float y = (float)2; return x == y")); + assertEquals(false, exec("def x = (double)7; float y = (float)1; return x == y")); + assertEquals(true, exec("def x = (byte)7; double y = (double)7; return x == y")); assertEquals(true, exec("def x = (short)6; double y = (double)6; return x == y")); assertEquals(true, exec("def x = (char)5; double y = (double)5; return x == y")); @@ -106,6 +178,7 @@ public void testDefEqr() { assertEquals(false, exec("def x = (long)5; def y = (int)3; return x === y")); assertEquals(false, exec("def x = (float)6; def y = (int)2; return x === y")); assertEquals(false, exec("def x = (double)7; def y = (int)1; return x === y")); + assertEquals(false, exec("def x = false; def y = true; return x === y")); assertEquals(false, exec("def x = new HashMap(); def y = new HashMap(); return x === y")); @@ -115,6 +188,14 @@ public void testDefEqr() { } public void testDefNe() { + assertEquals(false, exec("def x = (byte)7; def y = (char)7; return x != y")); + assertEquals(false, exec("def x = (short)6; def y = (char)6; return x != y")); + assertEquals(false, exec("def x = (char)5; def y = (char)5; return x != y")); + assertEquals(false, exec("def x = (int)4; def y = (char)4; return x != y")); + assertEquals(true, exec("def x = (long)5; def y = (char)3; return x != y")); + assertEquals(true, exec("def x = (float)6; def y = (char)2; return x != y")); + assertEquals(true, exec("def x = (double)7; def y = (char)1; return x != y")); + assertEquals(false, exec("def x = (byte)7; def y = (int)7; return x != y")); assertEquals(false, exec("def x = (short)6; def y = (int)6; return x != y")); assertEquals(false, exec("def x = (char)5; def y = (int)5; return x != y")); @@ -123,6 +204,22 @@ public void testDefNe() { assertEquals(true, exec("def x = (float)6; def y = (int)2; return x != y")); assertEquals(true, exec("def x = (double)7; def y = (int)1; return x != y")); + assertEquals(false, exec("def x = (byte)7; def y = (long)7; return x != y")); + assertEquals(false, exec("def x = (short)6; def y = (long)6; return x != y")); + assertEquals(false, exec("def x = (char)5; def y = (long)5; return x != y")); + assertEquals(false, exec("def x = (int)4; def y = (long)4; return x != y")); + assertEquals(true, exec("def x = (long)5; def y = (long)3; return x != y")); + assertEquals(true, exec("def x = (float)6; def y = (long)2; return x != y")); + assertEquals(true, exec("def x = (double)7; def y = (long)1; return x != y")); + + assertEquals(false, exec("def x = (byte)7; def y = (float)7; return x != y")); + assertEquals(false, exec("def x = (short)6; def y = (float)6; return x != y")); + assertEquals(false, exec("def x = (char)5; def y = (float)5; return x != y")); + assertEquals(false, exec("def x = (int)4; def y = (float)4; return x != y")); + assertEquals(true, exec("def x = (long)5; def y = (float)3; return x != y")); + assertEquals(true, exec("def x = (float)6; def y = (float)2; return x != y")); + assertEquals(true, exec("def x = (double)7; def y = (float)1; return x != y")); + assertEquals(false, exec("def x = (byte)7; def y = (double)7; return x != y")); assertEquals(false, exec("def x = (short)6; def y = (double)6; return x != y")); assertEquals(false, exec("def x = (char)5; def y = (double)5; return x != y")); @@ -143,6 +240,14 @@ public void testDefNe() { } public void testDefNeTypedLHS() { + assertEquals(false, exec("byte x = (byte)7; def y = (char)7; return x != y")); + assertEquals(false, exec("short x = (short)6; def y = (char)6; return x != y")); + assertEquals(false, exec("char x = (char)5; def y = (char)5; return x != y")); + assertEquals(false, exec("int x = (int)4; def y = (char)4; return x != y")); + assertEquals(true, exec("long x = (long)5; def y = (char)3; return x != y")); + assertEquals(true, exec("float x = (float)6; def y = (char)2; return x != y")); + assertEquals(true, exec("double x = (double)7; def y = (char)1; return x != y")); + assertEquals(false, exec("byte x = (byte)7; def y = (int)7; return x != y")); assertEquals(false, exec("short x = (short)6; def y = (int)6; return x != y")); assertEquals(false, exec("char x = (char)5; def y = (int)5; return x != y")); @@ -151,6 +256,22 @@ public void testDefNeTypedLHS() { assertEquals(true, exec("float x = (float)6; def y = (int)2; return x != y")); assertEquals(true, exec("double x = (double)7; def y = (int)1; return x != y")); + assertEquals(false, exec("byte x = (byte)7; def y = (long)7; return x != y")); + assertEquals(false, exec("short x = (short)6; def y = (long)6; return x != y")); + assertEquals(false, exec("char x = (char)5; def y = (long)5; return x != y")); + assertEquals(false, exec("int x = (int)4; def y = (long)4; return x != y")); + assertEquals(true, exec("long x = (long)5; def y = (long)3; return x != y")); + assertEquals(true, exec("float x = (float)6; def y = (long)2; return x != y")); + assertEquals(true, exec("double x = (double)7; def y = (long)1; return x != y")); + + assertEquals(false, exec("byte x = (byte)7; def y = (float)7; return x != y")); + assertEquals(false, exec("short x = (short)6; def y = (float)6; return x != y")); + assertEquals(false, exec("char x = (char)5; def y = (float)5; return x != y")); + assertEquals(false, exec("int x = (int)4; def y = (float)4; return x != y")); + assertEquals(true, exec("long x = (long)5; def y = (float)3; return x != y")); + assertEquals(true, exec("float x = (float)6; def y = (float)2; return x != y")); + assertEquals(true, exec("double x = (double)7; def y = (float)1; return x != y")); + assertEquals(false, exec("byte x = (byte)7; def y = (double)7; return x != y")); assertEquals(false, exec("short x = (short)6; def y = (double)6; return x != y")); assertEquals(false, exec("char x = (char)5; def y = (double)5; return x != y")); @@ -171,6 +292,14 @@ public void testDefNeTypedLHS() { } public void testDefNeTypedRHS() { + assertEquals(false, exec("def x = (byte)7; char y = (char)7; return x != y")); + assertEquals(false, exec("def x = (short)6; char y = (char)6; return x != y")); + assertEquals(false, exec("def x = (char)5; char y = (char)5; return x != y")); + assertEquals(false, exec("def x = (int)4; char y = (char)4; return x != y")); + assertEquals(true, exec("def x = (long)5; char y = (char)3; return x != y")); + assertEquals(true, exec("def x = (float)6; char y = (char)2; return x != y")); + assertEquals(true, exec("def x = (double)7; char y = (char)1; return x != y")); + assertEquals(false, exec("def x = (byte)7; int y = (int)7; return x != y")); assertEquals(false, exec("def x = (short)6; int y = (int)6; return x != y")); assertEquals(false, exec("def x = (char)5; int y = (int)5; return x != y")); @@ -179,6 +308,22 @@ public void testDefNeTypedRHS() { assertEquals(true, exec("def x = (float)6; int y = (int)2; return x != y")); assertEquals(true, exec("def x = (double)7; int y = (int)1; return x != y")); + assertEquals(false, exec("def x = (byte)7; long y = (long)7; return x != y")); + assertEquals(false, exec("def x = (short)6; long y = (long)6; return x != y")); + assertEquals(false, exec("def x = (char)5; long y = (long)5; return x != y")); + assertEquals(false, exec("def x = (int)4; long y = (long)4; return x != y")); + assertEquals(true, exec("def x = (long)5; long y = (long)3; return x != y")); + assertEquals(true, exec("def x = (float)6; long y = (long)2; return x != y")); + assertEquals(true, exec("def x = (double)7; long y = (long)1; return x != y")); + + assertEquals(false, exec("def x = (byte)7; float y = (float)7; return x != y")); + assertEquals(false, exec("def x = (short)6; float y = (float)6; return x != y")); + assertEquals(false, exec("def x = (char)5; float y = (float)5; return x != y")); + assertEquals(false, exec("def x = (int)4; float y = (float)4; return x != y")); + assertEquals(true, exec("def x = (long)5; float y = (float)3; return x != y")); + assertEquals(true, exec("def x = (float)6; float y = (float)2; return x != y")); + assertEquals(true, exec("def x = (double)7; float y = (float)1; return x != y")); + assertEquals(false, exec("def x = (byte)7; double y = (double)7; return x != y")); assertEquals(false, exec("def x = (short)6; double y = (double)6; return x != y")); assertEquals(false, exec("def x = (char)5; double y = (double)5; return x != y")); @@ -222,6 +367,22 @@ public void testDefLt() { assertEquals(false, exec("def x = (float)6; def y = (int)2; return x < y")); assertEquals(false, exec("def x = (double)7; def y = (int)1; return x < y")); + assertEquals(true, exec("def x = (byte)1; def y = (long)7; return x < y")); + assertEquals(true, exec("def x = (short)2; def y = (long)6; return x < y")); + assertEquals(true, exec("def x = (char)3; def y = (long)5; return x < y")); + assertEquals(false, exec("def x = (int)4; def y = (long)4; return x < y")); + assertEquals(false, exec("def x = (long)5; def y = (long)3; return x < y")); + assertEquals(false, exec("def x = (float)6; def y = (long)2; return x < y")); + assertEquals(false, exec("def x = (double)7; def y = (long)1; return x < y")); + + assertEquals(true, exec("def x = (byte)1; def y = (float)7; return x < y")); + assertEquals(true, exec("def x = (short)2; def y = (float)6; return x < y")); + assertEquals(true, exec("def x = (char)3; def y = (float)5; return x < y")); + assertEquals(false, exec("def x = (int)4; def y = (float)4; return x < y")); + assertEquals(false, exec("def x = (long)5; def y = (float)3; return x < y")); + assertEquals(false, exec("def x = (float)6; def y = (float)2; return x < y")); + assertEquals(false, exec("def x = (double)7; def y = (float)1; return x < y")); + assertEquals(true, exec("def x = (byte)1; def y = (double)7; return x < y")); assertEquals(true, exec("def x = (short)2; def y = (double)6; return x < y")); assertEquals(true, exec("def x = (char)3; def y = (double)5; return x < y")); @@ -240,6 +401,22 @@ public void testDefLtTypedLHS() { assertEquals(false, exec("float x = (float)6; def y = (int)2; return x < y")); assertEquals(false, exec("double x = (double)7; def y = (int)1; return x < y")); + assertEquals(true, exec("byte x = (byte)1; def y = (long)7; return x < y")); + assertEquals(true, exec("short x = (short)2; def y = (long)6; return x < y")); + assertEquals(true, exec("char x = (char)3; def y = (long)5; return x < y")); + assertEquals(false, exec("int x = (int)4; def y = (long)4; return x < y")); + assertEquals(false, exec("long x = (long)5; def y = (long)3; return x < y")); + assertEquals(false, exec("float x = (float)6; def y = (long)2; return x < y")); + assertEquals(false, exec("double x = (double)7; def y = (long)1; return x < y")); + + assertEquals(true, exec("byte x = (byte)1; def y = (float)7; return x < y")); + assertEquals(true, exec("short x = (short)2; def y = (float)6; return x < y")); + assertEquals(true, exec("char x = (char)3; def y = (float)5; return x < y")); + assertEquals(false, exec("int x = (int)4; def y = (float)4; return x < y")); + assertEquals(false, exec("long x = (long)5; def y = (float)3; return x < y")); + assertEquals(false, exec("float x = (float)6; def y = (float)2; return x < y")); + assertEquals(false, exec("double x = (double)7; def y = (float)1; return x < y")); + assertEquals(true, exec("byte x = (byte)1; def y = (double)7; return x < y")); assertEquals(true, exec("short x = (short)2; def y = (double)6; return x < y")); assertEquals(true, exec("char x = (char)3; def y = (double)5; return x < y")); @@ -258,6 +435,22 @@ public void testDefLtTypedRHS() { assertEquals(false, exec("def x = (float)6; int y = (int)2; return x < y")); assertEquals(false, exec("def x = (double)7; int y = (int)1; return x < y")); + assertEquals(true, exec("def x = (byte)1; long y = (long)7; return x < y")); + assertEquals(true, exec("def x = (short)2; long y = (long)6; return x < y")); + assertEquals(true, exec("def x = (char)3; long y = (long)5; return x < y")); + assertEquals(false, exec("def x = (int)4; long y = (long)4; return x < y")); + assertEquals(false, exec("def x = (long)5; long y = (long)3; return x < y")); + assertEquals(false, exec("def x = (float)6; long y = (long)2; return x < y")); + assertEquals(false, exec("def x = (double)7; long y = (long)1; return x < y")); + + assertEquals(true, exec("def x = (byte)1; float y = (float)7; return x < y")); + assertEquals(true, exec("def x = (short)2; float y = (float)6; return x < y")); + assertEquals(true, exec("def x = (char)3; float y = (float)5; return x < y")); + assertEquals(false, exec("def x = (int)4; float y = (float)4; return x < y")); + assertEquals(false, exec("def x = (long)5; float y = (float)3; return x < y")); + assertEquals(false, exec("def x = (float)6; float y = (float)2; return x < y")); + assertEquals(false, exec("def x = (double)7; float y = (float)1; return x < y")); + assertEquals(true, exec("def x = (byte)1; double y = (double)7; return x < y")); assertEquals(true, exec("def x = (short)2; double y = (double)6; return x < y")); assertEquals(true, exec("def x = (char)3; double y = (double)5; return x < y")); @@ -276,6 +469,22 @@ public void testDefLte() { assertEquals(false, exec("def x = (float)6; def y = (int)2; return x <= y")); assertEquals(false, exec("def x = (double)7; def y = (int)1; return x <= y")); + assertEquals(true, exec("def x = (byte)1; def y = (long)7; return x <= y")); + assertEquals(true, exec("def x = (short)2; def y = (long)6; return x <= y")); + assertEquals(true, exec("def x = (char)3; def y = (long)5; return x <= y")); + assertEquals(true, exec("def x = (int)4; def y = (long)4; return x <= y")); + assertEquals(false, exec("def x = (long)5; def y = (long)3; return x <= y")); + assertEquals(false, exec("def x = (float)6; def y = (long)2; return x <= y")); + assertEquals(false, exec("def x = (double)7; def y = (long)1; return x <= y")); + + assertEquals(true, exec("def x = (byte)1; def y = (float)7; return x <= y")); + assertEquals(true, exec("def x = (short)2; def y = (float)6; return x <= y")); + assertEquals(true, exec("def x = (char)3; def y = (float)5; return x <= y")); + assertEquals(true, exec("def x = (int)4; def y = (float)4; return x <= y")); + assertEquals(false, exec("def x = (long)5; def y = (float)3; return x <= y")); + assertEquals(false, exec("def x = (float)6; def y = (float)2; return x <= y")); + assertEquals(false, exec("def x = (double)7; def y = (float)1; return x <= y")); + assertEquals(true, exec("def x = (byte)1; def y = (double)7; return x <= y")); assertEquals(true, exec("def x = (short)2; def y = (double)6; return x <= y")); assertEquals(true, exec("def x = (char)3; def y = (double)5; return x <= y")); @@ -294,6 +503,22 @@ public void testDefLteTypedLHS() { assertEquals(false, exec("float x = (float)6; def y = (int)2; return x <= y")); assertEquals(false, exec("double x = (double)7; def y = (int)1; return x <= y")); + assertEquals(true, exec("byte x = (byte)1; def y = (long)7; return x <= y")); + assertEquals(true, exec("short x = (short)2; def y = (long)6; return x <= y")); + assertEquals(true, exec("char x = (char)3; def y = (long)5; return x <= y")); + assertEquals(true, exec("int x = (int)4; def y = (long)4; return x <= y")); + assertEquals(false, exec("long x = (long)5; def y = (long)3; return x <= y")); + assertEquals(false, exec("float x = (float)6; def y = (long)2; return x <= y")); + assertEquals(false, exec("double x = (double)7; def y = (long)1; return x <= y")); + + assertEquals(true, exec("byte x = (byte)1; def y = (float)7; return x <= y")); + assertEquals(true, exec("short x = (short)2; def y = (float)6; return x <= y")); + assertEquals(true, exec("char x = (char)3; def y = (float)5; return x <= y")); + assertEquals(true, exec("int x = (int)4; def y = (float)4; return x <= y")); + assertEquals(false, exec("long x = (long)5; def y = (float)3; return x <= y")); + assertEquals(false, exec("float x = (float)6; def y = (float)2; return x <= y")); + assertEquals(false, exec("double x = (double)7; def y = (float)1; return x <= y")); + assertEquals(true, exec("byte x = (byte)1; def y = (double)7; return x <= y")); assertEquals(true, exec("short x = (short)2; def y = (double)6; return x <= y")); assertEquals(true, exec("char x = (char)3; def y = (double)5; return x <= y")); @@ -312,6 +537,22 @@ public void testDefLteTypedRHS() { assertEquals(false, exec("def x = (float)6; int y = (int)2; return x <= y")); assertEquals(false, exec("def x = (double)7; int y = (int)1; return x <= y")); + assertEquals(true, exec("def x = (byte)1; long y = (long)7; return x <= y")); + assertEquals(true, exec("def x = (short)2; long y = (long)6; return x <= y")); + assertEquals(true, exec("def x = (char)3; long y = (long)5; return x <= y")); + assertEquals(true, exec("def x = (int)4; long y = (long)4; return x <= y")); + assertEquals(false, exec("def x = (long)5; long y = (long)3; return x <= y")); + assertEquals(false, exec("def x = (float)6; long y = (long)2; return x <= y")); + assertEquals(false, exec("def x = (double)7; long y = (long)1; return x <= y")); + + assertEquals(true, exec("def x = (byte)1; float y = (float)7; return x <= y")); + assertEquals(true, exec("def x = (short)2; float y = (float)6; return x <= y")); + assertEquals(true, exec("def x = (char)3; float y = (float)5; return x <= y")); + assertEquals(true, exec("def x = (int)4; float y = (float)4; return x <= y")); + assertEquals(false, exec("def x = (long)5; float y = (float)3; return x <= y")); + assertEquals(false, exec("def x = (float)6; float y = (float)2; return x <= y")); + assertEquals(false, exec("def x = (double)7; float y = (float)1; return x <= y")); + assertEquals(true, exec("def x = (byte)1; double y = (double)7; return x <= y")); assertEquals(true, exec("def x = (short)2; double y = (double)6; return x <= y")); assertEquals(true, exec("def x = (char)3; double y = (double)5; return x <= y")); @@ -330,6 +571,22 @@ public void testDefGt() { assertEquals(true, exec("def x = (float)6; def y = (int)2; return x > y")); assertEquals(true, exec("def x = (double)7; def y = (int)1; return x > y")); + assertEquals(false, exec("def x = (byte)1; def y = (long)7; return x > y")); + assertEquals(false, exec("def x = (short)2; def y = (long)6; return x > y")); + assertEquals(false, exec("def x = (char)3; def y = (long)5; return x > y")); + assertEquals(false, exec("def x = (int)4; def y = (long)4; return x > y")); + assertEquals(true, exec("def x = (long)5; def y = (long)3; return x > y")); + assertEquals(true, exec("def x = (float)6; def y = (long)2; return x > y")); + assertEquals(true, exec("def x = (double)7; def y = (long)1; return x > y")); + + assertEquals(false, exec("def x = (byte)1; def y = (float)7; return x > y")); + assertEquals(false, exec("def x = (short)2; def y = (float)6; return x > y")); + assertEquals(false, exec("def x = (char)3; def y = (float)5; return x > y")); + assertEquals(false, exec("def x = (int)4; def y = (float)4; return x > y")); + assertEquals(true, exec("def x = (long)5; def y = (float)3; return x > y")); + assertEquals(true, exec("def x = (float)6; def y = (float)2; return x > y")); + assertEquals(true, exec("def x = (double)7; def y = (float)1; return x > y")); + assertEquals(false, exec("def x = (byte)1; def y = (double)7; return x > y")); assertEquals(false, exec("def x = (short)2; def y = (double)6; return x > y")); assertEquals(false, exec("def x = (char)3; def y = (double)5; return x > y")); @@ -348,6 +605,22 @@ public void testDefGtTypedLHS() { assertEquals(true, exec("float x = (float)6; def y = (int)2; return x > y")); assertEquals(true, exec("double x = (double)7; def y = (int)1; return x > y")); + assertEquals(false, exec("byte x = (byte)1; def y = (long)7; return x > y")); + assertEquals(false, exec("short x = (short)2; def y = (long)6; return x > y")); + assertEquals(false, exec("char x = (char)3; def y = (long)5; return x > y")); + assertEquals(false, exec("int x = (int)4; def y = (long)4; return x > y")); + assertEquals(true, exec("long x = (long)5; def y = (long)3; return x > y")); + assertEquals(true, exec("float x = (float)6; def y = (long)2; return x > y")); + assertEquals(true, exec("double x = (double)7; def y = (long)1; return x > y")); + + assertEquals(false, exec("byte x = (byte)1; def y = (float)7; return x > y")); + assertEquals(false, exec("short x = (short)2; def y = (float)6; return x > y")); + assertEquals(false, exec("char x = (char)3; def y = (float)5; return x > y")); + assertEquals(false, exec("int x = (int)4; def y = (float)4; return x > y")); + assertEquals(true, exec("long x = (long)5; def y = (float)3; return x > y")); + assertEquals(true, exec("float x = (float)6; def y = (float)2; return x > y")); + assertEquals(true, exec("double x = (double)7; def y = (float)1; return x > y")); + assertEquals(false, exec("byte x = (byte)1; def y = (double)7; return x > y")); assertEquals(false, exec("short x = (short)2; def y = (double)6; return x > y")); assertEquals(false, exec("char x = (char)3; def y = (double)5; return x > y")); @@ -366,6 +639,22 @@ public void testDefGtTypedRHS() { assertEquals(true, exec("def x = (float)6; int y = (int)2; return x > y")); assertEquals(true, exec("def x = (double)7; int y = (int)1; return x > y")); + assertEquals(false, exec("def x = (byte)1; long y = (long)7; return x > y")); + assertEquals(false, exec("def x = (short)2; long y = (long)6; return x > y")); + assertEquals(false, exec("def x = (char)3; long y = (long)5; return x > y")); + assertEquals(false, exec("def x = (int)4; long y = (long)4; return x > y")); + assertEquals(true, exec("def x = (long)5; long y = (long)3; return x > y")); + assertEquals(true, exec("def x = (float)6; long y = (long)2; return x > y")); + assertEquals(true, exec("def x = (double)7; long y = (long)1; return x > y")); + + assertEquals(false, exec("def x = (byte)1; float y = (float)7; return x > y")); + assertEquals(false, exec("def x = (short)2; float y = (float)6; return x > y")); + assertEquals(false, exec("def x = (char)3; float y = (float)5; return x > y")); + assertEquals(false, exec("def x = (int)4; float y = (float)4; return x > y")); + assertEquals(true, exec("def x = (long)5; float y = (float)3; return x > y")); + assertEquals(true, exec("def x = (float)6; float y = (float)2; return x > y")); + assertEquals(true, exec("def x = (double)7; float y = (float)1; return x > y")); + assertEquals(false, exec("def x = (byte)1; double y = (double)7; return x > y")); assertEquals(false, exec("def x = (short)2; double y = (double)6; return x > y")); assertEquals(false, exec("def x = (char)3; double y = (double)5; return x > y")); @@ -384,6 +673,22 @@ public void testDefGte() { assertEquals(true, exec("def x = (float)6; def y = (int)2; return x >= y")); assertEquals(true, exec("def x = (double)7; def y = (int)1; return x >= y")); + assertEquals(false, exec("def x = (byte)1; def y = (long)7; return x >= y")); + assertEquals(false, exec("def x = (short)2; def y = (long)6; return x >= y")); + assertEquals(false, exec("def x = (char)3; def y = (long)5; return x >= y")); + assertEquals(true, exec("def x = (int)4; def y = (long)4; return x >= y")); + assertEquals(true, exec("def x = (long)5; def y = (long)3; return x >= y")); + assertEquals(true, exec("def x = (float)6; def y = (long)2; return x >= y")); + assertEquals(true, exec("def x = (double)7; def y = (long)1; return x >= y")); + + assertEquals(false, exec("def x = (byte)1; def y = (float)7; return x >= y")); + assertEquals(false, exec("def x = (short)2; def y = (float)6; return x >= y")); + assertEquals(false, exec("def x = (char)3; def y = (float)5; return x >= y")); + assertEquals(true, exec("def x = (int)4; def y = (float)4; return x >= y")); + assertEquals(true, exec("def x = (long)5; def y = (float)3; return x >= y")); + assertEquals(true, exec("def x = (float)6; def y = (float)2; return x >= y")); + assertEquals(true, exec("def x = (double)7; def y = (float)1; return x >= y")); + assertEquals(false, exec("def x = (byte)1; def y = (double)7; return x >= y")); assertEquals(false, exec("def x = (short)2; def y = (double)6; return x >= y")); assertEquals(false, exec("def x = (char)3; def y = (double)5; return x >= y")); @@ -402,6 +707,22 @@ public void testDefGteTypedLHS() { assertEquals(true, exec("float x = (float)6; def y = (int)2; return x >= y")); assertEquals(true, exec("double x = (double)7; def y = (int)1; return x >= y")); + assertEquals(false, exec("byte x = (byte)1; def y = (long)7; return x >= y")); + assertEquals(false, exec("short x = (short)2; def y = (long)6; return x >= y")); + assertEquals(false, exec("char x = (char)3; def y = (long)5; return x >= y")); + assertEquals(true, exec("int x = (int)4; def y = (long)4; return x >= y")); + assertEquals(true, exec("long x = (long)5; def y = (long)3; return x >= y")); + assertEquals(true, exec("float x = (float)6; def y = (long)2; return x >= y")); + assertEquals(true, exec("double x = (double)7; def y = (long)1; return x >= y")); + + assertEquals(false, exec("byte x = (byte)1; def y = (float)7; return x >= y")); + assertEquals(false, exec("short x = (short)2; def y = (float)6; return x >= y")); + assertEquals(false, exec("char x = (char)3; def y = (float)5; return x >= y")); + assertEquals(true, exec("int x = (int)4; def y = (float)4; return x >= y")); + assertEquals(true, exec("long x = (long)5; def y = (float)3; return x >= y")); + assertEquals(true, exec("float x = (float)6; def y = (float)2; return x >= y")); + assertEquals(true, exec("double x = (double)7; def y = (float)1; return x >= y")); + assertEquals(false, exec("byte x = (byte)1; def y = (double)7; return x >= y")); assertEquals(false, exec("short x = (short)2; def y = (double)6; return x >= y")); assertEquals(false, exec("char x = (char)3; def y = (double)5; return x >= y")); @@ -420,6 +741,22 @@ public void testDefGteTypedRHS() { assertEquals(true, exec("def x = (float)6; int y = (int)2; return x >= y")); assertEquals(true, exec("def x = (double)7; int y = (int)1; return x >= y")); + assertEquals(false, exec("def x = (byte)1; long y = (long)7; return x >= y")); + assertEquals(false, exec("def x = (short)2; long y = (long)6; return x >= y")); + assertEquals(false, exec("def x = (char)3; long y = (long)5; return x >= y")); + assertEquals(true, exec("def x = (int)4; long y = (long)4; return x >= y")); + assertEquals(true, exec("def x = (long)5; long y = (long)3; return x >= y")); + assertEquals(true, exec("def x = (float)6; long y = (long)2; return x >= y")); + assertEquals(true, exec("def x = (double)7; long y = (long)1; return x >= y")); + + assertEquals(false, exec("def x = (byte)1; float y = (float)7; return x >= y")); + assertEquals(false, exec("def x = (short)2; float y = (float)6; return x >= y")); + assertEquals(false, exec("def x = (char)3; float y = (float)5; return x >= y")); + assertEquals(true, exec("def x = (int)4; float y = (float)4; return x >= y")); + assertEquals(true, exec("def x = (long)5; float y = (float)3; return x >= y")); + assertEquals(true, exec("def x = (float)6; float y = (float)2; return x >= y")); + assertEquals(true, exec("def x = (double)7; float y = (float)1; return x >= y")); + assertEquals(false, exec("def x = (byte)1; double y = (double)7; return x >= y")); assertEquals(false, exec("def x = (short)2; double y = (double)6; return x >= y")); assertEquals(false, exec("def x = (char)3; double y = (double)5; return x >= y")); From f2c20c87f4520132f7a7acb2450afffda767890b Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Mon, 12 Dec 2022 15:58:19 +0100 Subject: [PATCH 236/919] Diff the list of filenames that are added by each new commit (#92238) When a CommitsListener is defined the CombinedDeletionPolicy also computes the set of new files added by the last commit before passing it to the listener. The list is computed outside the lock by diffing the list of file names between the previous last commit and the new last commit. --- docs/changelog/92238.yaml | 5 + .../index/engine/CombinedDeletionPolicy.java | 16 +- .../elasticsearch/index/engine/Engine.java | 11 +- .../index/engine/InternalEngine.java | 4 +- .../elasticsearch/index/IndexModuleTests.java | 2 +- .../engine/CombinedDeletionPolicyTests.java | 170 +++++++++++++++++- .../index/engine/InternalEngineTests.java | 2 +- 7 files changed, 191 insertions(+), 19 deletions(-) create mode 100644 docs/changelog/92238.yaml diff --git a/docs/changelog/92238.yaml b/docs/changelog/92238.yaml new file mode 100644 index 000000000000..c6d5128452bc --- /dev/null +++ b/docs/changelog/92238.yaml @@ -0,0 +1,5 @@ +pr: 92238 +summary: Diff the list of filenames that are added by each new commit +area: Engine +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/index/engine/CombinedDeletionPolicy.java b/server/src/main/java/org/elasticsearch/index/engine/CombinedDeletionPolicy.java index 77a72b27057c..6f5716c88031 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/CombinedDeletionPolicy.java +++ b/server/src/main/java/org/elasticsearch/index/engine/CombinedDeletionPolicy.java @@ -22,10 +22,13 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.function.LongSupplier; +import java.util.stream.Collectors; /** * An {@link IndexDeletionPolicy} that coordinates between Lucene's commits and the retention of translog generation files, @@ -43,7 +46,7 @@ public class CombinedDeletionPolicy extends IndexDeletionPolicy { interface CommitsListener { - void onNewAcquiredCommit(IndexCommit commit); + void onNewAcquiredCommit(IndexCommit commit, Set additionalFiles); void onDeletedCommit(IndexCommit commit); } @@ -102,13 +105,14 @@ public void onCommit(List commits) throws IOException { totalDocsOfSafeCommit = safeCommitInfo.docCount; } IndexCommit newCommit = null; + IndexCommit previousLastCommit = null; List deletedCommits = null; synchronized (this) { this.safeCommitInfo = new SafeCommitInfo( Long.parseLong(safeCommit.getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)), totalDocsOfSafeCommit ); - final IndexCommit previousLastCommit = this.lastCommit; + previousLastCommit = this.lastCommit; this.lastCommit = commits.get(commits.size() - 1); this.safeCommit = safeCommit; updateRetentionPolicy(); @@ -134,7 +138,8 @@ public void onCommit(List commits) throws IOException { assert assertSafeCommitUnchanged(safeCommit); if (commitsListener != null) { if (newCommit != null) { - commitsListener.onNewAcquiredCommit(newCommit); + final Set additionalFiles = listOfNewFileNames(previousLastCommit, newCommit); + commitsListener.onNewAcquiredCommit(newCommit, additionalFiles); } if (deletedCommits != null) { for (IndexCommit deletedCommit : deletedCommits) { @@ -266,6 +271,11 @@ private static int indexOfKeptCommits(List commits, long return 0; } + private Set listOfNewFileNames(IndexCommit previous, IndexCommit current) throws IOException { + final Set previousFiles = previous != null ? new HashSet<>(previous.getFileNames()) : Set.of(); + return current.getFileNames().stream().filter(f -> previousFiles.contains(f) == false).collect(Collectors.toUnmodifiableSet()); + } + /** * Checks whether the deletion policy is holding on to snapshotted commits */ diff --git a/server/src/main/java/org/elasticsearch/index/engine/Engine.java b/server/src/main/java/org/elasticsearch/index/engine/Engine.java index d97849195372..82974275effa 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/Engine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/Engine.java @@ -222,16 +222,17 @@ public interface IndexCommitListener { * {@link IndexCommitRef} files to be deleted from disk until the reference is closed. As such, the listener must close the * reference as soon as it is done with it. * - * @param shardId the {@link ShardId} of shard - * @param primaryTerm the shard's primary term value - * @param indexCommitRef a reference on the newly created index commit + * @param shardId the {@link ShardId} of shard + * @param primaryTerm the shard's primary term value + * @param indexCommitRef a reference on the newly created index commit + * @param additionalFiles the set of filenames that are added by the new commit */ - void onNewCommit(ShardId shardId, long primaryTerm, Engine.IndexCommitRef indexCommitRef); + void onNewCommit(ShardId shardId, long primaryTerm, IndexCommitRef indexCommitRef, Set additionalFiles); /** * This method is invoked after the policy deleted the given {@link IndexCommit}. A listener is never notified of a deleted commit * until the corresponding {@link Engine.IndexCommitRef} received through - * {@link #onNewCommit(ShardId, long, IndexCommitRef)} has been closed; closing which in turn can call this method directly. + * {@link #onNewCommit(ShardId, long, IndexCommitRef, Set)} has been closed; closing which in turn can call this method directly. * * @param shardId the {@link ShardId} of shard * @param deletedCommit the deleted {@link IndexCommit} diff --git a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java index 407e99df3971..2cd30e4dcb2d 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -331,10 +331,10 @@ private CombinedDeletionPolicy.CommitsListener newCommitsListener() { var primaryTerm = config().getPrimaryTermSupplier().getAsLong(); return new CombinedDeletionPolicy.CommitsListener() { @Override - public void onNewAcquiredCommit(final IndexCommit commit) { + public void onNewAcquiredCommit(final IndexCommit commit, final Set additionalFiles) { final IndexCommitRef indexCommitRef = acquireIndexCommitRef(() -> commit); assert indexCommitRef.getIndexCommit() == commit; - listener.onNewCommit(shardId, primaryTerm, indexCommitRef); + listener.onNewCommit(shardId, primaryTerm, indexCommitRef, additionalFiles); } @Override diff --git a/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java b/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java index d5d7cd504bc6..fc644953d1f3 100644 --- a/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java +++ b/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java @@ -644,7 +644,7 @@ public void testIndexCommitListenerIsBound() throws IOException, ExecutionExcept module.setIndexCommitListener(new Engine.IndexCommitListener() { @Override - public void onNewCommit(ShardId shardId, long primaryTerm, Engine.IndexCommitRef indexCommitRef) { + public void onNewCommit(ShardId shardId, long primaryTerm, Engine.IndexCommitRef indexCommitRef, Set additionalFiles) { lastAcquiredPrimaryTerm.set(primaryTerm); lastAcquiredCommit.set(indexCommitRef); } diff --git a/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java b/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java index 9dbe040168e3..b50251aef011 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java @@ -20,17 +20,22 @@ import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import static org.elasticsearch.index.seqno.SequenceNumbers.NO_OPS_PERFORMED; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.not; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -273,11 +278,15 @@ public void testCheckUnreferencedCommits() throws Exception { public void testCommitsListener() throws Exception { final List acquiredCommits = new ArrayList<>(); final List deletedCommits = new ArrayList<>(); + final Set newCommitFiles = new HashSet<>(); final CombinedDeletionPolicy.CommitsListener commitsListener = new CombinedDeletionPolicy.CommitsListener() { @Override - public void onNewAcquiredCommit(IndexCommit commit) { + public void onNewAcquiredCommit(IndexCommit commit, Set additionalFiles) { assertThat(commit, instanceOf(FilterIndexCommit.class)); assertThat(acquiredCommits.add(((FilterIndexCommit) commit).getIndexCommit()), equalTo(true)); + assertThat(additionalFiles, not(empty())); + newCommitFiles.clear(); + newCommitFiles.addAll(additionalFiles); } @Override @@ -315,44 +324,148 @@ synchronized boolean releaseCommit(IndexCommit indexCommit) { }; final UUID translogUUID = UUID.randomUUID(); - final IndexCommit commit0 = mockIndexCommit(NO_OPS_PERFORMED, NO_OPS_PERFORMED, translogUUID); + final IndexCommit commit0 = mockIndexCommit(NO_OPS_PERFORMED, NO_OPS_PERFORMED, translogUUID, Set.of("segments_0")); combinedDeletionPolicy.onInit(List.of(commit0)); assertThat(acquiredCommits, contains(commit0)); assertThat(deletedCommits, hasSize(0)); + assertThat(newCommitFiles, contains("segments_0")); - final IndexCommit commit1 = mockIndexCommit(10L, 10L, translogUUID); + final IndexCommit commit1 = mockIndexCommit(10L, 10L, translogUUID, Set.of("_0.cfe", "_0.si", "_0.cfs", "segments_1")); combinedDeletionPolicy.onCommit(List.of(commit0, commit1)); assertThat(acquiredCommits, contains(commit0, commit1)); assertThat(deletedCommits, hasSize(0)); + assertThat(newCommitFiles, containsInAnyOrder(equalTo("_0.cfe"), equalTo("_0.si"), equalTo("_0.cfs"), equalTo("segments_1"))); globalCheckpoint.set(10L); - final IndexCommit commit2 = mockIndexCommit(20L, 20L, translogUUID); + final IndexCommit commit2 = mockIndexCommit( + 20L, + 20L, + translogUUID, + Set.of("_1.cfs", "_0.cfe", "_0.si", "_1.cfe", "_1.si", "_0.cfs", "segments_2") + ); combinedDeletionPolicy.onCommit(List.of(commit0, commit1, commit2)); assertThat(acquiredCommits, contains(commit0, commit1, commit2)); assertThat(deletedCommits, hasSize(0)); + assertThat(newCommitFiles, containsInAnyOrder(equalTo("_1.cfe"), equalTo("_1.si"), equalTo("_1.cfs"), equalTo("segments_2"))); boolean maybeCleanUpCommits = combinedDeletionPolicy.releaseCommit(commit0); assertThat(maybeCleanUpCommits, equalTo(true)); globalCheckpoint.set(20L); - final IndexCommit commit3 = mockIndexCommit(30L, 30L, translogUUID); + final IndexCommit commit3 = mockIndexCommit( + 30L, + 30L, + translogUUID, + Set.of( + "_3.fdx", + "_3_Lucene90_0.tip", + "_3_Lucene90_0.dvm", + "_3.si", + "_3_0.tmd", + "_3_0.tim", + "_3_ES85BloomFilter_0.bfi", + "_3_0.pos", + "_3_ES85BloomFilter_0.bfm", + "_3_0.tip", + "_3_Lucene90_0.doc", + "_3.nvd", + "_3.nvm", + "_3.fnm", + "_3_0.doc", + "segments_3", + "_3.kdd", + "_3_Lucene90_0.tmd", + "_3.fdm", + "_3.kdi", + "_3_Lucene90_0.dvd", + "_3_Lucene90_0.pos", + "_3.kdm", + "_3.fdt", + "_3_Lucene90_0.tim" + ) + ); combinedDeletionPolicy.onCommit(List.of(commit0, commit1, commit2, commit3)); assertThat(acquiredCommits, contains(commit1, commit2, commit3)); assertThat(deletedCommits, contains(commit0)); + assertThat( + newCommitFiles, + containsInAnyOrder( + equalTo("_3.fdx"), + equalTo("_3_Lucene90_0.tip"), + equalTo("_3_Lucene90_0.dvm"), + equalTo("_3.si"), + equalTo("_3_0.tmd"), + equalTo("_3_0.tim"), + equalTo("_3_ES85BloomFilter_0.bfi"), + equalTo("_3_0.pos"), + equalTo("_3_ES85BloomFilter_0.bfm"), + equalTo("_3_0.tip"), + equalTo("_3_Lucene90_0.doc"), + equalTo("_3.nvd"), + equalTo("_3.nvm"), + equalTo("_3.fnm"), + equalTo("_3_0.doc"), + equalTo("segments_3"), + equalTo("_3.kdd"), + equalTo("_3_Lucene90_0.tmd"), + equalTo("_3.fdm"), + equalTo("_3.kdi"), + equalTo("_3_Lucene90_0.dvd"), + equalTo("_3_Lucene90_0.pos"), + equalTo("_3.kdm"), + equalTo("_3.fdt"), + equalTo("_3_Lucene90_0.tim") + ) + ); maybeCleanUpCommits = combinedDeletionPolicy.releaseCommit(commit2); assertThat("No commits to clean up (commit #2 is the safe commit)", maybeCleanUpCommits, equalTo(false)); globalCheckpoint.set(30L); - final IndexCommit commit4 = mockIndexCommit(40L, 40L, translogUUID); + final IndexCommit commit4 = mockIndexCommit( + 40L, + 40L, + translogUUID, + Set.of( + "_3.fdx", + "_3_Lucene90_0.tip", + "_3_Lucene90_0.dvm", + "_3.si", + "_3_0.tmd", + "_3_0.tim", + "_3_ES85BloomFilter_0.bfi", + "_3_0.pos", + "_3_ES85BloomFilter_0.bfm", + "_3_0.tip", + "_3_Lucene90_0.doc", + "_3.nvd", + "_4.cfe", + "_3.nvm", + "_3.fnm", + "_3_0.doc", + "segments_4", + "_3.kdd", + "_3_Lucene90_0.tmd", + "_3.fdm", + "_3.kdi", + "_4.cfs", + "_3_Lucene90_0.dvd", + "_3_Lucene90_0.pos", + "_3.kdm", + "_4.si", + "_3.fdt", + "_3_Lucene90_0.tim" + ) + ); combinedDeletionPolicy.onCommit(List.of(commit1, commit2, commit3, commit4)); assertThat(acquiredCommits, contains(commit1, commit3, commit4)); assertThat(deletedCommits, contains(commit0, commit2)); + assertThat(newCommitFiles, containsInAnyOrder(equalTo("_4.cfe"), equalTo("_4.si"), equalTo("_4.cfs"), equalTo("segments_4"))); maybeCleanUpCommits = combinedDeletionPolicy.releaseCommit(commit3); assertThat("No commits to clean up (commit #3 is the safe commit)", maybeCleanUpCommits, equalTo(false)); @@ -366,7 +479,44 @@ synchronized boolean releaseCommit(IndexCommit indexCommit) { final boolean globalCheckpointCatchUp = randomBoolean(); globalCheckpoint.set(globalCheckpointCatchUp ? 50L : 40L); - final IndexCommit commit5 = mockIndexCommit(50L, 50L, translogUUID); + final IndexCommit commit5 = mockIndexCommit( + 50L, + 50L, + translogUUID, + Set.of( + "_3.fdx", + "_3_Lucene90_0.tip", + "_3_Lucene90_0.dvm", + "_3.si", + "_3_0.tmd", + "_3_0.tim", + "_3_ES85BloomFilter_0.bfi", + "_3_0.pos", + "_3_ES85BloomFilter_0.bfm", + "_3_0.tip", + "_5.si", + "_3_Lucene90_0.doc", + "segments_5", + "_3.nvd", + "_5.cfs", + "_4.cfe", + "_3.nvm", + "_3.fnm", + "_3_0.doc", + "_3.kdd", + "_3_Lucene90_0.tmd", + "_3.fdm", + "_3.kdi", + "_5.cfe", + "_4.cfs", + "_3_Lucene90_0.dvd", + "_3_Lucene90_0.pos", + "_3.kdm", + "_4.si", + "_3.fdt", + "_3_Lucene90_0.tim" + ) + ); combinedDeletionPolicy.onCommit(List.of(commit1, commit3, commit4, commit5)); if (globalCheckpointCatchUp) { @@ -376,6 +526,7 @@ synchronized boolean releaseCommit(IndexCommit indexCommit) { assertThat(acquiredCommits, contains(commit4, commit5)); assertThat(deletedCommits, contains(commit0, commit2, commit1, commit3)); } + assertThat(newCommitFiles, containsInAnyOrder(equalTo("_5.cfe"), equalTo("_5.si"), equalTo("_5.cfs"), equalTo("segments_5"))); maybeCleanUpCommits = combinedDeletionPolicy.releaseCommit(commit5); assertThat("No commits to clean up (commit #5 is the last commit)", maybeCleanUpCommits, equalTo(false)); @@ -399,6 +550,10 @@ protected int getDocCountOfCommit(IndexCommit indexCommit) throws IOException { } IndexCommit mockIndexCommit(long localCheckpoint, long maxSeqNo, UUID translogUUID) throws IOException { + return mockIndexCommit(localCheckpoint, maxSeqNo, translogUUID, Set.of()); + } + + IndexCommit mockIndexCommit(long localCheckpoint, long maxSeqNo, UUID translogUUID, Set fileNames) throws IOException { final Map userData = new HashMap<>(); userData.put(SequenceNumbers.LOCAL_CHECKPOINT_KEY, Long.toString(localCheckpoint)); userData.put(SequenceNumbers.MAX_SEQ_NO, Long.toString(maxSeqNo)); @@ -407,6 +562,7 @@ IndexCommit mockIndexCommit(long localCheckpoint, long maxSeqNo, UUID translogUU final Directory directory = mock(Directory.class); when(commit.getUserData()).thenReturn(userData); when(commit.getDirectory()).thenReturn(directory); + when(commit.getFileNames()).thenReturn(fileNames); resetDeletion(commit); return commit; } diff --git a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java index 804a2e29b4d2..4f0441ef4f5f 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -7482,7 +7482,7 @@ public void testIndexCommitsListener() throws Exception { final Engine.IndexCommitListener indexCommitListener = new Engine.IndexCommitListener() { @Override - public void onNewCommit(ShardId shardId, long primaryTerm, Engine.IndexCommitRef indexCommitRef) { + public void onNewCommit(ShardId shardId, long primaryTerm, Engine.IndexCommitRef indexCommitRef, Set additionalFiles) { assertThat(acquiredCommits.put(indexCommitRef.getIndexCommit(), indexCommitRef), nullValue()); assertThat(shardId, equalTo(InternalEngineTests.this.shardId)); assertThat(primaryTerm, greaterThanOrEqualTo(0L)); From 0f9ccbd30ee0294fbec906c76e476731af796195 Mon Sep 17 00:00:00 2001 From: Abdon Pijpelink Date: Mon, 12 Dec 2022 16:06:16 +0100 Subject: [PATCH 237/919] [DOCS] Add missing privilege to bulk prerequisites (#92237) --- docs/reference/docs/bulk.asciidoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/reference/docs/bulk.asciidoc b/docs/reference/docs/bulk.asciidoc index 15814efde309..d90026073990 100644 --- a/docs/reference/docs/bulk.asciidoc +++ b/docs/reference/docs/bulk.asciidoc @@ -49,6 +49,9 @@ privilege. ** To automatically create a data stream or index with a bulk API request, you must have the `auto_configure`, `create_index`, or `manage` index privilege. +** To make the result of a bulk operation visible to search using the `refresh` +parameter, you must have the `maintenance` or `manage` index privilege. + * Automatic data stream creation requires a matching index template with data stream enabled. See <>. From f8636c6313686f6b871458fe5b792fe2b1b2a1eb Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 12 Dec 2022 15:26:26 +0000 Subject: [PATCH 238/919] Fix ref-counting in DisruptableMockTransport (#92245) Today `DisruptableMockTransport` leaks refs to transport messages in various ways if the transport is rebooted. This commit adds the missing ref-count handling. Closes #91837 --- .../core/AbstractRefCounted.java | 2 +- .../CleanableResponseHandler.java | 33 ++ .../coordination/JoinValidationService.java | 7 +- .../PublicationTransportHandler.java | 7 +- .../transport/TransportService.java | 4 + .../coordination/CoordinatorTests.java | 3 +- .../snapshots/SnapshotResiliencyTests.java | 4 +- .../AbstractCoordinatorTestCase.java | 10 +- .../test/transport/MockTransport.java | 18 +- .../DisruptableMockTransport.java | 89 +++--- .../DisruptableMockTransportTests.java | 293 +++++++++++++++--- 11 files changed, 374 insertions(+), 96 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/cluster/coordination/CleanableResponseHandler.java rename test/framework/src/main/java/org/elasticsearch/{test/disruption => transport}/DisruptableMockTransport.java (85%) rename test/framework/src/test/java/org/elasticsearch/{test/disruption => transport}/DisruptableMockTransportTests.java (64%) diff --git a/libs/core/src/main/java/org/elasticsearch/core/AbstractRefCounted.java b/libs/core/src/main/java/org/elasticsearch/core/AbstractRefCounted.java index 5f5331c55549..d77fbcc0bc12 100644 --- a/libs/core/src/main/java/org/elasticsearch/core/AbstractRefCounted.java +++ b/libs/core/src/main/java/org/elasticsearch/core/AbstractRefCounted.java @@ -47,7 +47,7 @@ public final boolean tryIncRef() { public final boolean decRef() { touch(); int i = refCount.decrementAndGet(); - assert i >= 0; + assert i >= 0 : "invalid decRef call: already closed"; if (i == 0) { try { closeInternal(); diff --git a/server/src/main/java/org/elasticsearch/cluster/coordination/CleanableResponseHandler.java b/server/src/main/java/org/elasticsearch/cluster/coordination/CleanableResponseHandler.java new file mode 100644 index 000000000000..ff1dafb99c4f --- /dev/null +++ b/server/src/main/java/org/elasticsearch/cluster/coordination/CleanableResponseHandler.java @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.cluster.coordination; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionListenerResponseHandler; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportResponse; + +/** + * Combines an ActionListenerResponseHandler with an ActionListener.runAfter action, but with an explicit type so that tests that simulate + * reboots can release resources without invoking the listener. + */ +public class CleanableResponseHandler extends ActionListenerResponseHandler { + private final Runnable cleanup; + + public CleanableResponseHandler(ActionListener listener, Writeable.Reader reader, String executor, Runnable cleanup) { + super(ActionListener.runAfter(listener, cleanup), reader, executor); + this.cleanup = cleanup; + } + + public void runCleanup() { + assert ThreadPool.assertCurrentThreadPool(); // should only be called from tests which simulate abrupt node restarts + cleanup.run(); + } +} diff --git a/server/src/main/java/org/elasticsearch/cluster/coordination/JoinValidationService.java b/server/src/main/java/org/elasticsearch/cluster/coordination/JoinValidationService.java index dee938d5e0bb..f2dde93f16e8 100644 --- a/server/src/main/java/org/elasticsearch/cluster/coordination/JoinValidationService.java +++ b/server/src/main/java/org/elasticsearch/cluster/coordination/JoinValidationService.java @@ -310,10 +310,11 @@ protected void doRun() throws Exception { JOIN_VALIDATE_ACTION_NAME, new BytesTransportRequest(bytes, discoveryNode.getVersion()), REQUEST_OPTIONS, - new ActionListenerResponseHandler<>( - ActionListener.runAfter(listener, bytes::decRef), + new CleanableResponseHandler<>( + listener, in -> TransportResponse.Empty.INSTANCE, - ThreadPool.Names.CLUSTER_COORDINATION + ThreadPool.Names.CLUSTER_COORDINATION, + bytes::decRef ) ); if (cachedBytes == null) { diff --git a/server/src/main/java/org/elasticsearch/cluster/coordination/PublicationTransportHandler.java b/server/src/main/java/org/elasticsearch/cluster/coordination/PublicationTransportHandler.java index ce92c98d6f8f..526eac3f2687 100644 --- a/server/src/main/java/org/elasticsearch/cluster/coordination/PublicationTransportHandler.java +++ b/server/src/main/java/org/elasticsearch/cluster/coordination/PublicationTransportHandler.java @@ -12,7 +12,6 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.ActionListenerResponseHandler; import org.elasticsearch.action.ActionRunnable; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStatePublicationEvent; @@ -470,11 +469,7 @@ private void sendClusterState( new BytesTransportRequest(bytes, destination.getVersion()), task, STATE_REQUEST_OPTIONS, - new ActionListenerResponseHandler<>( - ActionListener.runAfter(listener, bytes::decRef), - PublishWithJoinResponse::new, - ThreadPool.Names.CLUSTER_COORDINATION - ) + new CleanableResponseHandler<>(listener, PublishWithJoinResponse::new, ThreadPool.Names.CLUSTER_COORDINATION, bytes::decRef) ); } diff --git a/server/src/main/java/org/elasticsearch/transport/TransportService.java b/server/src/main/java/org/elasticsearch/transport/TransportService.java index df7b15f817c7..51781077e1c3 100644 --- a/server/src/main/java/org/elasticsearch/transport/TransportService.java +++ b/server/src/main/java/org/elasticsearch/transport/TransportService.java @@ -1387,6 +1387,10 @@ void setTimeoutHandler(TimeoutHandler timeoutHandler) { this.handler = timeoutHandler; } + // for tests + TransportResponseHandler unwrap() { + return delegate; + } } static class DirectResponseChannel implements TransportChannel { diff --git a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java index e2dc48e6bd6e..eb275cdc487e 100644 --- a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java @@ -1634,8 +1634,7 @@ public void testClusterCannotFormWithFailingJoinValidation() { reason = "test includes assertions about JoinHelper logging", value = "org.elasticsearch.cluster.coordination.JoinHelper:INFO" ) - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/91837") - public void testCannotJoinClusterWithDifferentUUID() throws IllegalAccessException { + public void testCannotJoinClusterWithDifferentUUID() { try (Cluster cluster1 = new Cluster(randomIntBetween(1, 3))) { cluster1.runRandomly(); cluster1.stabilise(); diff --git a/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java b/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java index 5ea3fe970ec8..b38348748ad8 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java +++ b/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java @@ -174,10 +174,10 @@ import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.fetch.FetchPhase; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.test.disruption.DisruptableMockTransport; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.tracing.Tracer; import org.elasticsearch.transport.BytesRefRecycler; +import org.elasticsearch.transport.DisruptableMockTransport; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.junit.After; @@ -1652,7 +1652,7 @@ protected void connectToNodesAndWait(ClusterState newClusterState) { } ); recoverySettings = new RecoverySettings(settings, clusterSettings); - mockTransport = new DisruptableMockTransport(node, logger, deterministicTaskQueue) { + mockTransport = new DisruptableMockTransport(node, deterministicTaskQueue) { @Override protected ConnectionStatus getConnectionStatus(DiscoveryNode destination) { if (node.equals(destination)) { diff --git a/test/framework/src/main/java/org/elasticsearch/cluster/coordination/AbstractCoordinatorTestCase.java b/test/framework/src/main/java/org/elasticsearch/cluster/coordination/AbstractCoordinatorTestCase.java index 92200734b646..9002b60276ff 100644 --- a/test/framework/src/main/java/org/elasticsearch/cluster/coordination/AbstractCoordinatorTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/cluster/coordination/AbstractCoordinatorTestCase.java @@ -71,11 +71,11 @@ import org.elasticsearch.monitor.NodeHealthService; import org.elasticsearch.monitor.StatusInfo; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.test.disruption.DisruptableMockTransport; -import org.elasticsearch.test.disruption.DisruptableMockTransport.ConnectionStatus; import org.elasticsearch.threadpool.Scheduler; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.BytesRefRecycler; +import org.elasticsearch.transport.DisruptableMockTransport; +import org.elasticsearch.transport.DisruptableMockTransport.ConnectionStatus; import org.elasticsearch.transport.TransportInterceptor; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.transport.TransportRequestOptions; @@ -1140,7 +1140,7 @@ public class ClusterNode { private void setUp() { final ThreadPool threadPool = deterministicTaskQueue.getThreadPool(this::onNode); clearableRecycler = new ClearableRecycler(recycler); - mockTransport = new DisruptableMockTransport(localNode, logger, deterministicTaskQueue) { + mockTransport = new DisruptableMockTransport(localNode, deterministicTaskQueue) { @Override protected void execute(Runnable runnable) { deterministicTaskQueue.scheduleNow(onNode(runnable)); @@ -1395,13 +1395,13 @@ Runnable onNode(Runnable runnable) { public void run() { if (clusterNodes.contains(ClusterNode.this)) { wrapped.run(); - } else if (runnable instanceof DisruptableMockTransport.RebootSensitiveRunnable) { + } else if (runnable instanceof DisruptableMockTransport.RebootSensitiveRunnable rebootSensitiveRunnable) { logger.trace( "completing reboot-sensitive runnable {} from node {} as node has been removed from cluster", runnable, localNode ); - ((DisruptableMockTransport.RebootSensitiveRunnable) runnable).ifRebooted(); + rebootSensitiveRunnable.ifRebooted(); } else { logger.trace("ignoring runnable {} from node {} as node has been removed from cluster", runnable, localNode); } diff --git a/test/framework/src/main/java/org/elasticsearch/test/transport/MockTransport.java b/test/framework/src/main/java/org/elasticsearch/test/transport/MockTransport.java index 72aab46b452c..efab83f84a2c 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/transport/MockTransport.java +++ b/test/framework/src/main/java/org/elasticsearch/test/transport/MockTransport.java @@ -89,8 +89,7 @@ public MockTransport() { */ @SuppressWarnings("unchecked") public void handleResponse(final long requestId, final Response response) { - final TransportResponseHandler transportResponseHandler = (TransportResponseHandler) getResponseHandlers() - .onResponseReceived(requestId, listener); + final TransportResponseHandler transportResponseHandler = getTransportResponseHandler(requestId); if (transportResponseHandler != null) { final Response deliveredResponse; try (BytesStreamOutput output = new BytesStreamOutput()) { @@ -100,8 +99,14 @@ public void handleResponse(final long reque ); } catch (IOException | UnsupportedOperationException e) { throw new AssertionError("failed to serialize/deserialize response " + response, e); + } finally { + response.decRef(); + } + try { + transportResponseHandler.handleResponse(deliveredResponse); + } finally { + deliveredResponse.decRef(); } - transportResponseHandler.handleResponse(deliveredResponse); } } @@ -154,12 +159,17 @@ public void handleRemoteError(final long requestId, final Throwable t) { * @param e the failure */ public void handleError(final long requestId, final TransportException e) { - final TransportResponseHandler transportResponseHandler = getResponseHandlers().onResponseReceived(requestId, listener); + final TransportResponseHandler transportResponseHandler = getTransportResponseHandler(requestId); if (transportResponseHandler != null) { transportResponseHandler.handleException(e); } } + @SuppressWarnings("unchecked") + public TransportResponseHandler getTransportResponseHandler(long requestId) { + return (TransportResponseHandler) getResponseHandlers().onResponseReceived(requestId, listener); + } + public Connection createConnection(DiscoveryNode node) { return new CloseableConnection() { @Override diff --git a/test/framework/src/main/java/org/elasticsearch/test/disruption/DisruptableMockTransport.java b/test/framework/src/main/java/org/elasticsearch/transport/DisruptableMockTransport.java similarity index 85% rename from test/framework/src/main/java/org/elasticsearch/test/disruption/DisruptableMockTransport.java rename to test/framework/src/main/java/org/elasticsearch/transport/DisruptableMockTransport.java index e939cbda3eca..a4d42b0306f9 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/disruption/DisruptableMockTransport.java +++ b/test/framework/src/main/java/org/elasticsearch/transport/DisruptableMockTransport.java @@ -5,11 +5,13 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -package org.elasticsearch.test.disruption; +package org.elasticsearch.transport; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.cluster.coordination.CleanableResponseHandler; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; @@ -21,19 +23,6 @@ import org.elasticsearch.test.transport.MockTransport; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.tracing.Tracer; -import org.elasticsearch.transport.CloseableConnection; -import org.elasticsearch.transport.ConnectTransportException; -import org.elasticsearch.transport.ConnectionProfile; -import org.elasticsearch.transport.NodeNotConnectedException; -import org.elasticsearch.transport.RemoteTransportException; -import org.elasticsearch.transport.RequestHandlerRegistry; -import org.elasticsearch.transport.TransportChannel; -import org.elasticsearch.transport.TransportException; -import org.elasticsearch.transport.TransportInterceptor; -import org.elasticsearch.transport.TransportRequest; -import org.elasticsearch.transport.TransportRequestOptions; -import org.elasticsearch.transport.TransportResponse; -import org.elasticsearch.transport.TransportService; import java.io.IOException; import java.util.ArrayList; @@ -48,14 +37,13 @@ public abstract class DisruptableMockTransport extends MockTransport { private final DiscoveryNode localNode; - private final Logger logger; + private final Logger logger = LogManager.getLogger(DisruptableMockTransport.class); private final DeterministicTaskQueue deterministicTaskQueue; private final List blackholedRequests = new ArrayList<>(); private final Set blockedActions = new HashSet<>(); - public DisruptableMockTransport(DiscoveryNode localNode, Logger logger, DeterministicTaskQueue deterministicTaskQueue) { + public DisruptableMockTransport(DiscoveryNode localNode, DeterministicTaskQueue deterministicTaskQueue) { this.localNode = localNode; - this.logger = logger; this.deterministicTaskQueue = deterministicTaskQueue; } @@ -111,7 +99,12 @@ public DiscoveryNode getNode() { public void sendRequest(long requestId, String action, TransportRequest request, TransportRequestOptions options) throws TransportException { if (blockedActions.contains(action)) { - execute(new Runnable() { + execute(new RebootSensitiveRunnable() { + @Override + public void ifRebooted() { + cleanupResponseHandler(requestId); + } + @Override public void run() { handleError( @@ -127,7 +120,7 @@ public void run() { @Override public String toString() { - return "error response delivery for action [" + action + "] on node [" + node + "]"; + return "error response delivery for blocked action [" + action + "] on node [" + node + "]"; } }); } else { @@ -172,28 +165,20 @@ public void run() { @Override public void ifRebooted() { request.decRef(); - deterministicTaskQueue.scheduleNow(new Runnable() { + execute(new RebootSensitiveRunnable() { @Override - public void run() { - execute(new Runnable() { - @Override - public void run() { - handleRemoteError( - requestId, - new NodeNotConnectedException(destinationTransport.getLocalNode(), "node rebooted") - ); - } + public void ifRebooted() { + cleanupResponseHandler(requestId); + } - @Override - public String toString() { - return "error response (reboot) to " + internalToString(); - } - }); + @Override + public void run() { + handleRemoteError(requestId, new NodeNotConnectedException(destinationTransport.getLocalNode(), "node rebooted")); } @Override public String toString() { - return "scheduling of error response (reboot) to " + internalToString(); + return "error response (reboot) to " + internalToString(); } }); } @@ -210,7 +195,12 @@ private String internalToString() { } protected Runnable getDisconnectException(long requestId, String action, DiscoveryNode destination) { - return new Runnable() { + return new RebootSensitiveRunnable() { + @Override + public void ifRebooted() { + cleanupResponseHandler(requestId); + } + @Override public void run() { handleError(requestId, new ConnectTransportException(destination, "disconnected")); @@ -272,13 +262,20 @@ public String getChannelType() { @Override public void sendResponse(final TransportResponse response) { - execute(new Runnable() { + execute(new RebootSensitiveRunnable() { + @Override + public void ifRebooted() { + response.decRef(); + cleanupResponseHandler(requestId); + } + @Override public void run() { final ConnectionStatus connectionStatus = destinationTransport.getConnectionStatus(getLocalNode()); switch (connectionStatus) { case CONNECTED, BLACK_HOLE_REQUESTS_ONLY -> handleResponse(requestId, response); case BLACK_HOLE, DISCONNECTED -> { + response.decRef(); logger.trace("delaying response to {}: channel is {}", requestDescription, connectionStatus); onBlackholedDuringSend(requestId, action, destinationTransport); } @@ -295,8 +292,12 @@ public String toString() { @Override public void sendResponse(Exception exception) { + execute(new RebootSensitiveRunnable() { + @Override + public void ifRebooted() { + cleanupResponseHandler(requestId); + } - execute(new Runnable() { @Override public void run() { final ConnectionStatus connectionStatus = destinationTransport.getConnectionStatus(getLocalNode()); @@ -333,6 +334,18 @@ public String toString() { } catch (Exception ee) { logger.warn("failed to send failure", e); } + } finally { + copiedRequest.decRef(); + } + } + + private void cleanupResponseHandler(long requestId) { + TransportResponseHandler handler = getTransportResponseHandler(requestId); + while (handler instanceof TransportService.ContextRestoreResponseHandler contextRestoreHandler) { + handler = contextRestoreHandler.unwrap(); + } + if (handler instanceof CleanableResponseHandler cleanableResponseHandler) { + cleanableResponseHandler.runCleanup(); } } diff --git a/test/framework/src/test/java/org/elasticsearch/test/disruption/DisruptableMockTransportTests.java b/test/framework/src/test/java/org/elasticsearch/transport/DisruptableMockTransportTests.java similarity index 64% rename from test/framework/src/test/java/org/elasticsearch/test/disruption/DisruptableMockTransportTests.java rename to test/framework/src/test/java/org/elasticsearch/transport/DisruptableMockTransportTests.java index b3fafa729856..ca2644c22d34 100644 --- a/test/framework/src/test/java/org/elasticsearch/test/disruption/DisruptableMockTransportTests.java +++ b/test/framework/src/test/java/org/elasticsearch/transport/DisruptableMockTransportTests.java @@ -6,29 +6,27 @@ * Side Public License, v 1. */ -package org.elasticsearch.test.disruption; +package org.elasticsearch.transport; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.cluster.coordination.CleanableResponseHandler; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.util.concurrent.DeterministicTaskQueue; +import org.elasticsearch.core.AbstractRefCounted; +import org.elasticsearch.core.RefCounted; import org.elasticsearch.core.Releasable; import org.elasticsearch.core.Tuple; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.test.disruption.DisruptableMockTransport.ConnectionStatus; import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.transport.AbstractSimpleTransportTestCase; -import org.elasticsearch.transport.ConnectTransportException; -import org.elasticsearch.transport.TransportChannel; -import org.elasticsearch.transport.TransportException; -import org.elasticsearch.transport.TransportRequest; -import org.elasticsearch.transport.TransportRequestHandler; -import org.elasticsearch.transport.TransportResponse; -import org.elasticsearch.transport.TransportResponseHandler; -import org.elasticsearch.transport.TransportService; +import org.elasticsearch.transport.DisruptableMockTransport.ConnectionStatus; +import org.junit.After; import org.junit.Before; import java.io.IOException; @@ -41,6 +39,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import java.util.stream.Stream; import static org.elasticsearch.transport.TransportService.NOOP_TRANSPORT_INTERCEPTOR; import static org.hamcrest.Matchers.containsString; @@ -49,6 +48,8 @@ public class DisruptableMockTransportTests extends ESTestCase { + private static final String TEST_ACTION = "internal:dummy"; + private DiscoveryNode node1; private DiscoveryNode node2; @@ -58,11 +59,15 @@ public class DisruptableMockTransportTests extends ESTestCase { private DeterministicTaskQueue deterministicTaskQueue; private Runnable deliverBlackholedRequests; + private Runnable blockTestAction; private Set> disconnectedLinks; private Set> blackholedLinks; private Set> blackholedRequestLinks; + private long activeRequestCount; + private final Set rebootedNodes = new HashSet<>(); + private ConnectionStatus getConnectionStatus(DiscoveryNode sender, DiscoveryNode destination) { Tuple link = Tuple.tuple(sender, destination); if (disconnectedLinks.contains(link)) { @@ -93,7 +98,7 @@ public void initTransports() { deterministicTaskQueue = new DeterministicTaskQueue(); - final DisruptableMockTransport transport1 = new DisruptableMockTransport(node1, logger, deterministicTaskQueue) { + final DisruptableMockTransport transport1 = new DisruptableMockTransport(node1, deterministicTaskQueue) { @Override protected ConnectionStatus getConnectionStatus(DiscoveryNode destination) { return DisruptableMockTransportTests.this.getConnectionStatus(getLocalNode(), destination); @@ -106,11 +111,11 @@ protected Optional getDisruptableMockTransport(Transpo @Override protected void execute(Runnable runnable) { - deterministicTaskQueue.scheduleNow(runnable); + deterministicTaskQueue.scheduleNow(unlessRebooted(node1, runnable)); } }; - final DisruptableMockTransport transport2 = new DisruptableMockTransport(node2, logger, deterministicTaskQueue) { + final DisruptableMockTransport transport2 = new DisruptableMockTransport(node2, deterministicTaskQueue) { @Override protected ConnectionStatus getConnectionStatus(DiscoveryNode destination) { return DisruptableMockTransportTests.this.getConnectionStatus(getLocalNode(), destination); @@ -123,7 +128,7 @@ protected Optional getDisruptableMockTransport(Transpo @Override protected void execute(Runnable runnable) { - deterministicTaskQueue.scheduleNow(runnable); + deterministicTaskQueue.scheduleNow(unlessRebooted(node2, runnable)); } }; @@ -159,42 +164,96 @@ protected void execute(Runnable runnable) { assertTrue(fut2.isDone()); deliverBlackholedRequests = () -> transports.forEach(DisruptableMockTransport::deliverBlackholedRequests); + + blockTestAction = new Runnable() { + @Override + public void run() { + transports.forEach(t -> t.addActionBlock(TEST_ACTION)); + } + + @Override + public String toString() { + return "add block for " + TEST_ACTION; + } + }; + + activeRequestCount = 0; + rebootedNodes.clear(); + } + + @After + public void assertAllRequestsReleased() { + assertEquals(0, activeRequestCount); + } + + private Runnable reboot(DiscoveryNode discoveryNode) { + return new Runnable() { + @Override + public void run() { + rebootedNodes.add(discoveryNode); + } + + @Override + public String toString() { + return "reboot " + discoveryNode; + } + }; + } + + private Runnable unlessRebooted(DiscoveryNode discoveryNode, Runnable runnable) { + return new Runnable() { + @Override + public void run() { + if (rebootedNodes.contains(discoveryNode)) { + if (runnable instanceof DisruptableMockTransport.RebootSensitiveRunnable rebootSensitiveRunnable) { + rebootSensitiveRunnable.ifRebooted(); + } + } else { + runnable.run(); + } + } + + @Override + public String toString() { + return "unlessRebooted[" + discoveryNode.getId() + "/" + runnable + "]"; + } + }; } - private TransportRequestHandler requestHandlerShouldNotBeCalled() { + private TransportRequestHandler requestHandlerShouldNotBeCalled() { return (request, channel, task) -> { throw new AssertionError("should not be called"); }; } - private TransportRequestHandler requestHandlerRepliesNormally() { + private TransportRequestHandler requestHandlerRepliesNormally() { return (request, channel, task) -> { logger.debug("got a dummy request, replying normally..."); - channel.sendResponse(TransportResponse.Empty.INSTANCE); + channel.sendResponse(new TestResponse()); }; } - private TransportRequestHandler requestHandlerRepliesExceptionally(Exception e) { + private TransportRequestHandler requestHandlerRepliesExceptionally(Exception e) { return (request, channel, task) -> { logger.debug("got a dummy request, replying exceptionally..."); channel.sendResponse(e); }; } - private TransportRequestHandler requestHandlerCaptures(Consumer channelConsumer) { + private TransportRequestHandler requestHandlerCaptures(Consumer channelConsumer) { return (request, channel, task) -> { logger.debug("got a dummy request..."); channelConsumer.accept(channel); }; } - private TransportResponseHandler responseHandlerShouldNotBeCalled() { + private TransportResponseHandler responseHandlerShouldNotBeCalled() { return new TransportResponseHandler<>() { @Override - public TransportResponse read(StreamInput in) { + public T read(StreamInput in) { throw new AssertionError("should not be called"); } @Override - public void handleResponse(TransportResponse response) { + public void handleResponse(T response) { throw new AssertionError("should not be called"); } @@ -205,10 +264,15 @@ public void handleException(TransportException exp) { }; } - private TransportResponseHandler responseHandlerShouldBeCalledNormally(Runnable onCalled) { - return new TransportResponseHandler.Empty() { + private TransportResponseHandler responseHandlerShouldBeCalledNormally(Runnable onCalled) { + return new TransportResponseHandler<>() { + @Override + public TestResponse read(StreamInput in) throws IOException { + return new TestResponse(in); + } + @Override - public void handleResponse(TransportResponse.Empty response) { + public void handleResponse(TestResponse response) { onCalled.run(); } @@ -219,15 +283,17 @@ public void handleException(TransportException exp) { }; } - private TransportResponseHandler responseHandlerShouldBeCalledExceptionally(Consumer onCalled) { + private TransportResponseHandler responseHandlerShouldBeCalledExceptionally( + Consumer onCalled + ) { return new TransportResponseHandler<>() { @Override - public TransportResponse read(StreamInput in) { + public T read(StreamInput in) { throw new AssertionError("should not be called"); } @Override - public void handleResponse(TransportResponse response) { + public void handleResponse(T response) { throw new AssertionError("should not be called"); } @@ -238,8 +304,8 @@ public void handleException(TransportException exp) { }; } - private void registerRequestHandler(TransportService transportService, TransportRequestHandler handler) { - transportService.registerRequestHandler("internal:dummy", ThreadPool.Names.GENERIC, TransportRequest.Empty::new, handler); + private void registerRequestHandler(TransportService transportService, TransportRequestHandler handler) { + transportService.registerRequestHandler(TEST_ACTION, ThreadPool.Names.GENERIC, TestRequest::new, handler); } private void send( @@ -247,7 +313,12 @@ private void send( DiscoveryNode destinationNode, TransportResponseHandler responseHandler ) { - transportService.sendRequest(destinationNode, "internal:dummy", TransportRequest.Empty.INSTANCE, responseHandler); + final var request = new TestRequest(); + try { + transportService.sendRequest(destinationNode, TEST_ACTION, request, responseHandler); + } finally { + request.decRef(); + } } public void testSuccessfulResponse() { @@ -259,6 +330,18 @@ public void testSuccessfulResponse() { assertTrue(responseHandlerCalled.get()); } + public void testBlockedAction() { + registerRequestHandler(service1, requestHandlerShouldNotBeCalled()); + registerRequestHandler(service2, requestHandlerRepliesNormally()); + blockTestAction.run(); + AtomicReference responseHandlerException = new AtomicReference<>(); + send(service1, node2, responseHandlerShouldBeCalledExceptionally(responseHandlerException::set)); + deterministicTaskQueue.runAllRunnableTasks(); + assertNotNull(responseHandlerException.get()); + assertNotNull(responseHandlerException.get().getCause()); + assertThat(responseHandlerException.get().getCause().getMessage(), containsString("action [" + TEST_ACTION + "] is blocked")); + } + public void testExceptionalResponse() { registerRequestHandler(service1, requestHandlerShouldNotBeCalled()); Exception e = new Exception("dummy exception"); @@ -310,7 +393,7 @@ public void testDisconnectedOnSuccessfulResponse() throws IOException { assertNull(responseHandlerException.get()); disconnectedLinks.add(Tuple.tuple(node2, node1)); - responseHandlerChannel.get().sendResponse(TransportResponse.Empty.INSTANCE); + responseHandlerChannel.get().sendResponse(new TestResponse()); deterministicTaskQueue.runAllTasks(); deliverBlackholedRequests.run(); deterministicTaskQueue.runAllTasks(); @@ -348,7 +431,7 @@ public void testUnavailableOnSuccessfulResponse() throws IOException { assertNotNull(responseHandlerChannel.get()); blackholedLinks.add(Tuple.tuple(node2, node1)); - responseHandlerChannel.get().sendResponse(TransportResponse.Empty.INSTANCE); + responseHandlerChannel.get().sendResponse(new TestResponse()); deterministicTaskQueue.runAllRunnableTasks(); } @@ -380,7 +463,7 @@ public void testUnavailableOnRequestOnlyReceivesSuccessfulResponse() throws IOEx blackholedRequestLinks.add(Tuple.tuple(node1, node2)); blackholedRequestLinks.add(Tuple.tuple(node2, node1)); - responseHandlerChannel.get().sendResponse(TransportResponse.Empty.INSTANCE); + responseHandlerChannel.get().sendResponse(new TestResponse()); deterministicTaskQueue.runAllRunnableTasks(); assertTrue(responseHandlerCalled.get()); @@ -406,6 +489,73 @@ public void testUnavailableOnRequestOnlyReceivesExceptionalResponse() throws IOE assertTrue(responseHandlerCalled.get()); } + public void testResponseWithReboots() { + registerRequestHandler(service1, requestHandlerShouldNotBeCalled()); + registerRequestHandler( + service2, + randomFrom(requestHandlerRepliesNormally(), requestHandlerRepliesExceptionally(new ElasticsearchException("simulated"))) + ); + + final var linkDisruptions = List.of( + Tuple.tuple("blackhole", blackholedLinks), + Tuple.tuple("blackhole-request", blackholedRequestLinks), + Tuple.tuple("disconnected", disconnectedLinks) + ); + + for (Runnable runnable : Stream.concat( + Stream.of(reboot(node1), reboot(node2), blockTestAction), + Stream.of(Tuple.tuple(node1, node2), Tuple.tuple(node2, node2)).map(link -> { + final var disruption = randomFrom(linkDisruptions); + return new Runnable() { + @Override + public void run() { + if (linkDisruptions.stream().noneMatch(otherDisruption -> otherDisruption.v2().contains(link))) { + disruption.v2().add(link); + } + } + + @Override + public String toString() { + return disruption.v1() + ": " + link.v1().getId() + " to " + link.v2().getId(); + } + }; + }) + ).toList()) { + if (randomBoolean()) { + deterministicTaskQueue.scheduleNow(runnable); + } + } + + AtomicBoolean responseHandlerCalled = new AtomicBoolean(); + AtomicBoolean responseHandlerReleased = new AtomicBoolean(); + deterministicTaskQueue.scheduleNow(new Runnable() { + @Override + public void run() { + DisruptableMockTransportTests.this.send( + service1, + node2, + new CleanableResponseHandler<>( + ActionListener.wrap(() -> assertFalse(responseHandlerCalled.getAndSet(true))), + TestResponse::new, + ThreadPool.Names.SAME, + () -> assertFalse(responseHandlerReleased.getAndSet(true)) + ) + ); + } + + @Override + public String toString() { + return "send test message"; + } + }); + + deterministicTaskQueue.runAllRunnableTasks(); + deliverBlackholedRequests.run(); + deterministicTaskQueue.runAllRunnableTasks(); + + assertTrue(responseHandlerReleased.get()); + } + public void testBrokenLinkFailsToConnect() { service1.disconnectFromNode(node2); @@ -440,4 +590,77 @@ public void testBrokenLinkFailsToConnect() { endsWith("does not exist") ); } + + private class TestRequest extends TransportRequest { + private final RefCounted refCounted; + + TestRequest() { + activeRequestCount++; + refCounted = AbstractRefCounted.of(() -> activeRequestCount--); + } + + TestRequest(StreamInput in) throws IOException { + super(in); + activeRequestCount++; + refCounted = AbstractRefCounted.of(() -> activeRequestCount--); + } + + @Override + public void incRef() { + refCounted.incRef(); + } + + @Override + public boolean tryIncRef() { + return refCounted.tryIncRef(); + } + + @Override + public boolean decRef() { + return refCounted.decRef(); + } + + @Override + public boolean hasReferences() { + return refCounted.hasReferences(); + } + } + + private class TestResponse extends TransportResponse { + private final RefCounted refCounted; + + TestResponse() { + activeRequestCount++; + refCounted = AbstractRefCounted.of(() -> activeRequestCount--); + } + + TestResponse(StreamInput in) throws IOException { + super(in); + activeRequestCount++; + refCounted = AbstractRefCounted.of(() -> activeRequestCount--); + } + + @Override + public void writeTo(StreamOutput out) {} + + @Override + public void incRef() { + refCounted.incRef(); + } + + @Override + public boolean tryIncRef() { + return refCounted.tryIncRef(); + } + + @Override + public boolean decRef() { + return refCounted.decRef(); + } + + @Override + public boolean hasReferences() { + return refCounted.hasReferences(); + } + } } From 6fa3d73fd516d77be2099a254b870dd92bb5b15b Mon Sep 17 00:00:00 2001 From: David Roberts Date: Mon, 12 Dec 2022 15:43:30 +0000 Subject: [PATCH 239/919] [ML] Make native inference generally available (#92213) Previously this functionality was beta. This PR changes it to GA. --- docs/changelog/92213.yaml | 5 +++++ .../apis/clear-trained-model-deployment-cache.asciidoc | 2 -- .../ml/trained-models/apis/infer-trained-model.asciidoc | 2 -- .../apis/put-trained-model-definition-part.asciidoc | 2 -- .../apis/put-trained-model-vocabulary.asciidoc | 2 -- .../apis/start-trained-model-deployment.asciidoc | 2 -- .../apis/stop-trained-model-deployment.asciidoc | 2 -- .../api/ml.clear_trained_model_deployment_cache.json | 2 +- .../resources/rest-api-spec/api/ml.infer_trained_model.json | 2 +- .../api/ml.put_trained_model_definition_part.json | 2 +- .../rest-api-spec/api/ml.put_trained_model_vocabulary.json | 2 +- .../rest-api-spec/api/ml.start_trained_model_deployment.json | 2 +- .../rest-api-spec/api/ml.stop_trained_model_deployment.json | 2 +- 13 files changed, 11 insertions(+), 18 deletions(-) create mode 100644 docs/changelog/92213.yaml diff --git a/docs/changelog/92213.yaml b/docs/changelog/92213.yaml new file mode 100644 index 000000000000..6cbde4e15bf0 --- /dev/null +++ b/docs/changelog/92213.yaml @@ -0,0 +1,5 @@ +pr: 92213 +summary: Make native inference generally available +area: Machine Learning +type: enhancement +issues: [] diff --git a/docs/reference/ml/trained-models/apis/clear-trained-model-deployment-cache.asciidoc b/docs/reference/ml/trained-models/apis/clear-trained-model-deployment-cache.asciidoc index 79d63848e66a..f0977430c9b0 100644 --- a/docs/reference/ml/trained-models/apis/clear-trained-model-deployment-cache.asciidoc +++ b/docs/reference/ml/trained-models/apis/clear-trained-model-deployment-cache.asciidoc @@ -8,8 +8,6 @@ Clears a trained model deployment cache on all nodes where the trained model is assigned. -beta::[] - [[clear-trained-model-deployment-cache-request]] == {api-request-title} diff --git a/docs/reference/ml/trained-models/apis/infer-trained-model.asciidoc b/docs/reference/ml/trained-models/apis/infer-trained-model.asciidoc index 9f64a4a0e10d..9846024b60b8 100644 --- a/docs/reference/ml/trained-models/apis/infer-trained-model.asciidoc +++ b/docs/reference/ml/trained-models/apis/infer-trained-model.asciidoc @@ -12,8 +12,6 @@ by {dfanalytics} or imported. NOTE: For model deployments with caching enabled, results may be returned directly from the {infer} cache. -beta::[] - [[infer-trained-model-request]] == {api-request-title} diff --git a/docs/reference/ml/trained-models/apis/put-trained-model-definition-part.asciidoc b/docs/reference/ml/trained-models/apis/put-trained-model-definition-part.asciidoc index 323f6953061a..53cad5cffa37 100644 --- a/docs/reference/ml/trained-models/apis/put-trained-model-definition-part.asciidoc +++ b/docs/reference/ml/trained-models/apis/put-trained-model-definition-part.asciidoc @@ -8,8 +8,6 @@ Creates part of a trained model definition. -beta::[] - [[ml-put-trained-model-definition-part-request]] == {api-request-title} diff --git a/docs/reference/ml/trained-models/apis/put-trained-model-vocabulary.asciidoc b/docs/reference/ml/trained-models/apis/put-trained-model-vocabulary.asciidoc index 31a49bae8aad..154aad8350e0 100644 --- a/docs/reference/ml/trained-models/apis/put-trained-model-vocabulary.asciidoc +++ b/docs/reference/ml/trained-models/apis/put-trained-model-vocabulary.asciidoc @@ -9,8 +9,6 @@ Creates a trained model vocabulary. This is supported only for natural language processing (NLP) models. -beta::[] - [[ml-put-trained-model-vocabulary-request]] == {api-request-title} diff --git a/docs/reference/ml/trained-models/apis/start-trained-model-deployment.asciidoc b/docs/reference/ml/trained-models/apis/start-trained-model-deployment.asciidoc index 104d6f3f9e76..beef62b88597 100644 --- a/docs/reference/ml/trained-models/apis/start-trained-model-deployment.asciidoc +++ b/docs/reference/ml/trained-models/apis/start-trained-model-deployment.asciidoc @@ -8,8 +8,6 @@ Starts a new trained model deployment. -beta::[] - [[start-trained-model-deployment-request]] == {api-request-title} diff --git a/docs/reference/ml/trained-models/apis/stop-trained-model-deployment.asciidoc b/docs/reference/ml/trained-models/apis/stop-trained-model-deployment.asciidoc index 6fb2b5bfa3c2..d5de2b61cfe2 100644 --- a/docs/reference/ml/trained-models/apis/stop-trained-model-deployment.asciidoc +++ b/docs/reference/ml/trained-models/apis/stop-trained-model-deployment.asciidoc @@ -8,8 +8,6 @@ Stops a trained model deployment. -beta::[] - [[stop-trained-model-deployment-request]] == {api-request-title} diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/ml.clear_trained_model_deployment_cache.json b/rest-api-spec/src/main/resources/rest-api-spec/api/ml.clear_trained_model_deployment_cache.json index ee92ab44ab8f..81f396a30b36 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/ml.clear_trained_model_deployment_cache.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/ml.clear_trained_model_deployment_cache.json @@ -4,7 +4,7 @@ "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/clear-trained-model-deployment-cache.html", "description":"Clear the cached results from a trained model deployment" }, - "stability":"beta", + "stability":"stable", "visibility":"public", "headers":{ "accept": [ "application/json"], diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/ml.infer_trained_model.json b/rest-api-spec/src/main/resources/rest-api-spec/api/ml.infer_trained_model.json index 539d46eb88dd..6041155b1ea6 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/ml.infer_trained_model.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/ml.infer_trained_model.json @@ -4,7 +4,7 @@ "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/infer-trained-model.html", "description":"Evaluate a trained model." }, - "stability":"beta", + "stability":"stable", "visibility":"public", "headers":{ "accept": [ "application/json"], diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/ml.put_trained_model_definition_part.json b/rest-api-spec/src/main/resources/rest-api-spec/api/ml.put_trained_model_definition_part.json index 09e34a5c9eea..bf826f36e93c 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/ml.put_trained_model_definition_part.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/ml.put_trained_model_definition_part.json @@ -4,7 +4,7 @@ "url":"https://www.elastic.co/guide/en/elasticsearch/reference/current/put-trained-model-definition-part.html", "description":"Creates part of a trained model definition" }, - "stability":"beta", + "stability":"stable", "visibility":"public", "headers":{ "accept": [ "application/json"], diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/ml.put_trained_model_vocabulary.json b/rest-api-spec/src/main/resources/rest-api-spec/api/ml.put_trained_model_vocabulary.json index 1d1c6005b369..f6fd70a75c00 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/ml.put_trained_model_vocabulary.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/ml.put_trained_model_vocabulary.json @@ -4,7 +4,7 @@ "url":"https://www.elastic.co/guide/en/elasticsearch/reference/current/put-trained-model-vocabulary.html", "description":"Creates a trained model vocabulary" }, - "stability":"beta", + "stability":"stable", "visibility":"public", "headers":{ "accept": [ "application/json"], diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/ml.start_trained_model_deployment.json b/rest-api-spec/src/main/resources/rest-api-spec/api/ml.start_trained_model_deployment.json index db3418759898..8f98bc03670a 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/ml.start_trained_model_deployment.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/ml.start_trained_model_deployment.json @@ -4,7 +4,7 @@ "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/start-trained-model-deployment.html", "description":"Start a trained model deployment." }, - "stability":"beta", + "stability":"stable", "visibility":"public", "headers":{ "accept": [ "application/json"], diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/ml.stop_trained_model_deployment.json b/rest-api-spec/src/main/resources/rest-api-spec/api/ml.stop_trained_model_deployment.json index 5cb9d5764067..016d88c684e4 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/ml.stop_trained_model_deployment.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/ml.stop_trained_model_deployment.json @@ -4,7 +4,7 @@ "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/stop-trained-model-deployment.html", "description":"Stop a trained model deployment." }, - "stability":"beta", + "stability":"stable", "visibility":"public", "headers":{ "accept": [ "application/json"], From 3746d8aa345fc58ad793003ba4fb87f9902e3c44 Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Mon, 12 Dec 2022 16:52:08 +0100 Subject: [PATCH 240/919] limit the H3 resolution we test until lucene bug is fixed (#92110) --- .../h3/ParentChildNavigationTests.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/libs/h3/src/test/java/org/elasticsearch/h3/ParentChildNavigationTests.java b/libs/h3/src/test/java/org/elasticsearch/h3/ParentChildNavigationTests.java index 06190cbef558..2ceae7c794a7 100644 --- a/libs/h3/src/test/java/org/elasticsearch/h3/ParentChildNavigationTests.java +++ b/libs/h3/src/test/java/org/elasticsearch/h3/ParentChildNavigationTests.java @@ -21,6 +21,7 @@ import com.carrotsearch.randomizedtesting.generators.RandomPicks; import org.apache.lucene.geo.Point; +import org.apache.lucene.spatial3d.geom.GeoArea; import org.apache.lucene.spatial3d.geom.GeoPoint; import org.apache.lucene.spatial3d.geom.GeoPolygon; import org.apache.lucene.spatial3d.geom.GeoPolygonFactory; @@ -132,13 +133,23 @@ private void assertHexRing(int res, String h3Address, String[] children) { public void testNoChildrenIntersecting() { String[] h3Addresses = H3.getStringRes0Cells(); String h3Address = RandomPicks.randomFrom(random(), h3Addresses); - for (int i = 1; i <= H3.MAX_H3_RES; i++) { + // Once testIssue91915 is fixed, put upper limit of the loop to H3.MAX_H3_RES + for (int i = 1; i <= 10; i++) { h3Addresses = H3.h3ToChildren(h3Address); assertIntersectingChildren(h3Address, h3Addresses); h3Address = RandomPicks.randomFrom(random(), h3Addresses); } } + public void testIssue91915() { + GeoPolygon polygon1 = getGeoPolygon("8cc373cb54069ff"); + GeoPolygon polygon2 = getGeoPolygon("8cc373cb54065ff"); + // these polygons are disjoint but due to https://github.com/apache/lucene/issues/11883 + // they are reported as intersects. Once this is fixed this test will fail, we should adjust + // testNoChildrenIntersecting + assertEquals("see https://github.com/elastic/elasticsearch/issues/91915", GeoArea.OVERLAPS, polygon1.getRelationship(polygon2)); + } + private void assertIntersectingChildren(String h3Address, String[] children) { int size = H3.h3ToNotIntersectingChildrenSize(h3Address); for (int i = 0; i < size; i++) { From d4a778e0e36ab4532676a96bafe9786dd7bdf277 Mon Sep 17 00:00:00 2001 From: Abdon Pijpelink Date: Mon, 12 Dec 2022 17:35:51 +0100 Subject: [PATCH 241/919] [DOCS] Move known issues section to top of 8.5.0 release notes (#92304) --- docs/reference/release-notes/8.5.0.asciidoc | 23 ++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/docs/reference/release-notes/8.5.0.asciidoc b/docs/reference/release-notes/8.5.0.asciidoc index e5e3bfc8655f..277ab3fbea16 100644 --- a/docs/reference/release-notes/8.5.0.asciidoc +++ b/docs/reference/release-notes/8.5.0.asciidoc @@ -3,6 +3,17 @@ Also see <>. +[[known-issues-8.5.0]] +[float] +=== Known issues + +* It is possible to inadvertently create an alias with the same name as an +index in version 8.5.0. This action leaves the cluster in an invalid state in +which several features will not work correctly, and it may not even be possible +to restart nodes while in this state. Upgrade to 8.5.1 as soon as possible to +avoid the risk of this occurring ({es-pull}91456[#91456]). If your cluster is +affected by this issue, upgrade to 8.5.3 to repair it ({es-pull}91887[#91887]). + [[breaking-8.5.0]] [float] === Breaking changes @@ -319,15 +330,3 @@ Client:: Packaging:: * Upgrade bundled JDK to Java 19 {es-pull}90571[#90571] - - -[[known-issues-8.5.0]] -[float] -=== Known issues - -* It is possible to inadvertently create an alias with the same name as an -index in version 8.5.0. This action leaves the cluster in an invalid state in -which several features will not work correctly, and it may not even be possible -to restart nodes while in this state. Upgrade to 8.5.1 as soon as possible to -avoid the risk of this occurring ({es-pull}91456[#91456]). If your cluster is -affected by this issue, upgrade to 8.5.3 to repair it ({es-pull}91887[#91887]). From a08426c349d6181aeb8b8c5d04ac70740217f9df Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 12 Dec 2022 16:50:02 +0000 Subject: [PATCH 242/919] AwaitsFixes for #92307 --- .../coordination/CoordinationDiagnosticsServiceTests.java | 2 ++ .../elasticsearch/cluster/coordination/CoordinatorTests.java | 2 ++ .../coordination/StableMasterHealthIndicatorServiceTests.java | 2 ++ .../coordination/votingonly/VotingOnlyNodeCoordinatorTests.java | 2 ++ 4 files changed, 8 insertions(+) diff --git a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinationDiagnosticsServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinationDiagnosticsServiceTests.java index 452f285ec684..507865d26b3b 100644 --- a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinationDiagnosticsServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinationDiagnosticsServiceTests.java @@ -8,6 +8,7 @@ package org.elasticsearch.cluster.coordination; +import org.apache.lucene.tests.util.LuceneTestCase; import org.elasticsearch.Version; import org.elasticsearch.cluster.ClusterChangedEvent; import org.elasticsearch.cluster.ClusterName; @@ -62,6 +63,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/92307") public class CoordinationDiagnosticsServiceTests extends AbstractCoordinatorTestCase { DiscoveryNode node1; DiscoveryNode node2; diff --git a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java index eb275cdc487e..df30724a83e8 100644 --- a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java @@ -11,6 +11,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.LogEvent; +import org.apache.lucene.tests.util.LuceneTestCase; import org.apache.lucene.util.Constants; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.Version; @@ -97,6 +98,7 @@ import static org.hamcrest.Matchers.startsWith; @TestLogging(reason = "these tests do a lot of log-worthy things but we usually don't care", value = "org.elasticsearch:FATAL") +@LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/92307") public class CoordinatorTests extends AbstractCoordinatorTestCase { public void testCanUpdateClusterStateAfterStabilisation() { diff --git a/server/src/test/java/org/elasticsearch/cluster/coordination/StableMasterHealthIndicatorServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/coordination/StableMasterHealthIndicatorServiceTests.java index 83ba8fb64459..4d4e2cd4f455 100644 --- a/server/src/test/java/org/elasticsearch/cluster/coordination/StableMasterHealthIndicatorServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/coordination/StableMasterHealthIndicatorServiceTests.java @@ -8,6 +8,7 @@ package org.elasticsearch.cluster.coordination; +import org.apache.lucene.tests.util.LuceneTestCase; import org.elasticsearch.Version; import org.elasticsearch.cluster.ClusterChangedEvent; import org.elasticsearch.cluster.ClusterName; @@ -51,6 +52,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/92307") public class StableMasterHealthIndicatorServiceTests extends AbstractCoordinatorTestCase { DiscoveryNode node1; DiscoveryNode node2; diff --git a/x-pack/plugin/voting-only-node/src/test/java/org/elasticsearch/cluster/coordination/votingonly/VotingOnlyNodeCoordinatorTests.java b/x-pack/plugin/voting-only-node/src/test/java/org/elasticsearch/cluster/coordination/votingonly/VotingOnlyNodeCoordinatorTests.java index 915f913cc144..bc00accb3812 100644 --- a/x-pack/plugin/voting-only-node/src/test/java/org/elasticsearch/cluster/coordination/votingonly/VotingOnlyNodeCoordinatorTests.java +++ b/x-pack/plugin/voting-only-node/src/test/java/org/elasticsearch/cluster/coordination/votingonly/VotingOnlyNodeCoordinatorTests.java @@ -6,6 +6,7 @@ */ package org.elasticsearch.cluster.coordination.votingonly; +import org.apache.lucene.tests.util.LuceneTestCase; import org.elasticsearch.Version; import org.elasticsearch.cluster.coordination.AbstractCoordinatorTestCase; import org.elasticsearch.cluster.coordination.ElectionStrategy; @@ -22,6 +23,7 @@ import static java.util.Collections.emptySet; +@LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/92307") public class VotingOnlyNodeCoordinatorTests extends AbstractCoordinatorTestCase { @Override From 8ccb38bcfa1704960f047bb73ff489f25362a84f Mon Sep 17 00:00:00 2001 From: Joe Gallo Date: Mon, 12 Dec 2022 12:14:41 -0500 Subject: [PATCH 243/919] Ingest cleanup collections code (#92249) --- ...bstractStringProcessorFactoryTestCase.java | 2 +- .../common/AppendProcessorFactoryTests.java | 13 +- .../ingest/common/AppendProcessorTests.java | 8 +- .../ingest/common/BytesProcessorTests.java | 16 +- .../CommunityIdProcessorFactoryTests.java | 4 +- .../common/ConvertProcessorFactoryTests.java | 9 +- .../ingest/common/ConvertProcessorTests.java | 17 +- .../common/CsvProcessorFactoryTests.java | 8 +- .../ingest/common/DateFormatTests.java | 2 +- .../common/DateIndexNameFactoryTests.java | 32 +- .../common/DateIndexNameProcessorTests.java | 61 +--- .../common/DateProcessorFactoryTests.java | 29 +- .../ingest/common/DateProcessorTests.java | 30 +- .../common/DissectProcessorFactoryTests.java | 9 +- .../ingest/common/DissectProcessorTests.java | 25 +- .../common/DotExpanderProcessorTests.java | 13 +- .../common/FailProcessorFactoryTests.java | 5 +- .../FingerprintProcessorFactoryTests.java | 3 +- .../common/ForEachProcessorFactoryTests.java | 28 +- .../ingest/common/ForEachProcessorTests.java | 34 +- .../common/GrokProcessorFactoryTests.java | 22 +- .../common/GrokProcessorGetActionTests.java | 6 +- .../ingest/common/GrokProcessorTests.java | 71 ++-- .../common/GsubProcessorFactoryTests.java | 2 +- .../common/JoinProcessorFactoryTests.java | 2 +- .../common/JsonProcessorFactoryTests.java | 2 +- .../ingest/common/JsonProcessorTests.java | 3 +- .../common/KeyValueProcessorFactoryTests.java | 18 +- .../ingest/common/KeyValueProcessorTests.java | 23 +- ...NetworkDirectionProcessorFactoryTests.java | 5 +- .../NetworkDirectionProcessorTests.java | 3 +- ...RegisteredDomainProcessorFactoryTests.java | 2 +- .../common/RemoveProcessorFactoryTests.java | 23 +- .../ingest/common/RemoveProcessorTests.java | 45 +-- .../common/RenameProcessorFactoryTests.java | 11 +- .../ingest/common/RenameProcessorTests.java | 18 +- .../common/ScriptProcessorFactoryTests.java | 25 +- .../ingest/common/ScriptProcessorTests.java | 22 +- .../common/SetProcessorFactoryTests.java | 17 +- .../ingest/common/SetProcessorTests.java | 11 +- .../common/SortProcessorFactoryTests.java | 2 +- .../ingest/common/SortProcessorTests.java | 12 +- .../common/SplitProcessorFactoryTests.java | 2 +- .../ingest/common/SplitProcessorTests.java | 15 +- .../common/UriPartsProcessorFactoryTests.java | 2 +- .../ingest/CompoundProcessorTests.java | 2 +- .../ingest/ConditionalProcessorTests.java | 47 ++- .../ingest/ConfigurationUtilsTests.java | 4 +- .../ingest/IngestServiceTests.java | 306 +++++------------- .../ingest/PipelineFactoryTests.java | 53 ++- .../ingest/PipelineProcessorTests.java | 33 +- .../ingest/TrackingResultProcessorTests.java | 76 ++--- .../ingest/ValueSourceTests.java | 9 +- 53 files changed, 472 insertions(+), 770 deletions(-) diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/AbstractStringProcessorFactoryTestCase.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/AbstractStringProcessorFactoryTestCase.java index b0e74000da57..209375ac12ed 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/AbstractStringProcessorFactoryTestCase.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/AbstractStringProcessorFactoryTestCase.java @@ -15,7 +15,7 @@ import java.util.HashMap; import java.util.Map; -import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; public abstract class AbstractStringProcessorFactoryTestCase extends ESTestCase { diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/AppendProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/AppendProcessorFactoryTests.java index 54f11932bfe5..daccbc9559d4 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/AppendProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/AppendProcessorFactoryTests.java @@ -15,13 +15,12 @@ import org.elasticsearch.test.ESTestCase; import org.junit.Before; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; -import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; public class AppendProcessorFactoryTests extends ESTestCase { @@ -39,14 +38,14 @@ public void testCreate() throws Exception { if (randomBoolean()) { value = "value1"; } else { - value = Arrays.asList("value1", "value2", "value3"); + value = List.of("value1", "value2", "value3"); } config.put("value", value); String processorTag = randomAlphaOfLength(10); AppendProcessor appendProcessor = factory.create(null, processorTag, null, config); assertThat(appendProcessor.getTag(), equalTo(processorTag)); - assertThat(appendProcessor.getField().newInstance(Collections.emptyMap()).execute(), equalTo("field1")); - assertThat(appendProcessor.getValue().copyAndResolve(Collections.emptyMap()), equalTo(value)); + assertThat(appendProcessor.getField().newInstance(Map.of()).execute(), equalTo("field1")); + assertThat(appendProcessor.getValue().copyAndResolve(Map.of()), equalTo(value)); } public void testCreateNoFieldPresent() throws Exception { @@ -110,7 +109,7 @@ public void testMediaType() throws Exception { // invalid media type expectedMediaType = randomValueOtherThanMany( - m -> Arrays.asList(ConfigurationUtils.VALID_MEDIA_TYPES).contains(m), + m -> List.of(ConfigurationUtils.VALID_MEDIA_TYPES).contains(m), () -> randomAlphaOfLengthBetween(5, 9) ); final Map config2 = new HashMap<>(); diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/AppendProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/AppendProcessorTests.java index d789f1bd79c4..3b5c2d39a3ca 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/AppendProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/AppendProcessorTests.java @@ -27,11 +27,11 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.sameInstance; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.sameInstance; public class AppendProcessorTests extends ESTestCase { diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/BytesProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/BytesProcessorTests.java index 1b35ea427c59..8ca6e71b4a51 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/BytesProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/BytesProcessorTests.java @@ -14,8 +14,8 @@ import org.elasticsearch.ingest.IngestDocument; import org.elasticsearch.ingest.Processor; import org.elasticsearch.ingest.RandomDocumentPicks; -import org.hamcrest.CoreMatchers; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; public class BytesProcessorTests extends AbstractStringProcessorTestCase { @@ -51,14 +51,8 @@ public void testTooLarge() { String fieldName = RandomDocumentPicks.addRandomField(random(), ingestDocument, "8912pb"); Processor processor = newProcessor(fieldName, randomBoolean(), fieldName); ElasticsearchException exception = expectThrows(ElasticsearchException.class, () -> processor.execute(ingestDocument)); - assertThat( - exception.getMessage(), - CoreMatchers.equalTo("failed to parse setting [Ingest Field] with value [8912pb] as a size in bytes") - ); - assertThat( - exception.getCause().getMessage(), - CoreMatchers.containsString("Values greater than 9223372036854775807 bytes are not supported") - ); + assertThat(exception.getMessage(), equalTo("failed to parse setting [Ingest Field] with value [8912pb] as a size in bytes")); + assertThat(exception.getCause().getMessage(), containsString("Values greater than 9223372036854775807 bytes are not supported")); } public void testNotBytes() { @@ -66,7 +60,7 @@ public void testNotBytes() { String fieldName = RandomDocumentPicks.addRandomField(random(), ingestDocument, "junk"); Processor processor = newProcessor(fieldName, randomBoolean(), fieldName); ElasticsearchException exception = expectThrows(ElasticsearchException.class, () -> processor.execute(ingestDocument)); - assertThat(exception.getMessage(), CoreMatchers.equalTo("failed to parse setting [Ingest Field] with value [junk]")); + assertThat(exception.getMessage(), equalTo("failed to parse setting [Ingest Field] with value [junk]")); } public void testMissingUnits() { @@ -74,7 +68,7 @@ public void testMissingUnits() { String fieldName = RandomDocumentPicks.addRandomField(random(), ingestDocument, "1"); Processor processor = newProcessor(fieldName, randomBoolean(), fieldName); ElasticsearchException exception = expectThrows(ElasticsearchException.class, () -> processor.execute(ingestDocument)); - assertThat(exception.getMessage(), CoreMatchers.containsString("unit is missing or unrecognized")); + assertThat(exception.getMessage(), containsString("unit is missing or unrecognized")); } public void testFractional() throws Exception { diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/CommunityIdProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/CommunityIdProcessorFactoryTests.java index 3ded531131f3..8d08040a4058 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/CommunityIdProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/CommunityIdProcessorFactoryTests.java @@ -25,8 +25,8 @@ import static org.elasticsearch.ingest.common.CommunityIdProcessor.Factory.DEFAULT_TARGET; import static org.elasticsearch.ingest.common.CommunityIdProcessor.Factory.DEFAULT_TRANSPORT; import static org.elasticsearch.ingest.common.CommunityIdProcessor.toUint16; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; public class CommunityIdProcessorFactoryTests extends ESTestCase { diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ConvertProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ConvertProcessorFactoryTests.java index 60ae12dd1aa0..df31f0ece6f6 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ConvertProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ConvertProcessorFactoryTests.java @@ -10,12 +10,11 @@ import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.test.ESTestCase; -import org.hamcrest.Matchers; import java.util.HashMap; import java.util.Map; -import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; @@ -46,7 +45,7 @@ public void testCreateUnsupportedType() throws Exception { factory.create(null, null, null, config); fail("factory create should have failed"); } catch (ElasticsearchParseException e) { - assertThat(e.getMessage(), Matchers.equalTo("[type] type [" + type + "] not supported, cannot convert field.")); + assertThat(e.getMessage(), equalTo("[type] type [" + type + "] not supported, cannot convert field.")); assertThat(e.getMetadata("es.processor_type").get(0), equalTo(ConvertProcessor.TYPE)); assertThat(e.getMetadata("es.property_name").get(0), equalTo("type")); assertThat(e.getMetadata("es.processor_tag"), nullValue()); @@ -62,7 +61,7 @@ public void testCreateNoFieldPresent() throws Exception { factory.create(null, null, null, config); fail("factory create should have failed"); } catch (ElasticsearchParseException e) { - assertThat(e.getMessage(), Matchers.equalTo("[field] required property is missing")); + assertThat(e.getMessage(), equalTo("[field] required property is missing")); } } @@ -74,7 +73,7 @@ public void testCreateNoTypePresent() throws Exception { factory.create(null, null, null, config); fail("factory create should have failed"); } catch (ElasticsearchParseException e) { - assertThat(e.getMessage(), Matchers.equalTo("[type] required property is missing")); + assertThat(e.getMessage(), equalTo("[type] required property is missing")); } } diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ConvertProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ConvertProcessorTests.java index 0590934ccd36..f1b7433e10a0 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ConvertProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ConvertProcessorTests.java @@ -514,7 +514,7 @@ public void testAutoConvertNotString() throws Exception { } default -> throw new UnsupportedOperationException(); } - IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Collections.singletonMap("field", randomValue)); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Map.of("field", randomValue)); Processor processor = new ConvertProcessor(randomAlphaOfLength(10), null, "field", "field", Type.AUTO, false); processor.execute(ingestDocument); Object convertedValue = ingestDocument.getFieldValue("field", Object.class); @@ -523,7 +523,7 @@ public void testAutoConvertNotString() throws Exception { public void testAutoConvertStringNotMatched() throws Exception { String value = "notAnIntFloatOrBool"; - IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Collections.singletonMap("field", value)); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Map.of("field", value)); Processor processor = new ConvertProcessor(randomAlphaOfLength(10), null, "field", "field", Type.AUTO, false); processor.execute(ingestDocument); Object convertedValue = ingestDocument.getFieldValue("field", Object.class); @@ -533,10 +533,7 @@ public void testAutoConvertStringNotMatched() throws Exception { public void testAutoConvertMatchBoolean() throws Exception { boolean randomBoolean = randomBoolean(); String booleanString = Boolean.toString(randomBoolean); - IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument( - random(), - Collections.singletonMap("field", booleanString) - ); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Map.of("field", booleanString)); Processor processor = new ConvertProcessor(randomAlphaOfLength(10), null, "field", "field", Type.AUTO, false); processor.execute(ingestDocument); Object convertedValue = ingestDocument.getFieldValue("field", Object.class); @@ -546,7 +543,7 @@ public void testAutoConvertMatchBoolean() throws Exception { public void testAutoConvertMatchInteger() throws Exception { int randomInt = randomInt(); String randomString = Integer.toString(randomInt); - IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Collections.singletonMap("field", randomString)); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Map.of("field", randomString)); Processor processor = new ConvertProcessor(randomAlphaOfLength(10), null, "field", "field", Type.AUTO, false); processor.execute(ingestDocument); Object convertedValue = ingestDocument.getFieldValue("field", Object.class); @@ -556,7 +553,7 @@ public void testAutoConvertMatchInteger() throws Exception { public void testAutoConvertMatchLong() throws Exception { long randomLong = randomLong(); String randomString = Long.toString(randomLong); - IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Collections.singletonMap("field", randomString)); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Map.of("field", randomString)); Processor processor = new ConvertProcessor(randomAlphaOfLength(10), null, "field", "field", Type.AUTO, false); processor.execute(ingestDocument); Object convertedValue = ingestDocument.getFieldValue("field", Object.class); @@ -567,7 +564,7 @@ public void testAutoConvertDoubleNotMatched() throws Exception { double randomDouble = randomDouble(); String randomString = Double.toString(randomDouble); float randomFloat = Float.parseFloat(randomString); - IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Collections.singletonMap("field", randomString)); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Map.of("field", randomString)); Processor processor = new ConvertProcessor(randomAlphaOfLength(10), null, "field", "field", Type.AUTO, false); processor.execute(ingestDocument); Object convertedValue = ingestDocument.getFieldValue("field", Object.class); @@ -578,7 +575,7 @@ public void testAutoConvertDoubleNotMatched() throws Exception { public void testAutoConvertMatchFloat() throws Exception { float randomFloat = randomFloat(); String randomString = Float.toString(randomFloat); - IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Collections.singletonMap("field", randomString)); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Map.of("field", randomString)); Processor processor = new ConvertProcessor(randomAlphaOfLength(10), null, "field", "field", Type.AUTO, false); processor.execute(ingestDocument); Object convertedValue = ingestDocument.getFieldValue("field", Object.class); diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/CsvProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/CsvProcessorFactoryTests.java index c4a631d05c35..7053e079a87d 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/CsvProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/CsvProcessorFactoryTests.java @@ -10,10 +10,10 @@ import org.elasticsearch.test.ESTestCase; -import java.util.Collections; import java.util.HashMap; +import java.util.List; +import java.util.Map; -import static java.util.Collections.emptyMap; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; @@ -24,7 +24,7 @@ public void testProcessorIsCreated() { CsvProcessor.Factory factory = new CsvProcessor.Factory(); HashMap properties = new HashMap<>(); properties.put("field", "field"); - properties.put("target_fields", Collections.singletonList("target")); + properties.put("target_fields", List.of("target")); properties.put("quote", "|"); properties.put("separator", "/"); properties.put("empty_value", "empty"); @@ -39,6 +39,6 @@ public void testProcessorIsCreated() { assertThat(csv.emptyValue, equalTo("empty")); assertThat(csv.trim, equalTo(true)); assertThat(csv.ignoreMissing, equalTo(true)); - assertThat(properties, is(emptyMap())); + assertThat(properties, is(Map.of())); } } diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DateFormatTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DateFormatTests.java index 2d3b3b9a4a9d..f7c94e24881d 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DateFormatTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DateFormatTests.java @@ -19,8 +19,8 @@ import java.util.Locale; import java.util.function.Function; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; -import static org.hamcrest.core.IsEqual.equalTo; public class DateFormatTests extends ESTestCase { diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DateIndexNameFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DateIndexNameFactoryTests.java index 32b181050102..1e247655c875 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DateIndexNameFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DateIndexNameFactoryTests.java @@ -11,14 +11,14 @@ import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ingest.TestTemplateService; import org.elasticsearch.test.ESTestCase; -import org.hamcrest.Matchers; import java.time.ZoneOffset; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import static org.hamcrest.Matchers.equalTo; + public class DateIndexNameFactoryTests extends ESTestCase { public void testDefaults() throws Exception { @@ -28,12 +28,12 @@ public void testDefaults() throws Exception { config.put("date_rounding", "y"); DateIndexNameProcessor processor = factory.create(null, null, null, config); - assertThat(processor.getDateFormats().size(), Matchers.equalTo(1)); - assertThat(processor.getField(), Matchers.equalTo("_field")); - assertThat(processor.getIndexNamePrefixTemplate().newInstance(Collections.emptyMap()).execute(), Matchers.equalTo("")); - assertThat(processor.getDateRoundingTemplate().newInstance(Collections.emptyMap()).execute(), Matchers.equalTo("y")); - assertThat(processor.getIndexNameFormatTemplate().newInstance(Collections.emptyMap()).execute(), Matchers.equalTo("yyyy-MM-dd")); - assertThat(processor.getTimezone(), Matchers.equalTo(ZoneOffset.UTC)); + assertThat(processor.getDateFormats().size(), equalTo(1)); + assertThat(processor.getField(), equalTo("_field")); + assertThat(processor.getIndexNamePrefixTemplate().newInstance(Map.of()).execute(), equalTo("")); + assertThat(processor.getDateRoundingTemplate().newInstance(Map.of()).execute(), equalTo("y")); + assertThat(processor.getIndexNameFormatTemplate().newInstance(Map.of()).execute(), equalTo("yyyy-MM-dd")); + assertThat(processor.getTimezone(), equalTo(ZoneOffset.UTC)); } public void testSpecifyOptionalSettings() throws Exception { @@ -42,10 +42,10 @@ public void testSpecifyOptionalSettings() throws Exception { config.put("field", "_field"); config.put("index_name_prefix", "_prefix"); config.put("date_rounding", "y"); - config.put("date_formats", Arrays.asList("UNIX", "UNIX_MS")); + config.put("date_formats", List.of("UNIX", "UNIX_MS")); DateIndexNameProcessor processor = factory.create(null, null, null, config); - assertThat(processor.getDateFormats().size(), Matchers.equalTo(2)); + assertThat(processor.getDateFormats().size(), equalTo(2)); config = new HashMap<>(); config.put("field", "_field"); @@ -54,7 +54,7 @@ public void testSpecifyOptionalSettings() throws Exception { config.put("index_name_format", "yyyyMMdd"); processor = factory.create(null, null, null, config); - assertThat(processor.getIndexNameFormatTemplate().newInstance(Collections.emptyMap()).execute(), Matchers.equalTo("yyyyMMdd")); + assertThat(processor.getIndexNameFormatTemplate().newInstance(Map.of()).execute(), equalTo("yyyyMMdd")); config = new HashMap<>(); config.put("field", "_field"); @@ -63,7 +63,7 @@ public void testSpecifyOptionalSettings() throws Exception { config.put("timezone", "+02:00"); processor = factory.create(null, null, null, config); - assertThat(processor.getTimezone(), Matchers.equalTo(ZoneOffset.ofHours(2))); + assertThat(processor.getTimezone(), equalTo(ZoneOffset.ofHours(2))); config = new HashMap<>(); config.put("field", "_field"); @@ -71,7 +71,7 @@ public void testSpecifyOptionalSettings() throws Exception { config.put("date_rounding", "y"); processor = factory.create(null, null, null, config); - assertThat(processor.getIndexNamePrefixTemplate().newInstance(Collections.emptyMap()).execute(), Matchers.equalTo("_prefix")); + assertThat(processor.getIndexNamePrefixTemplate().newInstance(Map.of()).execute(), equalTo("_prefix")); } public void testRequiredFields() throws Exception { @@ -79,11 +79,11 @@ public void testRequiredFields() throws Exception { Map config = new HashMap<>(); config.put("date_rounding", "y"); ElasticsearchParseException e = expectThrows(ElasticsearchParseException.class, () -> factory.create(null, null, null, config)); - assertThat(e.getMessage(), Matchers.equalTo("[field] required property is missing")); + assertThat(e.getMessage(), equalTo("[field] required property is missing")); config.clear(); config.put("field", "_field"); e = expectThrows(ElasticsearchParseException.class, () -> factory.create(null, null, null, config)); - assertThat(e.getMessage(), Matchers.equalTo("[date_rounding] required property is missing")); + assertThat(e.getMessage(), equalTo("[date_rounding] required property is missing")); } } diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DateIndexNameProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DateIndexNameProcessorTests.java index a79580d743a0..0257bbcd46a7 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DateIndexNameProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DateIndexNameProcessorTests.java @@ -15,54 +15,33 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.function.Function; -import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.equalTo; public class DateIndexNameProcessorTests extends ESTestCase { public void testJavaPattern() throws Exception { Function function = DateFormat.Java.getFunction("yyyy-MM-dd'T'HH:mm:ss.SSSXX", ZoneOffset.UTC, Locale.ROOT); - DateIndexNameProcessor processor = createProcessor( - "_field", - Collections.singletonList(function), - ZoneOffset.UTC, - "events-", - "y", - "yyyyMMdd" - ); - IngestDocument document = new IngestDocument( - "_index", - "_id", - 1, - null, - null, - Collections.singletonMap("_field", "2016-04-25T12:24:20.101Z") - ); + DateIndexNameProcessor processor = createProcessor("_field", List.of(function), ZoneOffset.UTC, "events-", "y", "yyyyMMdd"); + IngestDocument document = new IngestDocument("_index", "_id", 1, null, null, Map.of("_field", "2016-04-25T12:24:20.101Z")); processor.execute(document); assertThat(document.getSourceAndMetadata().get("_index"), equalTo("")); } public void testTAI64N() throws Exception { Function function = DateFormat.Tai64n.getFunction(null, ZoneOffset.UTC, null); - DateIndexNameProcessor dateProcessor = createProcessor( - "_field", - Collections.singletonList(function), - ZoneOffset.UTC, - "events-", - "m", - "yyyyMMdd" - ); + DateIndexNameProcessor dateProcessor = createProcessor("_field", List.of(function), ZoneOffset.UTC, "events-", "m", "yyyyMMdd"); IngestDocument document = new IngestDocument( "_index", "_id", 1, null, null, - Collections.singletonMap("_field", (randomBoolean() ? "@" : "") + "4000000050d506482dbdf024") + Map.of("_field", (randomBoolean() ? "@" : "") + "4000000050d506482dbdf024") ); dateProcessor.execute(document); assertThat(document.getSourceAndMetadata().get("_index"), equalTo("")); @@ -70,34 +49,20 @@ public void testTAI64N() throws Exception { public void testUnixMs() throws Exception { Function function = DateFormat.UnixMs.getFunction(null, ZoneOffset.UTC, null); - DateIndexNameProcessor dateProcessor = createProcessor( - "_field", - Collections.singletonList(function), - ZoneOffset.UTC, - "events-", - "m", - "yyyyMMdd" - ); - IngestDocument document = new IngestDocument("_index", "_id", 1, null, null, Collections.singletonMap("_field", "1000500")); + DateIndexNameProcessor dateProcessor = createProcessor("_field", List.of(function), ZoneOffset.UTC, "events-", "m", "yyyyMMdd"); + IngestDocument document = new IngestDocument("_index", "_id", 1, null, null, Map.of("_field", "1000500")); dateProcessor.execute(document); assertThat(document.getSourceAndMetadata().get("_index"), equalTo("")); - document = new IngestDocument("_index", "_id", 1, null, null, Collections.singletonMap("_field", 1000500L)); + document = new IngestDocument("_index", "_id", 1, null, null, Map.of("_field", 1000500L)); dateProcessor.execute(document); assertThat(document.getSourceAndMetadata().get("_index"), equalTo("")); } public void testUnix() throws Exception { Function function = DateFormat.Unix.getFunction(null, ZoneOffset.UTC, null); - DateIndexNameProcessor dateProcessor = createProcessor( - "_field", - Collections.singletonList(function), - ZoneOffset.UTC, - "events-", - "m", - "yyyyMMdd" - ); - IngestDocument document = new IngestDocument("_index", "_id", 1, null, null, Collections.singletonMap("_field", "1000.5")); + DateIndexNameProcessor dateProcessor = createProcessor("_field", List.of(function), ZoneOffset.UTC, "events-", "m", "yyyyMMdd"); + IngestDocument document = new IngestDocument("_index", "_id", 1, null, null, Map.of("_field", "1000.5")); dateProcessor.execute(document); assertThat(document.getSourceAndMetadata().get("_index"), equalTo("")); } @@ -111,14 +76,14 @@ public void testTemplatedFields() throws Exception { DateIndexNameProcessor dateProcessor = createProcessor( "_field", - Collections.singletonList(dateTimeFunction), + List.of(dateTimeFunction), ZoneOffset.UTC, indexNamePrefix, dateRounding, indexNameFormat ); - IngestDocument document = new IngestDocument("_index", "_id", 1, null, null, Collections.singletonMap("_field", date)); + IngestDocument document = new IngestDocument("_index", "_id", 1, null, null, Map.of("_field", date)); dateProcessor.execute(document); assertThat( diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DateProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DateProcessorFactoryTests.java index 5245a336acf4..52aa66c37d6a 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DateProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DateProcessorFactoryTests.java @@ -14,9 +14,8 @@ import org.junit.Before; import java.time.ZoneId; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; @@ -36,13 +35,13 @@ public void testBuildDefaults() throws Exception { Map config = new HashMap<>(); String sourceField = randomAlphaOfLengthBetween(1, 10); config.put("field", sourceField); - config.put("formats", Collections.singletonList("dd/MM/yyyyy")); + config.put("formats", List.of("dd/MM/yyyyy")); String processorTag = randomAlphaOfLength(10); DateProcessor processor = factory.create(null, processorTag, null, config); assertThat(processor.getTag(), equalTo(processorTag)); assertThat(processor.getField(), equalTo(sourceField)); assertThat(processor.getTargetField(), equalTo(DateProcessor.DEFAULT_TARGET_FIELD)); - assertThat(processor.getFormats(), equalTo(Collections.singletonList("dd/MM/yyyyy"))); + assertThat(processor.getFormats(), equalTo(List.of("dd/MM/yyyyy"))); assertNull(processor.getLocale()); assertNull(processor.getTimezone()); } @@ -51,7 +50,7 @@ public void testMatchFieldIsMandatory() throws Exception { Map config = new HashMap<>(); String targetField = randomAlphaOfLengthBetween(1, 10); config.put("target_field", targetField); - config.put("formats", Collections.singletonList("dd/MM/yyyyy")); + config.put("formats", List.of("dd/MM/yyyyy")); try { factory.create(null, null, null, config); @@ -80,34 +79,34 @@ public void testParseLocale() throws Exception { Map config = new HashMap<>(); String sourceField = randomAlphaOfLengthBetween(1, 10); config.put("field", sourceField); - config.put("formats", Collections.singletonList("dd/MM/yyyyy")); + config.put("formats", List.of("dd/MM/yyyyy")); Locale locale = randomFrom(Locale.GERMANY, Locale.FRENCH, Locale.ROOT); config.put("locale", locale.toLanguageTag()); DateProcessor processor = factory.create(null, null, null, config); - assertThat(processor.getLocale().newInstance(Collections.emptyMap()).execute(), equalTo(locale.toLanguageTag())); + assertThat(processor.getLocale().newInstance(Map.of()).execute(), equalTo(locale.toLanguageTag())); } public void testParseTimezone() throws Exception { Map config = new HashMap<>(); String sourceField = randomAlphaOfLengthBetween(1, 10); config.put("field", sourceField); - config.put("formats", Collections.singletonList("dd/MM/yyyyy")); + config.put("formats", List.of("dd/MM/yyyyy")); ZoneId timezone = randomZone(); config.put("timezone", timezone.getId()); DateProcessor processor = factory.create(null, null, null, config); - assertThat(processor.getTimezone().newInstance(Collections.emptyMap()).execute(), equalTo(timezone.getId())); + assertThat(processor.getTimezone().newInstance(Map.of()).execute(), equalTo(timezone.getId())); } public void testParseMatchFormats() throws Exception { Map config = new HashMap<>(); String sourceField = randomAlphaOfLengthBetween(1, 10); config.put("field", sourceField); - config.put("formats", Arrays.asList("dd/MM/yyyy", "dd-MM-yyyy")); + config.put("formats", List.of("dd/MM/yyyy", "dd-MM-yyyy")); DateProcessor processor = factory.create(null, null, null, config); - assertThat(processor.getFormats(), equalTo(Arrays.asList("dd/MM/yyyy", "dd-MM-yyyy"))); + assertThat(processor.getFormats(), equalTo(List.of("dd/MM/yyyy", "dd-MM-yyyy"))); } public void testParseMatchFormatsFailure() throws Exception { @@ -130,7 +129,7 @@ public void testParseTargetField() throws Exception { String targetField = randomAlphaOfLengthBetween(1, 10); config.put("field", sourceField); config.put("target_field", targetField); - config.put("formats", Arrays.asList("dd/MM/yyyy", "dd-MM-yyyy")); + config.put("formats", List.of("dd/MM/yyyy", "dd-MM-yyyy")); DateProcessor processor = factory.create(null, null, null, config); assertThat(processor.getTargetField(), equalTo(targetField)); @@ -143,7 +142,7 @@ public void testParseOutputFormat() throws Exception { String targetField = randomAlphaOfLengthBetween(1, 10); config.put("field", sourceField); config.put("target_field", targetField); - config.put("formats", Arrays.asList("dd/MM/yyyy", "dd-MM-yyyy")); + config.put("formats", List.of("dd/MM/yyyy", "dd-MM-yyyy")); config.put("output_format", outputFormat); DateProcessor processor = factory.create(null, null, null, config); assertThat(processor.getOutputFormat(), equalTo(outputFormat)); @@ -155,7 +154,7 @@ public void testDefaultOutputFormat() throws Exception { String targetField = randomAlphaOfLengthBetween(1, 10); config.put("field", sourceField); config.put("target_field", targetField); - config.put("formats", Arrays.asList("dd/MM/yyyy", "dd-MM-yyyy")); + config.put("formats", List.of("dd/MM/yyyy", "dd-MM-yyyy")); DateProcessor processor = factory.create(null, null, null, config); assertThat(processor.getOutputFormat(), equalTo(DateProcessor.DEFAULT_OUTPUT_FORMAT)); } @@ -167,7 +166,7 @@ public void testInvalidOutputFormatRejected() throws Exception { String targetField = randomAlphaOfLengthBetween(1, 10); config.put("field", sourceField); config.put("target_field", targetField); - config.put("formats", Arrays.asList("dd/MM/yyyy", "dd-MM-yyyy")); + config.put("formats", List.of("dd/MM/yyyy", "dd-MM-yyyy")); config.put("output_format", outputFormat); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> factory.create(null, null, null, config)); diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DateProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DateProcessorTests.java index be6ff3c5fdef..6ed5afdbf690 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DateProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DateProcessorTests.java @@ -19,15 +19,13 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; public class DateProcessorTests extends ESTestCase { @@ -46,7 +44,7 @@ public void testJavaPattern() { templatize(ZoneId.of("Europe/Amsterdam")), templatize(Locale.ENGLISH), "date_as_string", - Collections.singletonList("yyyy dd MM HH:mm:ss"), + List.of("yyyy dd MM HH:mm:ss"), "date_as_date" ); Map document = new HashMap<>(); @@ -129,7 +127,7 @@ public void testJavaPatternNoTimezone() { null, null, "date_as_string", - Arrays.asList("yyyy dd MM HH:mm:ss XXX"), + List.of("yyyy dd MM HH:mm:ss XXX"), "date_as_date" ); @@ -148,7 +146,7 @@ public void testInvalidJavaPattern() { templatize(ZoneOffset.UTC), templatize(randomLocale(random())), "date_as_string", - Collections.singletonList("invalid pattern"), + List.of("invalid pattern"), "date_as_date" ); Map document = new HashMap<>(); @@ -169,7 +167,7 @@ public void testJavaPatternLocale() { templatize(ZoneId.of("Europe/Amsterdam")), templatize(Locale.ITALIAN), "date_as_string", - Collections.singletonList("yyyy dd MMMM"), + List.of("yyyy dd MMMM"), "date_as_date" ); Map document = new HashMap<>(); @@ -187,7 +185,7 @@ public void testJavaPatternEnglishLocale() { templatize(ZoneId.of("Europe/Amsterdam")), templatize(Locale.ENGLISH), "date_as_string", - Collections.singletonList("yyyy dd MMMM"), + List.of("yyyy dd MMMM"), "date_as_date" ); Map document = new HashMap<>(); @@ -205,7 +203,7 @@ public void testJavaPatternDefaultYear() { templatize(ZoneId.of("Europe/Amsterdam")), templatize(Locale.ENGLISH), "date_as_string", - Collections.singletonList(format), + List.of(format), "date_as_date" ); Map document = new HashMap<>(); @@ -225,7 +223,7 @@ public void testTAI64N() { templatize(ZoneOffset.ofHours(2)), templatize(randomLocale(random())), "date_as_string", - Collections.singletonList("TAI64N"), + List.of("TAI64N"), "date_as_date" ); Map document = new HashMap<>(); @@ -243,7 +241,7 @@ public void testUnixMs() { templatize(ZoneOffset.UTC), templatize(randomLocale(random())), "date_as_string", - Collections.singletonList("UNIX_MS"), + List.of("UNIX_MS"), "date_as_date" ); Map document = new HashMap<>(); @@ -266,7 +264,7 @@ public void testUnix() { templatize(ZoneOffset.UTC), templatize(randomLocale(random())), "date_as_string", - Collections.singletonList("UNIX"), + List.of("UNIX"), "date_as_date" ); Map document = new HashMap<>(); @@ -283,7 +281,7 @@ public void testInvalidTimezone() { new TestTemplateService.MockTemplateScript.Factory("invalid_timezone"), templatize(randomLocale(random())), "date_as_string", - Collections.singletonList("yyyy"), + List.of("yyyy"), "date_as_date" ); Map document = new HashMap<>(); @@ -303,7 +301,7 @@ public void testInvalidLocale() { templatize(ZoneOffset.UTC), new TestTemplateService.MockTemplateScript.Factory("invalid_locale"), "date_as_string", - Collections.singletonList("yyyy"), + List.of("yyyy"), "date_as_date" ); Map document = new HashMap<>(); @@ -324,7 +322,7 @@ public void testOutputFormat() { null, null, "date_as_string", - Collections.singletonList("iso8601"), + List.of("iso8601"), "date_as_date", "HH:mm:ss.SSSSSSSSS" ); diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DissectProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DissectProcessorFactoryTests.java index c426ee1ef8be..b4bd9fea704b 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DissectProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DissectProcessorFactoryTests.java @@ -12,14 +12,13 @@ import org.elasticsearch.dissect.DissectException; import org.elasticsearch.ingest.RandomDocumentPicks; import org.elasticsearch.test.ESTestCase; -import org.hamcrest.Matchers; import java.util.HashMap; import java.util.Map; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; public class DissectProcessorFactoryTests extends ESTestCase { @@ -50,7 +49,7 @@ public void testCreateMissingField() { Map config = new HashMap<>(); config.put("pattern", "%{a},%{b},%{c}"); Exception e = expectThrows(ElasticsearchParseException.class, () -> factory.create(null, "_tag", null, config)); - assertThat(e.getMessage(), Matchers.equalTo("[field] required property is missing")); + assertThat(e.getMessage(), equalTo("[field] required property is missing")); } public void testCreateMissingPattern() { @@ -58,7 +57,7 @@ public void testCreateMissingPattern() { Map config = new HashMap<>(); config.put("field", randomAlphaOfLength(10)); Exception e = expectThrows(ElasticsearchParseException.class, () -> factory.create(null, "_tag", null, config)); - assertThat(e.getMessage(), Matchers.equalTo("[pattern] required property is missing")); + assertThat(e.getMessage(), equalTo("[pattern] required property is missing")); } public void testCreateMissingOptionals() { diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DissectProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DissectProcessorTests.java index ec2762ebfe53..f57d82a7c0d1 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DissectProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DissectProcessorTests.java @@ -14,12 +14,13 @@ import org.elasticsearch.ingest.Processor; import org.elasticsearch.ingest.RandomDocumentPicks; import org.elasticsearch.test.ESTestCase; -import org.hamcrest.CoreMatchers; import java.util.Collections; import java.util.HashMap; +import java.util.Map; import static org.elasticsearch.ingest.IngestDocumentMatcher.assertIngestDocument; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; /** @@ -29,14 +30,7 @@ public class DissectProcessorTests extends ESTestCase { public void testMatch() { - IngestDocument ingestDocument = new IngestDocument( - "_index", - "_id", - 1, - null, - null, - Collections.singletonMap("message", "foo,bar,baz") - ); + IngestDocument ingestDocument = new IngestDocument("_index", "_id", 1, null, null, Map.of("message", "foo,bar,baz")); DissectProcessor dissectProcessor = new DissectProcessor("", null, "message", "%{a},%{b},%{c}", "", true); dissectProcessor.execute(ingestDocument); assertThat(ingestDocument.getFieldValue("a", String.class), equalTo("foo")); @@ -68,7 +62,7 @@ public void testAdvancedMatch() { 1, null, null, - Collections.singletonMap("message", "foo bar,,,,,,,baz nope:notagain 😊 🐇 🙃") + Map.of("message", "foo bar,,,,,,,baz nope:notagain 😊 🐇 🙃") ); DissectProcessor dissectProcessor = new DissectProcessor( "", @@ -87,17 +81,10 @@ public void testAdvancedMatch() { } public void testMiss() { - IngestDocument ingestDocument = new IngestDocument( - "_index", - "_id", - 1, - null, - null, - Collections.singletonMap("message", "foo:bar,baz") - ); + IngestDocument ingestDocument = new IngestDocument("_index", "_id", 1, null, null, Map.of("message", "foo:bar,baz")); DissectProcessor dissectProcessor = new DissectProcessor("", null, "message", "%{a},%{b},%{c}", "", true); DissectException e = expectThrows(DissectException.class, () -> dissectProcessor.execute(ingestDocument)); - assertThat(e.getMessage(), CoreMatchers.containsString("Unable to find match for dissect pattern")); + assertThat(e.getMessage(), containsString("Unable to find match for dissect pattern")); } public void testNonStringValueWithIgnoreMissing() { diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DotExpanderProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DotExpanderProcessorTests.java index fc17506555ed..3e3c7af96486 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DotExpanderProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DotExpanderProcessorTests.java @@ -14,7 +14,6 @@ import org.elasticsearch.ingest.TestTemplateService; import org.elasticsearch.test.ESTestCase; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -43,7 +42,7 @@ public void testEscapeFields() throws Exception { source = new HashMap<>(); source.put("foo.bar", "baz1"); - source.put("foo", new HashMap<>(Collections.singletonMap("bar", "baz2"))); + source.put("foo", new HashMap<>(Map.of("bar", "baz2"))); document = TestIngestDocument.withDefaultVersion(source); processor = new DotExpanderProcessor("_tag", null, null, "foo.bar"); processor.execute(document); @@ -55,7 +54,7 @@ public void testEscapeFields() throws Exception { source = new HashMap<>(); source.put("foo.bar", "2"); - source.put("foo", new HashMap<>(Collections.singletonMap("bar", 1))); + source.put("foo", new HashMap<>(Map.of("bar", 1))); document = TestIngestDocument.withDefaultVersion(source); processor = new DotExpanderProcessor("_tag", null, null, "foo.bar"); processor.execute(document); @@ -103,7 +102,7 @@ public void testEscapeFields_valueField() throws Exception { source = new HashMap<>(); source.put("foo.bar.baz", "baz1"); - source.put("foo", new HashMap<>(Collections.singletonMap("bar", new HashMap<>()))); + source.put("foo", new HashMap<>(Map.of("bar", new HashMap<>()))); document = TestIngestDocument.withDefaultVersion(source); processor = new DotExpanderProcessor("_tag", null, null, "foo.bar.baz"); processor.execute(document); @@ -113,7 +112,7 @@ public void testEscapeFields_valueField() throws Exception { source = new HashMap<>(); source.put("foo.bar.baz", "baz1"); - source.put("foo", new HashMap<>(Collections.singletonMap("bar", "baz2"))); + source.put("foo", new HashMap<>(Map.of("bar", "baz2"))); IngestDocument document2 = TestIngestDocument.withDefaultVersion(source); Processor processor2 = new DotExpanderProcessor("_tag", null, null, "foo.bar.baz"); e = expectThrows(IllegalArgumentException.class, () -> processor2.execute(document2)); @@ -122,7 +121,7 @@ public void testEscapeFields_valueField() throws Exception { public void testEscapeFields_path() throws Exception { Map source = new HashMap<>(); - source.put("foo", new HashMap<>(Collections.singletonMap("bar.baz", "value"))); + source.put("foo", new HashMap<>(Map.of("bar.baz", "value"))); IngestDocument document = TestIngestDocument.withDefaultVersion(source); DotExpanderProcessor processor = new DotExpanderProcessor("_tag", null, "foo", "bar.baz"); processor.execute(document); @@ -131,7 +130,7 @@ public void testEscapeFields_path() throws Exception { assertThat(document.getFieldValue("foo.bar.baz", String.class), equalTo("value")); source = new HashMap<>(); - source.put("field", new HashMap<>(Collections.singletonMap("foo.bar.baz", "value"))); + source.put("field", new HashMap<>(Map.of("foo.bar.baz", "value"))); document = TestIngestDocument.withDefaultVersion(source); processor = new DotExpanderProcessor("_tag", null, "field", "foo.bar.baz"); processor.execute(document); diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/FailProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/FailProcessorFactoryTests.java index 4cf6ce1df10e..ad73ef65c6c9 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/FailProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/FailProcessorFactoryTests.java @@ -14,11 +14,10 @@ import org.elasticsearch.test.ESTestCase; import org.junit.Before; -import java.util.Collections; import java.util.HashMap; import java.util.Map; -import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.equalTo; public class FailProcessorFactoryTests extends ESTestCase { @@ -35,7 +34,7 @@ public void testCreate() throws Exception { String processorTag = randomAlphaOfLength(10); FailProcessor failProcessor = factory.create(null, processorTag, null, config); assertThat(failProcessor.getTag(), equalTo(processorTag)); - assertThat(failProcessor.getMessage().newInstance(Collections.emptyMap()).execute(), equalTo("error")); + assertThat(failProcessor.getMessage().newInstance(Map.of()).execute(), equalTo("error")); } public void testCreateMissingMessageField() throws Exception { diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/FingerprintProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/FingerprintProcessorFactoryTests.java index e7ec74520046..f9f808a0fe84 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/FingerprintProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/FingerprintProcessorFactoryTests.java @@ -14,7 +14,6 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.List; @@ -75,7 +74,7 @@ public void testMethod() throws Exception { // invalid method String invalidMethod = randomValueOtherThanMany( - m -> Arrays.asList(FingerprintProcessor.Factory.SUPPORTED_DIGESTS).contains(m), + m -> List.of(FingerprintProcessor.Factory.SUPPORTED_DIGESTS).contains(m), () -> randomAlphaOfLengthBetween(5, 9) ); config.put("fields", fieldList); diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ForEachProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ForEachProcessorFactoryTests.java index e5bd1910aa92..6aa6ab9b3e8e 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ForEachProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ForEachProcessorFactoryTests.java @@ -13,14 +13,16 @@ import org.elasticsearch.ingest.TestProcessor; import org.elasticsearch.script.ScriptService; import org.elasticsearch.test.ESTestCase; -import org.hamcrest.Matchers; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.function.Consumer; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.sameInstance; import static org.mockito.Mockito.mock; public class ForEachProcessorFactoryTests extends ESTestCase { @@ -36,11 +38,11 @@ public void testCreate() throws Exception { Map config = new HashMap<>(); config.put("field", "_field"); - config.put("processor", Collections.singletonMap("_name", Collections.emptyMap())); + config.put("processor", Map.of("_name", Collections.emptyMap())); ForEachProcessor forEachProcessor = forEachFactory.create(registry, null, null, config); - assertThat(forEachProcessor, Matchers.notNullValue()); + assertThat(forEachProcessor, notNullValue()); assertThat(forEachProcessor.getField(), equalTo("_field")); - assertThat(forEachProcessor.getInnerProcessor(), Matchers.sameInstance(processor)); + assertThat(forEachProcessor.getInnerProcessor(), sameInstance(processor)); assertFalse(forEachProcessor.isIgnoreMissing()); } @@ -52,12 +54,12 @@ public void testSetIgnoreMissing() throws Exception { Map config = new HashMap<>(); config.put("field", "_field"); - config.put("processor", Collections.singletonMap("_name", Collections.emptyMap())); + config.put("processor", Map.of("_name", Collections.emptyMap())); config.put("ignore_missing", true); ForEachProcessor forEachProcessor = forEachFactory.create(registry, null, null, config); - assertThat(forEachProcessor, Matchers.notNullValue()); + assertThat(forEachProcessor, notNullValue()); assertThat(forEachProcessor.getField(), equalTo("_field")); - assertThat(forEachProcessor.getInnerProcessor(), Matchers.sameInstance(processor)); + assertThat(forEachProcessor.getInnerProcessor(), sameInstance(processor)); assertTrue(forEachProcessor.isIgnoreMissing()); } @@ -71,8 +73,8 @@ public void testCreateWithTooManyProcessorTypes() throws Exception { Map config = new HashMap<>(); config.put("field", "_field"); Map processorTypes = new HashMap<>(); - processorTypes.put("_first", Collections.emptyMap()); - processorTypes.put("_second", Collections.emptyMap()); + processorTypes.put("_first", Map.of()); + processorTypes.put("_second", Map.of()); config.put("processor", processorTypes); Exception exception = expectThrows(ElasticsearchParseException.class, () -> forEachFactory.create(registry, null, null, config)); assertThat(exception.getMessage(), equalTo("[processor] Must specify exactly one processor type")); @@ -82,10 +84,10 @@ public void testCreateWithNonExistingProcessorType() throws Exception { ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService); Map config = new HashMap<>(); config.put("field", "_field"); - config.put("processor", Collections.singletonMap("_name", Collections.emptyMap())); + config.put("processor", Map.of("_name", Collections.emptyMap())); Exception expectedException = expectThrows( ElasticsearchParseException.class, - () -> forEachFactory.create(Collections.emptyMap(), null, null, config) + () -> forEachFactory.create(Map.of(), null, null, config) ); assertThat(expectedException.getMessage(), equalTo("No processor type exists with name [_name]")); } @@ -96,7 +98,7 @@ public void testCreateWithMissingField() throws Exception { registry.put("_name", (r, t, description, c) -> processor); ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService); Map config = new HashMap<>(); - config.put("processor", Collections.singletonList(Collections.singletonMap("_name", Collections.emptyMap()))); + config.put("processor", List.of(Map.of("_name", Map.of()))); Exception exception = expectThrows(Exception.class, () -> forEachFactory.create(registry, null, null, config)); assertThat(exception.getMessage(), equalTo("[field] required property is missing")); } @@ -105,7 +107,7 @@ public void testCreateWithMissingProcessor() { ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService); Map config = new HashMap<>(); config.put("field", "_field"); - Exception exception = expectThrows(Exception.class, () -> forEachFactory.create(Collections.emptyMap(), null, null, config)); + Exception exception = expectThrows(Exception.class, () -> forEachFactory.create(Map.of(), null, null, config)); assertThat(exception.getMessage(), equalTo("[processor] required property is missing")); } diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ForEachProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ForEachProcessorTests.java index 0d96e4c6680b..5f9936bd0c96 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ForEachProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ForEachProcessorTests.java @@ -17,8 +17,6 @@ import org.elasticsearch.test.ESTestCase; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -27,7 +25,6 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; -import java.util.stream.Collectors; import java.util.stream.IntStream; import static org.elasticsearch.ingest.IngestDocumentMatcher.assertIngestDocument; @@ -41,7 +38,7 @@ public void testExecuteWithAsyncProcessor() throws Exception { values.add("foo"); values.add("bar"); values.add("baz"); - IngestDocument ingestDocument = new IngestDocument("_index", "_id", 1, null, null, Collections.singletonMap("values", values)); + IngestDocument ingestDocument = new IngestDocument("_index", "_id", 1, null, null, Map.of("values", values)); ForEachProcessor processor = new ForEachProcessor("_tag", null, "values", new AsyncUpperCaseProcessor("_ingest._value"), false); execProcessor(processor, ingestDocument, (result, e) -> {}); @@ -57,14 +54,7 @@ public void testExecuteWithAsyncProcessor() throws Exception { } public void testExecuteWithFailure() { - IngestDocument ingestDocument = new IngestDocument( - "_index", - "_id", - 1, - null, - null, - Collections.singletonMap("values", Arrays.asList("a", "b", "c")) - ); + IngestDocument ingestDocument = new IngestDocument("_index", "_id", 1, null, null, Map.of("values", List.of("a", "b", "c"))); TestProcessor testProcessor = new TestProcessor(id -> { if ("c".equals(id.getFieldValue("_ingest._value", String.class))) { @@ -76,7 +66,7 @@ public void testExecuteWithFailure() { execProcessor(processor, ingestDocument, (result, e) -> exceptions[0] = e); assertThat(exceptions[0].getMessage(), equalTo("failure")); assertThat(testProcessor.getInvokedCounter(), equalTo(3)); - assertThat(ingestDocument.getFieldValue("values", List.class), equalTo(Arrays.asList("a", "b", "c"))); + assertThat(ingestDocument.getFieldValue("values", List.class), equalTo(List.of("a", "b", "c"))); testProcessor = new TestProcessor(id -> { String value = id.getFieldValue("_ingest._value", String.class); @@ -91,12 +81,12 @@ public void testExecuteWithFailure() { "_tag", null, "values", - new CompoundProcessor(false, Arrays.asList(testProcessor), Arrays.asList(onFailureProcessor)), + new CompoundProcessor(false, List.of(testProcessor), List.of(onFailureProcessor)), false ); execProcessor(processor, ingestDocument, (result, e) -> {}); assertThat(testProcessor.getInvokedCounter(), equalTo(3)); - assertThat(ingestDocument.getFieldValue("values", List.class), equalTo(Arrays.asList("A", "B", "c"))); + assertThat(ingestDocument.getFieldValue("values", List.class), equalTo(List.of("A", "B", "c"))); assertThat(ingestDocument.getFieldValue("error", String.class), equalTo("foo")); } @@ -104,7 +94,7 @@ public void testMetadataAvailable() { List> values = new ArrayList<>(); values.add(new HashMap<>()); values.add(new HashMap<>()); - IngestDocument ingestDocument = new IngestDocument("_index", "_id", 1, null, null, Collections.singletonMap("values", values)); + IngestDocument ingestDocument = new IngestDocument("_index", "_id", 1, null, null, Map.of("values", values)); TestProcessor innerProcessor = new TestProcessor(id -> { id.setFieldValue("_ingest._value.index", id.getSourceAndMetadata().get("_index")); @@ -180,9 +170,9 @@ public String getDescription() { } }; int numValues = randomIntBetween(1, 10000); - List values = IntStream.range(0, numValues).mapToObj(i -> "").collect(Collectors.toList()); + List values = IntStream.range(0, numValues).mapToObj(i -> "").toList(); - IngestDocument ingestDocument = new IngestDocument("_index", "_id", 1, null, null, Collections.singletonMap("values", values)); + IngestDocument ingestDocument = new IngestDocument("_index", "_id", 1, null, null, Map.of("values", values)); ForEachProcessor processor = new ForEachProcessor("_tag", null, "values", innerProcessor, false); execProcessor(processor, ingestDocument, (result, e) -> {}); @@ -198,7 +188,7 @@ public void testModifyFieldsOutsideArray() { values.add("string"); values.add(1); values.add(null); - IngestDocument ingestDocument = new IngestDocument("_index", "_id", 1, null, null, Collections.singletonMap("values", values)); + IngestDocument ingestDocument = new IngestDocument("_index", "_id", 1, null, null, Map.of("values", values)); TemplateScript.Factory template = new TestTemplateService.MockTemplateScript.Factory("errors"); @@ -209,7 +199,7 @@ public void testModifyFieldsOutsideArray() { new CompoundProcessor( false, List.of(new UppercaseProcessor("_tag_upper", null, "_ingest._value", false, "_ingest._value")), - List.of(new AppendProcessor("_tag", null, template, (model) -> (Collections.singletonList("added")), true)) + List.of(new AppendProcessor("_tag", null, template, (model) -> (List.of("added")), true)) ), false ); @@ -262,7 +252,7 @@ public void testNestedForEach() { value.put("values2", innerValues); values.add(value); - IngestDocument ingestDocument = new IngestDocument("_index", "_id", 1, null, null, Collections.singletonMap("values1", values)); + IngestDocument ingestDocument = new IngestDocument("_index", "_id", 1, null, null, Map.of("values1", values)); TestProcessor testProcessor = new TestProcessor( doc -> doc.setFieldValue("_ingest._value", doc.getFieldValue("_ingest._value", String.class).toUpperCase(Locale.ENGLISH)) @@ -337,7 +327,7 @@ public void testNestedForEachWithMapIteration() { } public void testIgnoreMissing() { - IngestDocument originalIngestDocument = new IngestDocument("_index", "_id", 1, null, null, Collections.emptyMap()); + IngestDocument originalIngestDocument = new IngestDocument("_index", "_id", 1, null, null, Map.of()); IngestDocument ingestDocument = new IngestDocument(originalIngestDocument); TestProcessor testProcessor = new TestProcessor(doc -> {}); ForEachProcessor processor = new ForEachProcessor("_tag", null, "_ingest._value", testProcessor, true); diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/GrokProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/GrokProcessorFactoryTests.java index 300f58de884e..41154634b43e 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/GrokProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/GrokProcessorFactoryTests.java @@ -12,8 +12,8 @@ import org.elasticsearch.grok.MatcherWatchdog; import org.elasticsearch.test.ESTestCase; -import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import static org.hamcrest.Matchers.equalTo; @@ -27,7 +27,7 @@ public void testBuild() throws Exception { Map config = new HashMap<>(); config.put("field", "_field"); - config.put("patterns", Collections.singletonList("(?\\w+)")); + config.put("patterns", List.of("(?\\w+)")); String processorTag = randomAlphaOfLength(10); GrokProcessor processor = factory.create(null, processorTag, null, config); assertThat(processor.getTag(), equalTo(processorTag)); @@ -41,7 +41,7 @@ public void testBuildWithIgnoreMissing() throws Exception { Map config = new HashMap<>(); config.put("field", "_field"); - config.put("patterns", Collections.singletonList("(?\\w+)")); + config.put("patterns", List.of("(?\\w+)")); config.put("ignore_missing", true); String processorTag = randomAlphaOfLength(10); GrokProcessor processor = factory.create(null, processorTag, null, config); @@ -54,7 +54,7 @@ public void testBuildWithIgnoreMissing() throws Exception { public void testBuildMissingField() throws Exception { GrokProcessor.Factory factory = new GrokProcessor.Factory(MatcherWatchdog.noop()); Map config = new HashMap<>(); - config.put("patterns", Collections.singletonList("(?\\w+)")); + config.put("patterns", List.of("(?\\w+)")); ElasticsearchParseException e = expectThrows(ElasticsearchParseException.class, () -> factory.create(null, null, null, config)); assertThat(e.getMessage(), equalTo("[field] required property is missing")); } @@ -71,7 +71,7 @@ public void testBuildEmptyPatternsList() throws Exception { GrokProcessor.Factory factory = new GrokProcessor.Factory(MatcherWatchdog.noop()); Map config = new HashMap<>(); config.put("field", "foo"); - config.put("patterns", Collections.emptyList()); + config.put("patterns", List.of()); ElasticsearchParseException e = expectThrows(ElasticsearchParseException.class, () -> factory.create(null, null, null, config)); assertThat(e.getMessage(), equalTo("[patterns] List of patterns must not be empty")); } @@ -81,8 +81,8 @@ public void testCreateWithCustomPatterns() throws Exception { Map config = new HashMap<>(); config.put("field", "_field"); - config.put("patterns", Collections.singletonList("%{MY_PATTERN:name}!")); - config.put("pattern_definitions", Collections.singletonMap("MY_PATTERN", "foo")); + config.put("patterns", List.of("%{MY_PATTERN:name}!")); + config.put("pattern_definitions", Map.of("MY_PATTERN", "foo")); GrokProcessor processor = factory.create(null, null, null, config); assertThat(processor.getMatchField(), equalTo("_field")); assertThat(processor.getGrok(), notNullValue()); @@ -93,7 +93,7 @@ public void testCreateWithInvalidPattern() throws Exception { GrokProcessor.Factory factory = new GrokProcessor.Factory(MatcherWatchdog.noop()); Map config = new HashMap<>(); config.put("field", "_field"); - config.put("patterns", Collections.singletonList("[")); + config.put("patterns", List.of("[")); ElasticsearchParseException e = expectThrows(ElasticsearchParseException.class, () -> factory.create(null, null, null, config)); assertThat(e.getMessage(), equalTo("[patterns] Invalid regex pattern found in: [[]. premature end of char-class")); } @@ -102,8 +102,8 @@ public void testCreateWithInvalidPatternDefinition() throws Exception { GrokProcessor.Factory factory = new GrokProcessor.Factory(MatcherWatchdog.noop()); Map config = new HashMap<>(); config.put("field", "_field"); - config.put("patterns", Collections.singletonList("%{MY_PATTERN:name}!")); - config.put("pattern_definitions", Collections.singletonMap("MY_PATTERN", "[")); + config.put("patterns", List.of("%{MY_PATTERN:name}!")); + config.put("pattern_definitions", Map.of("MY_PATTERN", "[")); ElasticsearchParseException e = expectThrows(ElasticsearchParseException.class, () -> factory.create(null, null, null, config)); assertThat( e.getMessage(), @@ -115,7 +115,7 @@ public void testCreateWithInvalidEcsCompatibilityMode() throws Exception { GrokProcessor.Factory factory = new GrokProcessor.Factory(MatcherWatchdog.noop()); Map config = new HashMap<>(); config.put("field", "_field"); - config.put("patterns", Collections.singletonList("(?\\w+)")); + config.put("patterns", List.of("(?\\w+)")); String invalidEcsMode = randomAlphaOfLength(3); config.put("ecs_compatibility", invalidEcsMode); ElasticsearchParseException e = expectThrows(ElasticsearchParseException.class, () -> factory.create(null, null, null, config)); diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/GrokProcessorGetActionTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/GrokProcessorGetActionTests.java index bf7b18814ca4..75f6d7a61735 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/GrokProcessorGetActionTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/GrokProcessorGetActionTests.java @@ -26,11 +26,11 @@ import java.util.List; import java.util.Map; -import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; -import static org.hamcrest.core.IsNull.notNullValue; -import static org.hamcrest.core.IsNull.nullValue; import static org.mockito.Mockito.mock; public class GrokProcessorGetActionTests extends ESTestCase { diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/GrokProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/GrokProcessorTests.java index bc8efeb32b56..2b0ff05ac9be 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/GrokProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/GrokProcessorTests.java @@ -14,9 +14,8 @@ import org.elasticsearch.ingest.TestIngestDocument; import org.elasticsearch.test.ESTestCase; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import static org.elasticsearch.ingest.IngestDocumentMatcher.assertIngestDocument; @@ -31,8 +30,8 @@ public void testMatch() throws Exception { GrokProcessor processor = new GrokProcessor( randomAlphaOfLength(10), null, - Collections.singletonMap("ONE", "1"), - Collections.singletonList("%{ONE:one}"), + Map.of("ONE", "1"), + List.of("%{ONE:one}"), fieldName, false, false, @@ -49,8 +48,8 @@ public void testIgnoreCase() throws Exception { GrokProcessor processor = new GrokProcessor( randomAlphaOfLength(10), null, - Collections.emptyMap(), - Collections.singletonList("(?(?i)A)"), + Map.of(), + List.of("(?(?i)A)"), fieldName, false, false, @@ -67,8 +66,8 @@ public void testNoMatch() { GrokProcessor processor = new GrokProcessor( randomAlphaOfLength(10), null, - Collections.singletonMap("ONE", "1"), - Collections.singletonList("%{ONE:one}"), + Map.of("ONE", "1"), + List.of("%{ONE:one}"), fieldName, false, false, @@ -87,8 +86,8 @@ public void testNoMatchingPatternName() { () -> new GrokProcessor( randomAlphaOfLength(10), null, - Collections.singletonMap("ONE", "1"), - Collections.singletonList("%{NOTONE:not_one}"), + Map.of("ONE", "1"), + List.of("%{NOTONE:not_one}"), fieldName, false, false, @@ -106,8 +105,8 @@ public void testMatchWithoutCaptures() throws Exception { GrokProcessor processor = new GrokProcessor( randomAlphaOfLength(10), null, - Collections.emptyMap(), - Collections.singletonList(fieldName), + Map.of(), + List.of(fieldName), fieldName, false, false, @@ -124,8 +123,8 @@ public void testNullField() { GrokProcessor processor = new GrokProcessor( randomAlphaOfLength(10), null, - Collections.singletonMap("ONE", "1"), - Collections.singletonList("%{ONE:one}"), + Map.of("ONE", "1"), + List.of("%{ONE:one}"), fieldName, false, false, @@ -143,8 +142,8 @@ public void testNullFieldWithIgnoreMissing() throws Exception { GrokProcessor processor = new GrokProcessor( randomAlphaOfLength(10), null, - Collections.singletonMap("ONE", "1"), - Collections.singletonList("%{ONE:one}"), + Map.of("ONE", "1"), + List.of("%{ONE:one}"), fieldName, false, true, @@ -161,8 +160,8 @@ public void testNotStringField() { GrokProcessor processor = new GrokProcessor( randomAlphaOfLength(10), null, - Collections.singletonMap("ONE", "1"), - Collections.singletonList("%{ONE:one}"), + Map.of("ONE", "1"), + List.of("%{ONE:one}"), fieldName, false, false, @@ -179,8 +178,8 @@ public void testNotStringFieldWithIgnoreMissing() { GrokProcessor processor = new GrokProcessor( randomAlphaOfLength(10), null, - Collections.singletonMap("ONE", "1"), - Collections.singletonList("%{ONE:one}"), + Map.of("ONE", "1"), + List.of("%{ONE:one}"), fieldName, false, true, @@ -196,8 +195,8 @@ public void testMissingField() { GrokProcessor processor = new GrokProcessor( randomAlphaOfLength(10), null, - Collections.singletonMap("ONE", "1"), - Collections.singletonList("%{ONE:one}"), + Map.of("ONE", "1"), + List.of("%{ONE:one}"), fieldName, false, false, @@ -214,8 +213,8 @@ public void testMissingFieldWithIgnoreMissing() throws Exception { GrokProcessor processor = new GrokProcessor( randomAlphaOfLength(10), null, - Collections.singletonMap("ONE", "1"), - Collections.singletonList("%{ONE:one}"), + Map.of("ONE", "1"), + List.of("%{ONE:one}"), fieldName, false, true, @@ -237,7 +236,7 @@ public void testMultiplePatternsWithMatchReturn() throws Exception { randomAlphaOfLength(10), null, patternBank, - Arrays.asList("%{ONE:one}", "%{TWO:two}", "%{THREE:three}"), + List.of("%{ONE:one}", "%{TWO:two}", "%{THREE:three}"), fieldName, false, false, @@ -261,7 +260,7 @@ public void testSetMetadata() throws Exception { randomAlphaOfLength(10), null, patternBank, - Arrays.asList("%{ONE:one}", "%{TWO:two}", "%{THREE:three}"), + List.of("%{ONE:one}", "%{TWO:two}", "%{THREE:three}"), fieldName, true, false, @@ -284,7 +283,7 @@ public void testTraceWithOnePattern() throws Exception { randomAlphaOfLength(10), null, patternBank, - Arrays.asList("%{ONE:one}"), + List.of("%{ONE:one}"), fieldName, true, false, @@ -297,17 +296,17 @@ public void testTraceWithOnePattern() throws Exception { public void testCombinedPatterns() { String combined; - combined = GrokProcessor.combinePatterns(Arrays.asList(""), false); + combined = GrokProcessor.combinePatterns(List.of(""), false); assertThat(combined, equalTo("")); - combined = GrokProcessor.combinePatterns(Arrays.asList(""), true); + combined = GrokProcessor.combinePatterns(List.of(""), true); assertThat(combined, equalTo("")); - combined = GrokProcessor.combinePatterns(Arrays.asList("foo"), false); + combined = GrokProcessor.combinePatterns(List.of("foo"), false); assertThat(combined, equalTo("foo")); - combined = GrokProcessor.combinePatterns(Arrays.asList("foo"), true); + combined = GrokProcessor.combinePatterns(List.of("foo"), true); assertThat(combined, equalTo("foo")); - combined = GrokProcessor.combinePatterns(Arrays.asList("foo", "bar"), false); + combined = GrokProcessor.combinePatterns(List.of("foo", "bar"), false); assertThat(combined, equalTo("(?:foo)|(?:bar)")); - combined = GrokProcessor.combinePatterns(Arrays.asList("foo", "bar"), true); + combined = GrokProcessor.combinePatterns(List.of("foo", "bar"), true); assertThat(combined, equalTo("(?<_ingest._grok_match_index.0>foo)|(?<_ingest._grok_match_index.1>bar)")); } @@ -323,7 +322,7 @@ public void testCombineSamePatternNameAcrossPatterns() throws Exception { randomAlphaOfLength(10), null, patternBank, - Arrays.asList("%{ONE:first}-%{TWO:second}", "%{ONE:first}-%{THREE:second}"), + List.of("%{ONE:first}-%{TWO:second}", "%{ONE:first}-%{THREE:second}"), fieldName, randomBoolean(), randomBoolean(), @@ -344,7 +343,7 @@ public void testFirstWinNamedCapture() throws Exception { randomAlphaOfLength(10), null, patternBank, - Collections.singletonList("%{ONETWO:first}%{ONETWO:first}"), + List.of("%{ONETWO:first}%{ONETWO:first}"), fieldName, randomBoolean(), randomBoolean(), @@ -365,7 +364,7 @@ public void testUnmatchedNamesNotIncludedInDocument() throws Exception { randomAlphaOfLength(10), null, patternBank, - Collections.singletonList("%{ONETWO:first}|%{THREE:second}"), + List.of("%{ONETWO:first}|%{THREE:second}"), fieldName, randomBoolean(), randomBoolean(), diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/GsubProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/GsubProcessorFactoryTests.java index 0e0d13cb9269..ce5a9e798741 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/GsubProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/GsubProcessorFactoryTests.java @@ -13,8 +13,8 @@ import java.util.HashMap; import java.util.Map; -import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; public class GsubProcessorFactoryTests extends AbstractStringProcessorFactoryTestCase { diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/JoinProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/JoinProcessorFactoryTests.java index 25303a42cf24..8f768f9c01bb 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/JoinProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/JoinProcessorFactoryTests.java @@ -14,7 +14,7 @@ import java.util.HashMap; import java.util.Map; -import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.equalTo; public class JoinProcessorFactoryTests extends ESTestCase { diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/JsonProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/JsonProcessorFactoryTests.java index c6d7d9deff80..288dbde352a5 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/JsonProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/JsonProcessorFactoryTests.java @@ -15,7 +15,7 @@ import java.util.HashMap; import java.util.Map; -import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.equalTo; public class JsonProcessorFactoryTests extends ESTestCase { diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/JsonProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/JsonProcessorTests.java index a9596fb0083a..50670b0ebaae 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/JsonProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/JsonProcessorTests.java @@ -17,7 +17,6 @@ import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xcontent.json.JsonXContent; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -128,7 +127,7 @@ public void testString() throws Exception { public void testArray() throws Exception { JsonProcessor jsonProcessor = new JsonProcessor("tag", null, "field", "target_field", false, REPLACE, false); Map document = new HashMap<>(); - List value = Arrays.asList(true, true, false); + List value = List.of(true, true, false); document.put("field", value.toString()); IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), document); jsonProcessor.execute(ingestDocument); diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/KeyValueProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/KeyValueProcessorFactoryTests.java index 3495428c9567..5d1669a33b7b 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/KeyValueProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/KeyValueProcessorFactoryTests.java @@ -15,12 +15,12 @@ import org.elasticsearch.test.ESTestCase; import org.junit.Before; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Set; -import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; @@ -41,7 +41,7 @@ public void testCreateWithDefaults() throws Exception { String processorTag = randomAlphaOfLength(10); KeyValueProcessor processor = factory.create(null, processorTag, null, config); assertThat(processor.getTag(), equalTo(processorTag)); - assertThat(processor.getField().newInstance(Collections.emptyMap()).execute(), equalTo("field1")); + assertThat(processor.getField().newInstance(Map.of()).execute(), equalTo("field1")); assertThat(processor.getFieldSplit(), equalTo("&")); assertThat(processor.getValueSplit(), equalTo("=")); assertThat(processor.getIncludeKeys(), is(nullValue())); @@ -55,18 +55,18 @@ public void testCreateWithAllFieldsSet() throws Exception { config.put("field_split", "&"); config.put("value_split", "="); config.put("target_field", "target"); - config.put("include_keys", Arrays.asList("a", "b")); - config.put("exclude_keys", Collections.emptyList()); + config.put("include_keys", List.of("a", "b")); + config.put("exclude_keys", List.of()); config.put("ignore_missing", true); String processorTag = randomAlphaOfLength(10); KeyValueProcessor processor = factory.create(null, processorTag, null, config); assertThat(processor.getTag(), equalTo(processorTag)); - assertThat(processor.getField().newInstance(Collections.emptyMap()).execute(), equalTo("field1")); + assertThat(processor.getField().newInstance(Map.of()).execute(), equalTo("field1")); assertThat(processor.getFieldSplit(), equalTo("&")); assertThat(processor.getValueSplit(), equalTo("=")); assertThat(processor.getIncludeKeys(), equalTo(Sets.newHashSet("a", "b"))); - assertThat(processor.getExcludeKeys(), equalTo(Collections.emptySet())); - assertThat(processor.getTargetField().newInstance(Collections.emptyMap()).execute(), equalTo("target")); + assertThat(processor.getExcludeKeys(), equalTo(Set.of())); + assertThat(processor.getTargetField().newInstance(Map.of()).execute(), equalTo("target")); assertTrue(processor.isIgnoreMissing()); } diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/KeyValueProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/KeyValueProcessorTests.java index 44eed2611723..af9d7d952f33 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/KeyValueProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/KeyValueProcessorTests.java @@ -16,7 +16,6 @@ import org.elasticsearch.test.ESTestCase; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -36,24 +35,24 @@ public void test() throws Exception { Processor processor = createKvProcessor(fieldName, "&", "=", null, null, "target", false); processor.execute(ingestDocument); assertThat(ingestDocument.getFieldValue("target.first", String.class), equalTo("hello")); - assertThat(ingestDocument.getFieldValue("target.second", List.class), equalTo(Arrays.asList("world", "universe"))); + assertThat(ingestDocument.getFieldValue("target.second", List.class), equalTo(List.of("world", "universe"))); } public void testRootTarget() throws Exception { - IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Collections.emptyMap()); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Map.of()); ingestDocument.setFieldValue("myField", "first=hello&second=world&second=universe"); Processor processor = createKvProcessor("myField", "&", "=", null, null, null, false); processor.execute(ingestDocument); assertThat(ingestDocument.getFieldValue("first", String.class), equalTo("hello")); - assertThat(ingestDocument.getFieldValue("second", List.class), equalTo(Arrays.asList("world", "universe"))); + assertThat(ingestDocument.getFieldValue("second", List.class), equalTo(List.of("world", "universe"))); } public void testKeySameAsSourceField() throws Exception { - IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Collections.emptyMap()); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Map.of()); ingestDocument.setFieldValue("first", "first=hello"); Processor processor = createKvProcessor("first", "&", "=", null, null, null, false); processor.execute(ingestDocument); - assertThat(ingestDocument.getFieldValue("first", List.class), equalTo(Arrays.asList("first=hello", "hello"))); + assertThat(ingestDocument.getFieldValue("first", List.class), equalTo(List.of("first=hello", "hello"))); } public void testIncludeKeys() throws Exception { @@ -97,7 +96,7 @@ public void testIncludeAndExcludeKeys() throws Exception { } public void testMissingField() throws Exception { - IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Collections.emptyMap()); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Map.of()); Processor processor = createKvProcessor("unknown", "&", "=", null, null, "target", false); IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> processor.execute(ingestDocument)); assertThat(exception.getMessage(), equalTo("field [unknown] doesn't exist")); @@ -116,7 +115,7 @@ public void testNullValueWithIgnoreMissing() throws Exception { } public void testNonExistentWithIgnoreMissing() throws Exception { - IngestDocument originalIngestDocument = RandomDocumentPicks.randomIngestDocument(random(), Collections.emptyMap()); + IngestDocument originalIngestDocument = RandomDocumentPicks.randomIngestDocument(random(), Map.of()); IngestDocument ingestDocument = new IngestDocument(originalIngestDocument); Processor processor = createKvProcessor("unknown", "", "", null, null, "target", true); processor.execute(ingestDocument); @@ -133,7 +132,7 @@ public void testFailFieldSplitMatch() throws Exception { } public void testFailValueSplitMatch() throws Exception { - IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Collections.singletonMap("foo", "bar")); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Map.of("foo", "bar")); Processor processor = createKvProcessor("foo", "&", "=", null, null, "target", false); Exception exception = expectThrows(IllegalArgumentException.class, () -> processor.execute(ingestDocument)); assertThat(exception.getMessage(), equalTo("field [foo] does not contain value_split [=]")); @@ -145,7 +144,7 @@ public void testTrimKeyAndValue() throws Exception { Processor processor = createKvProcessor(fieldName, "&", "=", null, null, "target", false, " ", " ", false, null); processor.execute(ingestDocument); assertThat(ingestDocument.getFieldValue("target.first", String.class), equalTo("hello")); - assertThat(ingestDocument.getFieldValue("target.second", List.class), equalTo(Arrays.asList("world", "universe"))); + assertThat(ingestDocument.getFieldValue("target.second", List.class), equalTo(List.of("world", "universe"))); } public void testTrimMultiCharSequence() throws Exception { @@ -177,7 +176,7 @@ public void testStripBrackets() throws Exception { Processor processor = createKvProcessor(fieldName, "&", "=", null, null, "target", false, null, null, true, null); processor.execute(ingestDocument); assertThat(ingestDocument.getFieldValue("target.first", String.class), equalTo("hello")); - assertThat(ingestDocument.getFieldValue("target.second", List.class), equalTo(Arrays.asList("world", "universe"))); + assertThat(ingestDocument.getFieldValue("target.second", List.class), equalTo(List.of("world", "universe"))); assertThat(ingestDocument.getFieldValue("target.third", String.class), equalTo("foo")); assertThat(ingestDocument.getFieldValue("target.fourth", String.class), equalTo("bar")); assertThat(ingestDocument.getFieldValue("target.fifth", String.class), equalTo("last")); @@ -189,7 +188,7 @@ public void testAddPrefix() throws Exception { Processor processor = createKvProcessor(fieldName, "&", "=", null, null, "target", false, null, null, false, "arg_"); processor.execute(ingestDocument); assertThat(ingestDocument.getFieldValue("target.arg_first", String.class), equalTo("hello")); - assertThat(ingestDocument.getFieldValue("target.arg_second", List.class), equalTo(Arrays.asList("world", "universe"))); + assertThat(ingestDocument.getFieldValue("target.arg_second", List.class), equalTo(List.of("world", "universe"))); } private static KeyValueProcessor createKvProcessor( diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/NetworkDirectionProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/NetworkDirectionProcessorFactoryTests.java index deece8362678..59787e106307 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/NetworkDirectionProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/NetworkDirectionProcessorFactoryTests.java @@ -14,7 +14,6 @@ import org.junit.Before; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -22,7 +21,7 @@ import static org.elasticsearch.ingest.common.NetworkDirectionProcessor.Factory.DEFAULT_DEST_IP; import static org.elasticsearch.ingest.common.NetworkDirectionProcessor.Factory.DEFAULT_SOURCE_IP; import static org.elasticsearch.ingest.common.NetworkDirectionProcessor.Factory.DEFAULT_TARGET; -import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; public class NetworkDirectionProcessorFactoryTests extends ESTestCase { @@ -56,7 +55,7 @@ public void testCreate() throws Exception { assertThat(networkProcessor.getDestinationIpField(), equalTo(destIpField)); assertThat(networkProcessor.getTargetField(), equalTo(targetField)); assertThat(networkProcessor.getInternalNetworks().size(), greaterThan(0)); - assertThat(networkProcessor.getInternalNetworks().get(0).newInstance(Collections.emptyMap()).execute(), equalTo("10.0.0.0/8")); + assertThat(networkProcessor.getInternalNetworks().get(0).newInstance(Map.of()).execute(), equalTo("10.0.0.0/8")); assertThat(networkProcessor.getIgnoreMissing(), equalTo(ignoreMissing)); } diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/NetworkDirectionProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/NetworkDirectionProcessorTests.java index 4ae543afc393..7c53df0ca3f4 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/NetworkDirectionProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/NetworkDirectionProcessorTests.java @@ -15,7 +15,6 @@ import org.elasticsearch.test.ESTestCase; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -183,7 +182,7 @@ private void testNetworkDirectionProcessor( ) throws Exception { List networks = null; - if (internalNetworks != null) networks = Arrays.asList(internalNetworks); + if (internalNetworks != null) networks = List.of(internalNetworks); String processorTag = randomAlphaOfLength(10); Map config = new HashMap<>(); diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RegisteredDomainProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RegisteredDomainProcessorFactoryTests.java index f5100298c63d..5dc20dd9e07b 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RegisteredDomainProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RegisteredDomainProcessorFactoryTests.java @@ -15,7 +15,7 @@ import java.util.HashMap; import java.util.Map; -import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.equalTo; public class RegisteredDomainProcessorFactoryTests extends ESTestCase { diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RemoveProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RemoveProcessorFactoryTests.java index 876f7823839f..49a9dd065184 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RemoveProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RemoveProcessorFactoryTests.java @@ -14,13 +14,11 @@ import org.elasticsearch.test.ESTestCase; import org.junit.Before; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; -import java.util.stream.Collectors; -import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.equalTo; public class RemoveProcessorFactoryTests extends ESTestCase { @@ -37,31 +35,28 @@ public void testCreate() throws Exception { String processorTag = randomAlphaOfLength(10); RemoveProcessor removeProcessor = factory.create(null, processorTag, null, config); assertThat(removeProcessor.getTag(), equalTo(processorTag)); - assertThat(removeProcessor.getFieldsToRemove().get(0).newInstance(Collections.emptyMap()).execute(), equalTo("field1")); + assertThat(removeProcessor.getFieldsToRemove().get(0).newInstance(Map.of()).execute(), equalTo("field1")); } public void testCreateKeepField() throws Exception { Map config = new HashMap<>(); - config.put("keep", Arrays.asList("field1", "field2")); + config.put("keep", List.of("field1", "field2")); String processorTag = randomAlphaOfLength(10); RemoveProcessor removeProcessor = factory.create(null, processorTag, null, config); assertThat(removeProcessor.getTag(), equalTo(processorTag)); - assertThat(removeProcessor.getFieldsToKeep().get(0).newInstance(Collections.emptyMap()).execute(), equalTo("field1")); - assertThat(removeProcessor.getFieldsToKeep().get(1).newInstance(Collections.emptyMap()).execute(), equalTo("field2")); + assertThat(removeProcessor.getFieldsToKeep().get(0).newInstance(Map.of()).execute(), equalTo("field1")); + assertThat(removeProcessor.getFieldsToKeep().get(1).newInstance(Map.of()).execute(), equalTo("field2")); } public void testCreateMultipleFields() throws Exception { Map config = new HashMap<>(); - config.put("field", Arrays.asList("field1", "field2")); + config.put("field", List.of("field1", "field2")); String processorTag = randomAlphaOfLength(10); RemoveProcessor removeProcessor = factory.create(null, processorTag, null, config); assertThat(removeProcessor.getTag(), equalTo(processorTag)); assertThat( - removeProcessor.getFieldsToRemove() - .stream() - .map(template -> template.newInstance(Collections.emptyMap()).execute()) - .collect(Collectors.toList()), - equalTo(Arrays.asList("field1", "field2")) + removeProcessor.getFieldsToRemove().stream().map(template -> template.newInstance(Map.of()).execute()).toList(), + equalTo(List.of("field1", "field2")) ); } diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RemoveProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RemoveProcessorTests.java index f17f019dc36c..5bda0972401e 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RemoveProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RemoveProcessorTests.java @@ -16,7 +16,6 @@ import org.elasticsearch.test.ESTestCase; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -32,8 +31,8 @@ public void testRemoveFields() throws Exception { Processor processor = new RemoveProcessor( randomAlphaOfLength(10), null, - Collections.singletonList(new TestTemplateService.MockTemplateScript.Factory(field)), - Collections.emptyList(), + List.of(new TestTemplateService.MockTemplateScript.Factory(field)), + List.of(), false ); processor.execute(ingestDocument); @@ -107,34 +106,16 @@ public void testShouldKeep(String a, String b) { IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), source); - assertTrue( - RemoveProcessor.shouldKeep( - "name", - Collections.singletonList(new TestTemplateService.MockTemplateScript.Factory("name")), - ingestDocument - ) - ); + assertTrue(RemoveProcessor.shouldKeep("name", List.of(new TestTemplateService.MockTemplateScript.Factory("name")), ingestDocument)); - assertTrue( - RemoveProcessor.shouldKeep( - "age", - Collections.singletonList(new TestTemplateService.MockTemplateScript.Factory("age")), - ingestDocument - ) - ); + assertTrue(RemoveProcessor.shouldKeep("age", List.of(new TestTemplateService.MockTemplateScript.Factory("age")), ingestDocument)); - assertFalse( - RemoveProcessor.shouldKeep( - "name", - Collections.singletonList(new TestTemplateService.MockTemplateScript.Factory("age")), - ingestDocument - ) - ); + assertFalse(RemoveProcessor.shouldKeep("name", List.of(new TestTemplateService.MockTemplateScript.Factory("age")), ingestDocument)); assertTrue( RemoveProcessor.shouldKeep( "address", - Collections.singletonList(new TestTemplateService.MockTemplateScript.Factory("address.street")), + List.of(new TestTemplateService.MockTemplateScript.Factory("address.street")), ingestDocument ) ); @@ -142,7 +123,7 @@ public void testShouldKeep(String a, String b) { assertTrue( RemoveProcessor.shouldKeep( "address", - Collections.singletonList(new TestTemplateService.MockTemplateScript.Factory("address.number")), + List.of(new TestTemplateService.MockTemplateScript.Factory("address.number")), ingestDocument ) ); @@ -150,7 +131,7 @@ public void testShouldKeep(String a, String b) { assertTrue( RemoveProcessor.shouldKeep( "address.street", - Collections.singletonList(new TestTemplateService.MockTemplateScript.Factory("address")), + List.of(new TestTemplateService.MockTemplateScript.Factory("address")), ingestDocument ) ); @@ -158,23 +139,19 @@ public void testShouldKeep(String a, String b) { assertTrue( RemoveProcessor.shouldKeep( "address.number", - Collections.singletonList(new TestTemplateService.MockTemplateScript.Factory("address")), + List.of(new TestTemplateService.MockTemplateScript.Factory("address")), ingestDocument ) ); assertTrue( - RemoveProcessor.shouldKeep( - "address", - Collections.singletonList(new TestTemplateService.MockTemplateScript.Factory("address")), - ingestDocument - ) + RemoveProcessor.shouldKeep("address", List.of(new TestTemplateService.MockTemplateScript.Factory("address")), ingestDocument) ); assertFalse( RemoveProcessor.shouldKeep( "address.street", - Collections.singletonList(new TestTemplateService.MockTemplateScript.Factory("address.number")), + List.of(new TestTemplateService.MockTemplateScript.Factory("address.number")), ingestDocument ) ); diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RenameProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RenameProcessorFactoryTests.java index ad1f6f0962de..2299081eb22c 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RenameProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RenameProcessorFactoryTests.java @@ -13,11 +13,10 @@ import org.elasticsearch.test.ESTestCase; import org.junit.Before; -import java.util.Collections; import java.util.HashMap; import java.util.Map; -import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.equalTo; public class RenameProcessorFactoryTests extends ESTestCase { @@ -35,8 +34,8 @@ public void testCreate() throws Exception { String processorTag = randomAlphaOfLength(10); RenameProcessor renameProcessor = factory.create(null, processorTag, null, config); assertThat(renameProcessor.getTag(), equalTo(processorTag)); - assertThat(renameProcessor.getField().newInstance(Collections.emptyMap()).execute(), equalTo("old_field")); - assertThat(renameProcessor.getTargetField().newInstance(Collections.emptyMap()).execute(), equalTo("new_field")); + assertThat(renameProcessor.getField().newInstance(Map.of()).execute(), equalTo("old_field")); + assertThat(renameProcessor.getTargetField().newInstance(Map.of()).execute(), equalTo("new_field")); assertThat(renameProcessor.isIgnoreMissing(), equalTo(false)); } @@ -48,8 +47,8 @@ public void testCreateWithIgnoreMissing() throws Exception { String processorTag = randomAlphaOfLength(10); RenameProcessor renameProcessor = factory.create(null, processorTag, null, config); assertThat(renameProcessor.getTag(), equalTo(processorTag)); - assertThat(renameProcessor.getField().newInstance(Collections.emptyMap()).execute(), equalTo("old_field")); - assertThat(renameProcessor.getTargetField().newInstance(Collections.emptyMap()).execute(), equalTo("new_field")); + assertThat(renameProcessor.getField().newInstance(Map.of()).execute(), equalTo("old_field")); + assertThat(renameProcessor.getTargetField().newInstance(Map.of()).execute(), equalTo("new_field")); assertThat(renameProcessor.isIgnoreMissing(), equalTo(true)); } diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RenameProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RenameProcessorTests.java index 5908fc8784d8..1d10c3090990 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RenameProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RenameProcessorTests.java @@ -17,7 +17,6 @@ import org.elasticsearch.test.ESTestCase; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -50,8 +49,8 @@ public void testRenameArrayElement() throws Exception { list.add("item3"); document.put("list", list); List> one = new ArrayList<>(); - one.add(Collections.singletonMap("one", "one")); - one.add(Collections.singletonMap("two", "two")); + one.add(Map.of("one", "one")); + one.add(Map.of("two", "two")); document.put("one", one); IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), document); @@ -139,7 +138,7 @@ public void testRenameExistingFieldNullValue() throws Exception { public void testRenameAtomicOperationSetFails() throws Exception { Map metadata = new HashMap<>(); - metadata.put("list", Collections.singletonList("item")); + metadata.put("list", List.of("item")); IngestDocument ingestDocument = TestIngestDocument.ofMetadataWithValidator( metadata, @@ -162,7 +161,7 @@ public void testRenameAtomicOperationSetFails() throws Exception { public void testRenameAtomicOperationRemoveFails() throws Exception { Map metadata = new HashMap<>(); - metadata.put("list", Collections.singletonList("item")); + metadata.put("list", List.of("item")); IngestDocument ingestDocument = TestIngestDocument.ofMetadataWithValidator( metadata, @@ -185,16 +184,13 @@ public void testRenameLeafIntoBranch() throws Exception { IngestDocument ingestDocument = TestIngestDocument.withDefaultVersion(source); Processor processor1 = createRenameProcessor("foo", "foo.bar", false); processor1.execute(ingestDocument); - assertThat(ingestDocument.getFieldValue("foo", Map.class), equalTo(Collections.singletonMap("bar", "bar"))); + assertThat(ingestDocument.getFieldValue("foo", Map.class), equalTo(Map.of("bar", "bar"))); assertThat(ingestDocument.getFieldValue("foo.bar", String.class), equalTo("bar")); Processor processor2 = createRenameProcessor("foo.bar", "foo.bar.baz", false); processor2.execute(ingestDocument); - assertThat( - ingestDocument.getFieldValue("foo", Map.class), - equalTo(Collections.singletonMap("bar", Collections.singletonMap("baz", "bar"))) - ); - assertThat(ingestDocument.getFieldValue("foo.bar", Map.class), equalTo(Collections.singletonMap("baz", "bar"))); + assertThat(ingestDocument.getFieldValue("foo", Map.class), equalTo(Map.of("bar", Map.of("baz", "bar")))); + assertThat(ingestDocument.getFieldValue("foo.bar", Map.class), equalTo(Map.of("baz", "bar"))); assertThat(ingestDocument.getFieldValue("foo.bar.baz", String.class), equalTo("bar")); // for fun lets try to restore it (which don't allow today) diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ScriptProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ScriptProcessorFactoryTests.java index 4a8a01218b52..b3e4a870177b 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ScriptProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ScriptProcessorFactoryTests.java @@ -23,14 +23,14 @@ import org.elasticsearch.xcontent.XContentParseException; import org.junit.Before; -import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; -import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -55,7 +55,7 @@ public void testFactoryValidationWithDefaultLang() throws Exception { ScriptProcessor processor = factory.create(null, randomAlphaOfLength(10), null, configMap); assertThat(processor.getScript().getLang(), equalTo(randomType.equals("id") ? null : Script.DEFAULT_SCRIPT_LANG)); assertThat(processor.getScript().getType().toString(), equalTo(INGEST_SCRIPT_PARAM_TO_TYPE.get(randomType))); - assertThat(processor.getScript().getParams(), equalTo(Collections.emptyMap())); + assertThat(processor.getScript().getParams(), equalTo(Map.of())); } public void testFactoryValidationWithParams() throws Exception { @@ -65,7 +65,7 @@ public void testFactoryValidationWithParams() throws Exception { Map configMap = new HashMap<>(); String randomType = randomFrom("id", "source"); - Map randomParams = Collections.singletonMap(randomAlphaOfLength(10), randomAlphaOfLength(10)); + Map randomParams = Map.of(randomAlphaOfLength(10), randomAlphaOfLength(10)); configMap.put(randomType, "foo"); configMap.put("params", randomParams); ScriptProcessor processor = factory.create(null, randomAlphaOfLength(10), null, configMap); @@ -117,7 +117,7 @@ public void testFactoryInvalidateWithInvalidCompiledScript() throws Exception { ScriptException thrownException = new ScriptException( "compile-time exception", new RuntimeException(), - Collections.emptyList(), + List.of(), "script", "mockscript" ); @@ -139,13 +139,10 @@ public void testInlineIsCompiled() throws Exception { String scriptName = "foo"; ScriptService scriptService = new ScriptService( Settings.builder().build(), - Collections.singletonMap( - Script.DEFAULT_SCRIPT_LANG, - new MockScriptEngine(Script.DEFAULT_SCRIPT_LANG, Collections.singletonMap(scriptName, ctx -> { - ctx.put("foo", "bar"); - return null; - }), Collections.emptyMap()) - ), + Map.of(Script.DEFAULT_SCRIPT_LANG, new MockScriptEngine(Script.DEFAULT_SCRIPT_LANG, Map.of(scriptName, ctx -> { + ctx.put("foo", "bar"); + return null; + }), Map.of())), new HashMap<>(ScriptModule.CORE_CONTEXTS), () -> 1L ); @@ -156,7 +153,7 @@ public void testInlineIsCompiled() throws Exception { ScriptProcessor processor = factory.create(null, null, randomAlphaOfLength(10), configMap); assertThat(processor.getScript().getLang(), equalTo(Script.DEFAULT_SCRIPT_LANG)); assertThat(processor.getScript().getType(), equalTo(ScriptType.INLINE)); - assertThat(processor.getScript().getParams(), equalTo(Collections.emptyMap())); + assertThat(processor.getScript().getParams(), equalTo(Map.of())); assertNotNull(processor.getPrecompiledIngestScriptFactory()); CtxMap ctx = TestIngestDocument.emptyIngestDocument().getCtxMap(); processor.getPrecompiledIngestScriptFactory().newInstance(null, ctx).execute(); @@ -172,7 +169,7 @@ public void testStoredIsNotCompiled() throws Exception { ScriptProcessor processor = factory.create(null, null, randomAlphaOfLength(10), configMap); assertNull(processor.getScript().getLang()); assertThat(processor.getScript().getType(), equalTo(ScriptType.STORED)); - assertThat(processor.getScript().getParams(), equalTo(Collections.emptyMap())); + assertThat(processor.getScript().getParams(), equalTo(Map.of())); assertNull(processor.getPrecompiledIngestScriptFactory()); } } diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ScriptProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ScriptProcessorTests.java index 972ca029b7b0..91dd0c861c02 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ScriptProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ScriptProcessorTests.java @@ -20,13 +20,12 @@ import org.elasticsearch.test.ESTestCase; import org.junit.Before; -import java.util.Collections; import java.util.HashMap; import java.util.Map; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasKey; -import static org.hamcrest.core.Is.is; +import static org.hamcrest.Matchers.is; public class ScriptProcessorTests extends ESTestCase { @@ -39,20 +38,17 @@ public void setupScripting() { String scriptName = "script"; scriptService = new ScriptService( Settings.builder().build(), - Collections.singletonMap( - Script.DEFAULT_SCRIPT_LANG, - new MockScriptEngine(Script.DEFAULT_SCRIPT_LANG, Collections.singletonMap(scriptName, ctx -> { - Integer bytesIn = (Integer) ctx.get("bytes_in"); - Integer bytesOut = (Integer) ctx.get("bytes_out"); - ctx.put("bytes_total", bytesIn + bytesOut); - ctx.put("_dynamic_templates", Map.of("foo", "bar")); - return null; - }), Collections.emptyMap()) - ), + Map.of(Script.DEFAULT_SCRIPT_LANG, new MockScriptEngine(Script.DEFAULT_SCRIPT_LANG, Map.of(scriptName, ctx -> { + Integer bytesIn = (Integer) ctx.get("bytes_in"); + Integer bytesOut = (Integer) ctx.get("bytes_out"); + ctx.put("bytes_total", bytesIn + bytesOut); + ctx.put("_dynamic_templates", Map.of("foo", "bar")); + return null; + }), Map.of())), new HashMap<>(ScriptModule.CORE_CONTEXTS), () -> 1L ); - script = new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, scriptName, Collections.emptyMap()); + script = new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, scriptName, Map.of()); ingestScriptFactory = scriptService.compile(script, IngestScript.CONTEXT); } diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SetProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SetProcessorFactoryTests.java index ac1af5f3dcdd..085cc68fe363 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SetProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SetProcessorFactoryTests.java @@ -15,13 +15,12 @@ import org.elasticsearch.test.ESTestCase; import org.junit.Before; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; -import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; public class SetProcessorFactoryTests extends ESTestCase { @@ -39,8 +38,8 @@ public void testCreate() throws Exception { String processorTag = randomAlphaOfLength(10); SetProcessor setProcessor = factory.create(null, processorTag, null, config); assertThat(setProcessor.getTag(), equalTo(processorTag)); - assertThat(setProcessor.getField().newInstance(Collections.emptyMap()).execute(), equalTo("field1")); - assertThat(setProcessor.getValue().copyAndResolve(Collections.emptyMap()), equalTo("value1")); + assertThat(setProcessor.getField().newInstance(Map.of()).execute(), equalTo("field1")); + assertThat(setProcessor.getValue().copyAndResolve(Map.of()), equalTo("value1")); assertThat(setProcessor.isOverrideEnabled(), equalTo(true)); } @@ -53,8 +52,8 @@ public void testCreateWithOverride() throws Exception { String processorTag = randomAlphaOfLength(10); SetProcessor setProcessor = factory.create(null, processorTag, null, config); assertThat(setProcessor.getTag(), equalTo(processorTag)); - assertThat(setProcessor.getField().newInstance(Collections.emptyMap()).execute(), equalTo("field1")); - assertThat(setProcessor.getValue().copyAndResolve(Collections.emptyMap()), equalTo("value1")); + assertThat(setProcessor.getField().newInstance(Map.of()).execute(), equalTo("field1")); + assertThat(setProcessor.getValue().copyAndResolve(Map.of()), equalTo("value1")); assertThat(setProcessor.isOverrideEnabled(), equalTo(overrideEnabled)); } @@ -113,7 +112,7 @@ public void testCreateWithCopyFrom() throws Exception { String processorTag = randomAlphaOfLength(10); SetProcessor setProcessor = factory.create(null, processorTag, null, config); assertThat(setProcessor.getTag(), equalTo(processorTag)); - assertThat(setProcessor.getField().newInstance(Collections.emptyMap()).execute(), equalTo("field1")); + assertThat(setProcessor.getField().newInstance(Map.of()).execute(), equalTo("field1")); assertThat(setProcessor.getCopyFrom(), equalTo("field2")); } @@ -143,7 +142,7 @@ public void testMediaType() throws Exception { // invalid media type expectedMediaType = randomValueOtherThanMany( - m -> Arrays.asList(ConfigurationUtils.VALID_MEDIA_TYPES).contains(m), + m -> List.of(ConfigurationUtils.VALID_MEDIA_TYPES).contains(m), () -> randomAlphaOfLengthBetween(5, 9) ); final Map config2 = new HashMap<>(); diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SetProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SetProcessorTests.java index e6477b8940a8..5973e4fe5741 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SetProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SetProcessorTests.java @@ -15,7 +15,6 @@ import org.elasticsearch.ingest.TestTemplateService; import org.elasticsearch.ingest.ValueSource; import org.elasticsearch.test.ESTestCase; -import org.hamcrest.Matchers; import java.util.ArrayList; import java.util.Date; @@ -109,7 +108,7 @@ public void testSetMetadataExceptVersion() throws Exception { Processor processor = createSetProcessor(randomMetadata.getFieldName(), "_value", null, true, false); IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random()); processor.execute(ingestDocument); - assertThat(ingestDocument.getFieldValue(randomMetadata.getFieldName(), String.class), Matchers.equalTo("_value")); + assertThat(ingestDocument.getFieldValue(randomMetadata.getFieldName(), String.class), equalTo("_value")); } public void testSetMetadataVersion() throws Exception { @@ -117,7 +116,7 @@ public void testSetMetadataVersion() throws Exception { Processor processor = createSetProcessor(Metadata.VERSION.getFieldName(), version, null, true, false); IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random()); processor.execute(ingestDocument); - assertThat(ingestDocument.getFieldValue(Metadata.VERSION.getFieldName(), Long.class), Matchers.equalTo(version)); + assertThat(ingestDocument.getFieldValue(Metadata.VERSION.getFieldName(), Long.class), equalTo(version)); } public void testSetMetadataVersionType() throws Exception { @@ -125,7 +124,7 @@ public void testSetMetadataVersionType() throws Exception { Processor processor = createSetProcessor(Metadata.VERSION_TYPE.getFieldName(), versionType, null, true, false); IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random()); processor.execute(ingestDocument); - assertThat(ingestDocument.getFieldValue(Metadata.VERSION_TYPE.getFieldName(), String.class), Matchers.equalTo(versionType)); + assertThat(ingestDocument.getFieldValue(Metadata.VERSION_TYPE.getFieldName(), String.class), equalTo(versionType)); } public void testSetMetadataIfSeqNo() throws Exception { @@ -133,7 +132,7 @@ public void testSetMetadataIfSeqNo() throws Exception { Processor processor = createSetProcessor(Metadata.IF_SEQ_NO.getFieldName(), ifSeqNo, null, true, false); IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random()); processor.execute(ingestDocument); - assertThat(ingestDocument.getFieldValue(Metadata.IF_SEQ_NO.getFieldName(), Long.class), Matchers.equalTo(ifSeqNo)); + assertThat(ingestDocument.getFieldValue(Metadata.IF_SEQ_NO.getFieldName(), Long.class), equalTo(ifSeqNo)); } public void testSetMetadataIfPrimaryTerm() throws Exception { @@ -141,7 +140,7 @@ public void testSetMetadataIfPrimaryTerm() throws Exception { Processor processor = createSetProcessor(Metadata.IF_PRIMARY_TERM.getFieldName(), ifPrimaryTerm, null, true, false); IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random()); processor.execute(ingestDocument); - assertThat(ingestDocument.getFieldValue(Metadata.IF_PRIMARY_TERM.getFieldName(), Long.class), Matchers.equalTo(ifPrimaryTerm)); + assertThat(ingestDocument.getFieldValue(Metadata.IF_PRIMARY_TERM.getFieldName(), Long.class), equalTo(ifPrimaryTerm)); } public void testSetDynamicTemplates() throws Exception { diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SortProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SortProcessorFactoryTests.java index 51abbf305837..0aeae9e6eed4 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SortProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SortProcessorFactoryTests.java @@ -15,7 +15,7 @@ import java.util.HashMap; import java.util.Map; -import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.equalTo; public class SortProcessorFactoryTests extends ESTestCase { diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SortProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SortProcessorTests.java index 7d5a64216654..f638f8c12315 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SortProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SortProcessorTests.java @@ -15,10 +15,10 @@ import org.elasticsearch.test.ESTestCase; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; @@ -53,7 +53,7 @@ public void testSortIntegersNonRandom() throws Exception { Integer[] expectedResult = new Integer[] { 1, 2, 3, 4, 5, 10, 20, 21, 22, 50, 100 }; List fieldValue = new ArrayList<>(expectedResult.length); - fieldValue.addAll(Arrays.asList(expectedResult).subList(0, expectedResult.length)); + fieldValue.addAll(List.of(expectedResult).subList(0, expectedResult.length)); Collections.shuffle(fieldValue, random()); String fieldName = RandomDocumentPicks.addRandomField(random(), ingestDocument, fieldValue); @@ -265,7 +265,7 @@ public void testSortNullValue() throws Exception { } public void testDescendingSortWithTargetField() throws Exception { - IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Collections.emptyMap()); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Map.of()); int numItems = randomIntBetween(1, 10); List fieldValue = new ArrayList<>(numItems); List expectedResult = new ArrayList<>(numItems); @@ -285,7 +285,7 @@ public void testDescendingSortWithTargetField() throws Exception { } public void testAscendingSortWithTargetField() throws Exception { - IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Collections.emptyMap()); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Map.of()); int numItems = randomIntBetween(1, 10); List fieldValue = new ArrayList<>(numItems); List expectedResult = new ArrayList<>(numItems); @@ -305,8 +305,8 @@ public void testAscendingSortWithTargetField() throws Exception { } public void testSortWithTargetFieldLeavesOriginalUntouched() throws Exception { - IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Collections.emptyMap()); - List fieldValue = Arrays.asList(1, 5, 4); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Map.of()); + List fieldValue = List.of(1, 5, 4); List expectedResult = new ArrayList<>(fieldValue); Collections.sort(expectedResult); diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SplitProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SplitProcessorFactoryTests.java index 891f67bd45a6..10d96b8add96 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SplitProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SplitProcessorFactoryTests.java @@ -14,7 +14,7 @@ import java.util.HashMap; import java.util.Map; -import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.equalTo; public class SplitProcessorFactoryTests extends ESTestCase { diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SplitProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SplitProcessorTests.java index 73c94efdb985..500debd79b96 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SplitProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/SplitProcessorTests.java @@ -14,7 +14,6 @@ import org.elasticsearch.ingest.TestIngestDocument; import org.elasticsearch.test.ESTestCase; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -31,7 +30,7 @@ public void testSplit() throws Exception { String fieldName = RandomDocumentPicks.addRandomField(random(), ingestDocument, "127.0.0.1"); Processor processor = new SplitProcessor(randomAlphaOfLength(10), null, fieldName, "\\.", false, false, fieldName); processor.execute(ingestDocument); - assertThat(ingestDocument.getFieldValue(fieldName, List.class), equalTo(Arrays.asList("127", "0", "0", "1"))); + assertThat(ingestDocument.getFieldValue(fieldName, List.class), equalTo(List.of("127", "0", "0", "1"))); } public void testSplitFieldNotFound() throws Exception { @@ -70,7 +69,7 @@ public void testSplitNullValueWithIgnoreMissing() throws Exception { } public void testSplitNonExistentWithIgnoreMissing() throws Exception { - IngestDocument originalIngestDocument = RandomDocumentPicks.randomIngestDocument(random(), Collections.emptyMap()); + IngestDocument originalIngestDocument = RandomDocumentPicks.randomIngestDocument(random(), Map.of()); IngestDocument ingestDocument = new IngestDocument(originalIngestDocument); Processor processor = new SplitProcessor(randomAlphaOfLength(10), null, "field", "\\.", true, false, "field"); processor.execute(ingestDocument); @@ -104,11 +103,11 @@ public void testSplitAppendable() throws Exception { splitProcessor.execute(ingestDocument); @SuppressWarnings("unchecked") List flags = (List) ingestDocument.getFieldValue("flags", List.class); - assertThat(flags, equalTo(Arrays.asList("new", "hot", "super", "fun", "interesting"))); + assertThat(flags, equalTo(List.of("new", "hot", "super", "fun", "interesting"))); ingestDocument.appendFieldValue("flags", "additional_flag"); assertThat( ingestDocument.getFieldValue("flags", List.class), - equalTo(Arrays.asList("new", "hot", "super", "fun", "interesting", "additional_flag")) + equalTo(List.of("new", "hot", "super", "fun", "interesting", "additional_flag")) ); } @@ -118,15 +117,15 @@ public void testSplitWithTargetField() throws Exception { String targetFieldName = fieldName + randomAlphaOfLength(5); Processor processor = new SplitProcessor(randomAlphaOfLength(10), null, fieldName, "\\.", false, false, targetFieldName); processor.execute(ingestDocument); - assertThat(ingestDocument.getFieldValue(targetFieldName, List.class), equalTo(Arrays.asList("127", "0", "0", "1"))); + assertThat(ingestDocument.getFieldValue(targetFieldName, List.class), equalTo(List.of("127", "0", "0", "1"))); } public void testSplitWithPreserveTrailing() throws Exception { - doTestSplitWithPreserveTrailing(true, "foo|bar|baz||", Arrays.asList("foo", "bar", "baz", "", "")); + doTestSplitWithPreserveTrailing(true, "foo|bar|baz||", List.of("foo", "bar", "baz", "", "")); } public void testSplitWithoutPreserveTrailing() throws Exception { - doTestSplitWithPreserveTrailing(false, "foo|bar|baz||", Arrays.asList("foo", "bar", "baz")); + doTestSplitWithPreserveTrailing(false, "foo|bar|baz||", List.of("foo", "bar", "baz")); } private void doTestSplitWithPreserveTrailing(boolean preserveTrailing, String fieldValue, List expected) throws Exception { diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/UriPartsProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/UriPartsProcessorFactoryTests.java index 08e99326330a..a6ba0a32b7bb 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/UriPartsProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/UriPartsProcessorFactoryTests.java @@ -15,7 +15,7 @@ import java.util.HashMap; import java.util.Map; -import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.equalTo; public class UriPartsProcessorFactoryTests extends ESTestCase { diff --git a/server/src/test/java/org/elasticsearch/ingest/CompoundProcessorTests.java b/server/src/test/java/org/elasticsearch/ingest/CompoundProcessorTests.java index 8c6c44bd17f5..397573729ea0 100644 --- a/server/src/test/java/org/elasticsearch/ingest/CompoundProcessorTests.java +++ b/server/src/test/java/org/elasticsearch/ingest/CompoundProcessorTests.java @@ -19,7 +19,7 @@ import java.util.function.Consumer; import java.util.function.LongSupplier; -import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; diff --git a/server/src/test/java/org/elasticsearch/ingest/ConditionalProcessorTests.java b/server/src/test/java/org/elasticsearch/ingest/ConditionalProcessorTests.java index e505dfc2ce64..b61d760ec6c0 100644 --- a/server/src/test/java/org/elasticsearch/ingest/ConditionalProcessorTests.java +++ b/server/src/test/java/org/elasticsearch/ingest/ConditionalProcessorTests.java @@ -22,7 +22,6 @@ import java.text.ParseException; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -37,8 +36,8 @@ import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; -import static org.hamcrest.core.Is.is; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -52,12 +51,12 @@ public void testChecksCondition() throws Exception { String trueValue = "truthy"; ScriptService scriptService = new ScriptService( Settings.builder().build(), - Collections.singletonMap( + Map.of( Script.DEFAULT_SCRIPT_LANG, new MockScriptEngine( Script.DEFAULT_SCRIPT_LANG, - Collections.singletonMap(scriptName, ctx -> trueValue.equals(ctx.get(conditionalField))), - Collections.emptyMap() + Map.of(scriptName, ctx -> trueValue.equals(ctx.get(conditionalField))), + Map.of() ) ), new HashMap<>(ScriptModule.CORE_CONTEXTS), @@ -69,7 +68,7 @@ public void testChecksCondition() throws Exception { ConditionalProcessor processor = new ConditionalProcessor( randomAlphaOfLength(10), "description", - new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, scriptName, Collections.emptyMap()), + new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, scriptName, Map.of()), scriptService, new Processor() { @Override @@ -150,13 +149,10 @@ public void testTypeDeprecation() throws Exception { ScriptService scriptService = new ScriptService( Settings.builder().build(), - Collections.singletonMap( - Script.DEFAULT_SCRIPT_LANG, - new MockScriptEngine(Script.DEFAULT_SCRIPT_LANG, Collections.singletonMap(scriptName, ctx -> { - ctx.get("_type"); - return true; - }), Collections.emptyMap()) - ), + Map.of(Script.DEFAULT_SCRIPT_LANG, new MockScriptEngine(Script.DEFAULT_SCRIPT_LANG, Map.of(scriptName, ctx -> { + ctx.get("_type"); + return true; + }), Map.of())), new HashMap<>(ScriptModule.CORE_CONTEXTS), () -> 1L ); @@ -166,7 +162,7 @@ public void testTypeDeprecation() throws Exception { ConditionalProcessor processor = new ConditionalProcessor( randomAlphaOfLength(10), "description", - new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, scriptName, Collections.emptyMap()), + new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, scriptName, Map.of()), scriptService, new Processor() { @Override @@ -192,7 +188,7 @@ public String getDescription() { relativeTimeProvider ); - IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Collections.emptyMap()); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), Map.of()); execProcessor(processor, ingestDocument, (result, e) -> {}); assertWarnings("[types removal] Looking up doc types [_type] in scripts is deprecated."); } @@ -255,17 +251,14 @@ private static void assertMutatingCtxThrows(Consumer> mutati CompletableFuture expectedException = new CompletableFuture<>(); ScriptService scriptService = new ScriptService( Settings.builder().build(), - Collections.singletonMap( - Script.DEFAULT_SCRIPT_LANG, - new MockScriptEngine(Script.DEFAULT_SCRIPT_LANG, Collections.singletonMap(scriptName, ctx -> { - try { - mutation.accept(ctx); - } catch (Exception e) { - expectedException.complete(e); - } - return false; - }), Collections.emptyMap()) - ), + Map.of(Script.DEFAULT_SCRIPT_LANG, new MockScriptEngine(Script.DEFAULT_SCRIPT_LANG, Map.of(scriptName, ctx -> { + try { + mutation.accept(ctx); + } catch (Exception e) { + expectedException.complete(e); + } + return false; + }), Map.of())), new HashMap<>(ScriptModule.CORE_CONTEXTS), () -> 1L ); @@ -273,7 +266,7 @@ private static void assertMutatingCtxThrows(Consumer> mutati ConditionalProcessor processor = new ConditionalProcessor( randomAlphaOfLength(10), "desription", - new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, scriptName, Collections.emptyMap()), + new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, scriptName, Map.of()), scriptService, new FakeProcessor(null, null, null, null) ); diff --git a/server/src/test/java/org/elasticsearch/ingest/ConfigurationUtilsTests.java b/server/src/test/java/org/elasticsearch/ingest/ConfigurationUtilsTests.java index ace57f6a2ebc..5e9e03bbb626 100644 --- a/server/src/test/java/org/elasticsearch/ingest/ConfigurationUtilsTests.java +++ b/server/src/test/java/org/elasticsearch/ingest/ConfigurationUtilsTests.java @@ -28,8 +28,8 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; diff --git a/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java b/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java index 8e61698c0f4b..d24c89ae768b 100644 --- a/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java +++ b/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java @@ -70,13 +70,13 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -87,8 +87,6 @@ import java.util.function.LongSupplier; import java.util.stream.Collectors; -import static java.util.Collections.emptyMap; -import static java.util.Collections.emptySet; import static org.elasticsearch.cluster.service.ClusterStateTaskExecutorUtils.executeAndAssertSuccessful; import static org.elasticsearch.core.Tuple.tuple; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -101,13 +99,13 @@ import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.argThat; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -119,7 +117,7 @@ public class IngestServiceTests extends ESTestCase { private static final IngestPlugin DUMMY_PLUGIN = new IngestPlugin() { @Override public Map getProcessors(Processor.Parameters parameters) { - return Collections.singletonMap("foo", (factories, tag, description, config) -> null); + return Map.of("foo", (factories, tag, description, config) -> null); } }; @@ -140,7 +138,7 @@ public void testIngestPlugin() { null, null, null, - Collections.singletonList(DUMMY_PLUGIN), + List.of(DUMMY_PLUGIN), client ); Map factories = ingestService.getProcessorFactories(); @@ -152,15 +150,7 @@ public void testIngestPluginDuplicate() { Client client = mock(Client.class); IllegalArgumentException e = expectThrows( IllegalArgumentException.class, - () -> new IngestService( - mock(ClusterService.class), - threadPool, - null, - null, - null, - Arrays.asList(DUMMY_PLUGIN, DUMMY_PLUGIN), - client - ) + () -> new IngestService(mock(ClusterService.class), threadPool, null, null, null, List.of(DUMMY_PLUGIN, DUMMY_PLUGIN), client) ); assertTrue(e.getMessage(), e.getMessage().contains("already registered")); } @@ -173,11 +163,11 @@ public void testExecuteIndexPipelineDoesNotExist() { null, null, null, - Collections.singletonList(DUMMY_PLUGIN), + List.of(DUMMY_PLUGIN), client ); final IndexRequest indexRequest = new IndexRequest("_index").id("_id") - .source(emptyMap()) + .source(Map.of()) .setPipeline("_id") .setFinalPipeline("_none"); @@ -192,14 +182,7 @@ public void testExecuteIndexPipelineDoesNotExist() { @SuppressWarnings("unchecked") final BiConsumer completionHandler = mock(BiConsumer.class); - ingestService.executeBulkRequest( - 1, - Collections.singletonList(indexRequest), - indexReq -> {}, - failureHandler, - completionHandler, - Names.WRITE - ); + ingestService.executeBulkRequest(1, List.of(indexRequest), indexReq -> {}, failureHandler, completionHandler, Names.WRITE); assertTrue(failure.get()); verify(completionHandler, times(1)).accept(Thread.currentThread(), null); @@ -214,7 +197,7 @@ public void testUpdatePipelines() { PipelineConfiguration pipeline = new PipelineConfiguration("_id", new BytesArray(""" {"processors": [{"set" : {"field": "_field", "value": "_value"}}]}"""), XContentType.JSON); - IngestMetadata ingestMetadata = new IngestMetadata(Collections.singletonMap("_id", pipeline)); + IngestMetadata ingestMetadata = new IngestMetadata(Map.of("_id", pipeline)); clusterState = ClusterState.builder(clusterState) .metadata(Metadata.builder().putCustom(IngestMetadata.TYPE, ingestMetadata)) .build(); @@ -291,7 +274,7 @@ public void testDelete() { IngestService ingestService = createWithProcessors(); PipelineConfiguration config = new PipelineConfiguration("_id", new BytesArray(""" {"processors": [{"set" : {"field": "_field", "value": "_value"}}]}"""), XContentType.JSON); - IngestMetadata ingestMetadata = new IngestMetadata(Collections.singletonMap("_id", config)); + IngestMetadata ingestMetadata = new IngestMetadata(Map.of("_id", config)); ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); ClusterState previousClusterState = clusterState; clusterState = ClusterState.builder(clusterState) @@ -323,19 +306,13 @@ public void testValidateNoIngestInfo() throws Exception { var pipelineConfig = XContentHelper.convertToMap(putRequest.getSource(), false, putRequest.getXContentType()).v2(); Exception e = expectThrows( IllegalStateException.class, - () -> ingestService.validatePipeline(emptyMap(), putRequest.getId(), pipelineConfig) + () -> ingestService.validatePipeline(Map.of(), putRequest.getId(), pipelineConfig) ); assertEquals("Ingest info is empty", e.getMessage()); - DiscoveryNode discoveryNode = new DiscoveryNode( - "_node_id", - buildNewFakeTransportAddress(), - emptyMap(), - emptySet(), - Version.CURRENT - ); - IngestInfo ingestInfo = new IngestInfo(Collections.singletonList(new ProcessorInfo("set"))); - ingestService.validatePipeline(Collections.singletonMap(discoveryNode, ingestInfo), putRequest.getId(), pipelineConfig); + DiscoveryNode discoveryNode = new DiscoveryNode("_node_id", buildNewFakeTransportAddress(), Map.of(), Set.of(), Version.CURRENT); + IngestInfo ingestInfo = new IngestInfo(List.of(new ProcessorInfo("set"))); + ingestService.validatePipeline(Map.of(discoveryNode, ingestInfo), putRequest.getId(), pipelineConfig); } public void testValidateNotInUse() { @@ -543,13 +520,10 @@ public void testGetProcessorsInPipelineComplexConditional() throws Exception { String scriptName = "conditionalScript"; ScriptService scriptService = new ScriptService( Settings.builder().build(), - Collections.singletonMap( - Script.DEFAULT_SCRIPT_LANG, - new MockScriptEngine(Script.DEFAULT_SCRIPT_LANG, Collections.singletonMap(scriptName, ctx -> { - ctx.get("_type"); - return true; - }), Collections.emptyMap()) - ), + Map.of(Script.DEFAULT_SCRIPT_LANG, new MockScriptEngine(Script.DEFAULT_SCRIPT_LANG, Map.of(scriptName, ctx -> { + ctx.get("_type"); + return true; + }), Map.of())), new HashMap<>(ScriptModule.CORE_CONTEXTS), () -> 1L ); @@ -562,12 +536,12 @@ public void testGetProcessorsInPipelineComplexConditional() throws Exception { return new ConditionalProcessor( randomAlphaOfLength(10), null, - new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, scriptName, Collections.emptyMap()), + new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, scriptName, Map.of()), scriptService, new ConditionalProcessor( randomAlphaOfLength(10) + "-nested", null, - new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, scriptName, Collections.emptyMap()), + new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, scriptName, Map.of()), scriptService, new FakeProcessor("complexSet", tag, description, (ingestDocument) -> ingestDocument.setFieldValue(field, value)) ) @@ -777,7 +751,7 @@ public void testDeleteWithIndexUsePipeline() { IngestService ingestService = createWithProcessors(); PipelineConfiguration config = new PipelineConfiguration("_id", new BytesArray(""" {"processors": [{"set" : {"field": "_field", "value": "_value"}}]}"""), XContentType.JSON); - IngestMetadata ingestMetadata = new IngestMetadata(Collections.singletonMap("_id", config)); + IngestMetadata ingestMetadata = new IngestMetadata(Map.of("_id", config)); Metadata.Builder builder = Metadata.builder(); for (int i = 0; i < randomIntBetween(2, 10); i++) { builder.put( @@ -896,11 +870,11 @@ public void testValidate() throws Exception { }"""), XContentType.JSON); var pipelineConfig = XContentHelper.convertToMap(putRequest.getSource(), false, putRequest.getXContentType()).v2(); - DiscoveryNode node1 = new DiscoveryNode("_node_id1", buildNewFakeTransportAddress(), emptyMap(), emptySet(), Version.CURRENT); - DiscoveryNode node2 = new DiscoveryNode("_node_id2", buildNewFakeTransportAddress(), emptyMap(), emptySet(), Version.CURRENT); + DiscoveryNode node1 = new DiscoveryNode("_node_id1", buildNewFakeTransportAddress(), Map.of(), Set.of(), Version.CURRENT); + DiscoveryNode node2 = new DiscoveryNode("_node_id2", buildNewFakeTransportAddress(), Map.of(), Set.of(), Version.CURRENT); Map ingestInfos = new HashMap<>(); - ingestInfos.put(node1, new IngestInfo(Arrays.asList(new ProcessorInfo("set"), new ProcessorInfo("remove")))); - ingestInfos.put(node2, new IngestInfo(Arrays.asList(new ProcessorInfo("set")))); + ingestInfos.put(node1, new IngestInfo(List.of(new ProcessorInfo("set"), new ProcessorInfo("remove")))); + ingestInfos.put(node2, new IngestInfo(List.of(new ProcessorInfo("set")))); ElasticsearchParseException e = expectThrows( ElasticsearchParseException.class, @@ -911,13 +885,13 @@ public void testValidate() throws Exception { assertEquals("tag2", e.getMetadata("es.processor_tag").get(0)); var pipelineConfig2 = XContentHelper.convertToMap(putRequest.getSource(), false, putRequest.getXContentType()).v2(); - ingestInfos.put(node2, new IngestInfo(Arrays.asList(new ProcessorInfo("set"), new ProcessorInfo("remove")))); + ingestInfos.put(node2, new IngestInfo(List.of(new ProcessorInfo("set"), new ProcessorInfo("remove")))); ingestService.validatePipeline(ingestInfos, putRequest.getId(), pipelineConfig2); } public void testExecuteIndexPipelineExistsButFailedParsing() { IngestService ingestService = createWithProcessors( - Collections.singletonMap("mock", (factories, tag, description, config) -> new AbstractProcessor("mock", "description") { + Map.of("mock", (factories, tag, description, config) -> new AbstractProcessor("mock", "description") { @Override public IngestDocument execute(IngestDocument ingestDocument) { throw new IllegalStateException("error"); @@ -943,11 +917,11 @@ public String getType() { BulkRequest bulkRequest = new BulkRequest(); final IndexRequest indexRequest1 = new IndexRequest("_index").id("_id1") - .source(emptyMap()) + .source(Map.of()) .setPipeline("_none") .setFinalPipeline("_none"); bulkRequest.add(indexRequest1); - IndexRequest indexRequest2 = new IndexRequest("_index").id("_id2").source(emptyMap()).setPipeline(id).setFinalPipeline("_none"); + IndexRequest indexRequest2 = new IndexRequest("_index").id("_id2").source(Map.of()).setPipeline(id).setFinalPipeline("_none"); bulkRequest.add(indexRequest2); final BiConsumer failureHandler = (slot, e) -> { @@ -975,7 +949,7 @@ public String getType() { public void testExecuteBulkPipelineDoesNotExist() { IngestService ingestService = createWithProcessors( - Collections.singletonMap("mock", (factories, tag, description, config) -> mockCompoundProcessor()) + Map.of("mock", (factories, tag, description, config) -> mockCompoundProcessor()) ); PutPipelineRequest putRequest = new PutPipelineRequest( @@ -990,15 +964,12 @@ public void testExecuteBulkPipelineDoesNotExist() { BulkRequest bulkRequest = new BulkRequest(); - IndexRequest indexRequest1 = new IndexRequest("_index").id("_id1") - .source(emptyMap()) - .setPipeline("_none") - .setFinalPipeline("_none"); + IndexRequest indexRequest1 = new IndexRequest("_index").id("_id1").source(Map.of()).setPipeline("_none").setFinalPipeline("_none"); bulkRequest.add(indexRequest1); - IndexRequest indexRequest2 = new IndexRequest("_index").id("_id2").source(emptyMap()).setPipeline("_id").setFinalPipeline("_none"); + IndexRequest indexRequest2 = new IndexRequest("_index").id("_id2").source(Map.of()).setPipeline("_id").setFinalPipeline("_none"); bulkRequest.add(indexRequest2); IndexRequest indexRequest3 = new IndexRequest("_index").id("_id3") - .source(Collections.emptyMap()) + .source(Map.of()) .setPipeline("does_not_exist") .setFinalPipeline("_none"); bulkRequest.add(indexRequest3); @@ -1023,7 +994,7 @@ public void testExecuteBulkPipelineDoesNotExist() { public void testExecuteSuccess() { IngestService ingestService = createWithProcessors( - Collections.singletonMap("mock", (factories, tag, description, config) -> mockCompoundProcessor()) + Map.of("mock", (factories, tag, description, config) -> mockCompoundProcessor()) ); PutPipelineRequest putRequest = new PutPipelineRequest( "_id", @@ -1035,28 +1006,21 @@ public void testExecuteSuccess() { clusterState = executePut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); final IndexRequest indexRequest = new IndexRequest("_index").id("_id") - .source(emptyMap()) + .source(Map.of()) .setPipeline("_id") .setFinalPipeline("_none"); @SuppressWarnings("unchecked") final BiConsumer failureHandler = mock(BiConsumer.class); @SuppressWarnings("unchecked") final BiConsumer completionHandler = mock(BiConsumer.class); - ingestService.executeBulkRequest( - 1, - Collections.singletonList(indexRequest), - indexReq -> {}, - failureHandler, - completionHandler, - Names.WRITE - ); + ingestService.executeBulkRequest(1, List.of(indexRequest), indexReq -> {}, failureHandler, completionHandler, Names.WRITE); verify(failureHandler, never()).accept(any(), any()); verify(completionHandler, times(1)).accept(Thread.currentThread(), null); } public void testDynamicTemplates() throws Exception { IngestService ingestService = createWithProcessors( - Collections.singletonMap( + Map.of( "set", (factories, tag, description, config) -> new FakeProcessor( "set", @@ -1076,26 +1040,19 @@ public void testDynamicTemplates() throws Exception { clusterState = executePut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); final IndexRequest indexRequest = new IndexRequest("_index").id("_id") - .source(emptyMap()) + .source(Map.of()) .setPipeline("_id") .setFinalPipeline("_none"); CountDownLatch latch = new CountDownLatch(1); final BiConsumer failureHandler = (v, e) -> { throw new AssertionError("must never fail", e); }; final BiConsumer completionHandler = (t, e) -> latch.countDown(); - ingestService.executeBulkRequest( - 1, - Collections.singletonList(indexRequest), - indexReq -> {}, - failureHandler, - completionHandler, - Names.WRITE - ); + ingestService.executeBulkRequest(1, List.of(indexRequest), indexReq -> {}, failureHandler, completionHandler, Names.WRITE); latch.await(); assertThat(indexRequest.getDynamicTemplates(), equalTo(Map.of("foo", "bar", "foo.bar", "baz"))); } public void testExecuteEmptyPipeline() throws Exception { - IngestService ingestService = createWithProcessors(emptyMap()); + IngestService ingestService = createWithProcessors(Map.of()); PutPipelineRequest putRequest = new PutPipelineRequest("_id", new BytesArray(""" {"processors": [], "description": "_description"}"""), XContentType.JSON); ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty @@ -1103,30 +1060,21 @@ public void testExecuteEmptyPipeline() throws Exception { clusterState = executePut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); final IndexRequest indexRequest = new IndexRequest("_index").id("_id") - .source(emptyMap()) + .source(Map.of()) .setPipeline("_id") .setFinalPipeline("_none"); @SuppressWarnings("unchecked") final BiConsumer failureHandler = mock(BiConsumer.class); @SuppressWarnings("unchecked") final BiConsumer completionHandler = mock(BiConsumer.class); - ingestService.executeBulkRequest( - 1, - Collections.singletonList(indexRequest), - indexReq -> {}, - failureHandler, - completionHandler, - Names.WRITE - ); + ingestService.executeBulkRequest(1, List.of(indexRequest), indexReq -> {}, failureHandler, completionHandler, Names.WRITE); verify(failureHandler, never()).accept(any(), any()); verify(completionHandler, times(1)).accept(Thread.currentThread(), null); } public void testExecutePropagateAllMetadataUpdates() throws Exception { final CompoundProcessor processor = mockCompoundProcessor(); - IngestService ingestService = createWithProcessors( - Collections.singletonMap("mock", (factories, tag, description, config) -> processor) - ); + IngestService ingestService = createWithProcessors(Map.of("mock", (factories, tag, description, config) -> processor)); PutPipelineRequest putRequest = new PutPipelineRequest( "_id", new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), @@ -1166,21 +1114,14 @@ public void testExecutePropagateAllMetadataUpdates() throws Exception { return null; }).when(processor).execute(any(), any()); final IndexRequest indexRequest = new IndexRequest("_index").id("_id") - .source(emptyMap()) + .source(Map.of()) .setPipeline("_id") .setFinalPipeline("_none"); @SuppressWarnings("unchecked") final BiConsumer failureHandler = mock(BiConsumer.class); @SuppressWarnings("unchecked") final BiConsumer completionHandler = mock(BiConsumer.class); - ingestService.executeBulkRequest( - 1, - Collections.singletonList(indexRequest), - indexReq -> {}, - failureHandler, - completionHandler, - Names.WRITE - ); + ingestService.executeBulkRequest(1, List.of(indexRequest), indexReq -> {}, failureHandler, completionHandler, Names.WRITE); verify(processor).execute(any(), any()); verify(failureHandler, never()).accept(any(), any()); verify(completionHandler, times(1)).accept(Thread.currentThread(), null); @@ -1195,9 +1136,7 @@ public void testExecutePropagateAllMetadataUpdates() throws Exception { public void testExecuteFailure() throws Exception { final CompoundProcessor processor = mockCompoundProcessor(); - IngestService ingestService = createWithProcessors( - Collections.singletonMap("mock", (factories, tag, description, config) -> processor) - ); + IngestService ingestService = createWithProcessors(Map.of("mock", (factories, tag, description, config) -> processor)); PutPipelineRequest putRequest = new PutPipelineRequest( "_id", new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), @@ -1208,24 +1147,17 @@ public void testExecuteFailure() throws Exception { clusterState = executePut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); final IndexRequest indexRequest = new IndexRequest("_index").id("_id") - .source(emptyMap()) + .source(Map.of()) .setPipeline("_id") .setFinalPipeline("_none"); doThrow(new RuntimeException()).when(processor) - .execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), emptyMap()), any()); + .execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), Map.of()), any()); @SuppressWarnings("unchecked") final BiConsumer failureHandler = mock(BiConsumer.class); @SuppressWarnings("unchecked") final BiConsumer completionHandler = mock(BiConsumer.class); - ingestService.executeBulkRequest( - 1, - Collections.singletonList(indexRequest), - indexReq -> {}, - failureHandler, - completionHandler, - Names.WRITE - ); - verify(processor).execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), emptyMap()), any()); + ingestService.executeBulkRequest(1, List.of(indexRequest), indexReq -> {}, failureHandler, completionHandler, Names.WRITE); + verify(processor).execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), Map.of()), any()); verify(failureHandler, times(1)).accept(eq(0), any(RuntimeException.class)); verify(completionHandler, times(1)).accept(Thread.currentThread(), null); } @@ -1239,7 +1171,7 @@ public void testExecuteSuccessWithOnFailure() throws Exception { BiConsumer handler = (BiConsumer) args.getArguments()[1]; handler.accept(null, new RuntimeException()); return null; - }).when(processor).execute(eqIndexTypeId(emptyMap()), any()); + }).when(processor).execute(eqIndexTypeId(Map.of()), any()); final Processor onFailureProcessor = mock(Processor.class); doAnswer(args -> { @@ -1248,16 +1180,14 @@ public void testExecuteSuccessWithOnFailure() throws Exception { BiConsumer handler = (BiConsumer) args.getArguments()[1]; handler.accept(ingestDocument, null); return null; - }).when(onFailureProcessor).execute(eqIndexTypeId(emptyMap()), any()); + }).when(onFailureProcessor).execute(eqIndexTypeId(Map.of()), any()); final CompoundProcessor compoundProcessor = new CompoundProcessor( false, - Collections.singletonList(processor), - Collections.singletonList(new CompoundProcessor(onFailureProcessor)) - ); - IngestService ingestService = createWithProcessors( - Collections.singletonMap("mock", (factories, tag, description, config) -> compoundProcessor) + List.of(processor), + List.of(new CompoundProcessor(onFailureProcessor)) ); + IngestService ingestService = createWithProcessors(Map.of("mock", (factories, tag, description, config) -> compoundProcessor)); PutPipelineRequest putRequest = new PutPipelineRequest( "_id", new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), @@ -1268,21 +1198,14 @@ public void testExecuteSuccessWithOnFailure() throws Exception { clusterState = executePut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); final IndexRequest indexRequest = new IndexRequest("_index").id("_id") - .source(emptyMap()) + .source(Map.of()) .setPipeline("_id") .setFinalPipeline("_none"); @SuppressWarnings("unchecked") final BiConsumer failureHandler = mock(BiConsumer.class); @SuppressWarnings("unchecked") final BiConsumer completionHandler = mock(BiConsumer.class); - ingestService.executeBulkRequest( - 1, - Collections.singletonList(indexRequest), - indexReq -> {}, - failureHandler, - completionHandler, - Names.WRITE - ); + ingestService.executeBulkRequest(1, List.of(indexRequest), indexReq -> {}, failureHandler, completionHandler, Names.WRITE); verify(failureHandler, never()).accept(eq(0), any(IngestProcessorException.class)); verify(completionHandler, times(1)).accept(Thread.currentThread(), null); } @@ -1294,16 +1217,14 @@ public void testExecuteFailureWithNestedOnFailure() throws Exception { when(onFailureProcessor.isAsync()).thenReturn(true); final Processor onFailureOnFailureProcessor = mock(Processor.class); when(onFailureOnFailureProcessor.isAsync()).thenReturn(true); - final List processors = Collections.singletonList(onFailureProcessor); - final List onFailureProcessors = Collections.singletonList(onFailureOnFailureProcessor); + final List processors = List.of(onFailureProcessor); + final List onFailureProcessors = List.of(onFailureOnFailureProcessor); final CompoundProcessor compoundProcessor = new CompoundProcessor( false, - Collections.singletonList(processor), - Collections.singletonList(new CompoundProcessor(false, processors, onFailureProcessors)) - ); - IngestService ingestService = createWithProcessors( - Collections.singletonMap("mock", (factories, tag, description, config) -> compoundProcessor) + List.of(processor), + List.of(new CompoundProcessor(false, processors, onFailureProcessors)) ); + IngestService ingestService = createWithProcessors(Map.of("mock", (factories, tag, description, config) -> compoundProcessor)); PutPipelineRequest putRequest = new PutPipelineRequest( "_id", new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), @@ -1314,28 +1235,21 @@ public void testExecuteFailureWithNestedOnFailure() throws Exception { clusterState = executePut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); final IndexRequest indexRequest = new IndexRequest("_index").id("_id") - .source(emptyMap()) + .source(Map.of()) .setPipeline("_id") .setFinalPipeline("_none"); doThrow(new RuntimeException()).when(onFailureOnFailureProcessor) - .execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), emptyMap()), any()); + .execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), Map.of()), any()); doThrow(new RuntimeException()).when(onFailureProcessor) - .execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), emptyMap()), any()); + .execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), Map.of()), any()); doThrow(new RuntimeException()).when(processor) - .execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), emptyMap()), any()); + .execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), Map.of()), any()); @SuppressWarnings("unchecked") final BiConsumer failureHandler = mock(BiConsumer.class); @SuppressWarnings("unchecked") final BiConsumer completionHandler = mock(BiConsumer.class); - ingestService.executeBulkRequest( - 1, - Collections.singletonList(indexRequest), - indexReq -> {}, - failureHandler, - completionHandler, - Names.WRITE - ); - verify(processor).execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), emptyMap()), any()); + ingestService.executeBulkRequest(1, List.of(indexRequest), indexReq -> {}, failureHandler, completionHandler, Names.WRITE); + verify(processor).execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), Map.of()), any()); verify(failureHandler, times(1)).accept(eq(0), any(RuntimeException.class)); verify(completionHandler, times(1)).accept(Thread.currentThread(), null); } @@ -1365,7 +1279,7 @@ public void testBulkRequestExecutionWithFailures() throws Exception { CompoundProcessor processor = mock(CompoundProcessor.class); when(processor.isAsync()).thenReturn(true); - when(processor.getProcessors()).thenReturn(Collections.singletonList(mock(Processor.class))); + when(processor.getProcessors()).thenReturn(List.of(mock(Processor.class))); Exception error = new RuntimeException(); doAnswer(args -> { @SuppressWarnings("unchecked") @@ -1373,9 +1287,7 @@ public void testBulkRequestExecutionWithFailures() throws Exception { handler.accept(null, error); return null; }).when(processor).execute(any(), any()); - IngestService ingestService = createWithProcessors( - Collections.singletonMap("mock", (factories, tag, description, config) -> processor) - ); + IngestService ingestService = createWithProcessors(Map.of("mock", (factories, tag, description, config) -> processor)); PutPipelineRequest putRequest = new PutPipelineRequest( "_id", new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), @@ -1514,14 +1426,7 @@ public void testStats() throws Exception { final IndexRequest indexRequest = new IndexRequest("_index"); indexRequest.setPipeline("_id1").setFinalPipeline("_none"); indexRequest.source(randomAlphaOfLength(10), randomAlphaOfLength(10)); - ingestService.executeBulkRequest( - 1, - Collections.singletonList(indexRequest), - indexReq -> {}, - failureHandler, - completionHandler, - Names.WRITE - ); + ingestService.executeBulkRequest(1, List.of(indexRequest), indexReq -> {}, failureHandler, completionHandler, Names.WRITE); final IngestStats afterFirstRequestStats = ingestService.stats(); assertThat(afterFirstRequestStats.getPipelineStats().size(), equalTo(2)); @@ -1538,14 +1443,7 @@ public void testStats() throws Exception { assertProcessorStats(0, afterFirstRequestStats, "_id2", 0, 0, 0); indexRequest.setPipeline("_id2"); - ingestService.executeBulkRequest( - 1, - Collections.singletonList(indexRequest), - indexReq -> {}, - failureHandler, - completionHandler, - Names.WRITE - ); + ingestService.executeBulkRequest(1, List.of(indexRequest), indexReq -> {}, failureHandler, completionHandler, Names.WRITE); final IngestStats afterSecondRequestStats = ingestService.stats(); assertThat(afterSecondRequestStats.getPipelineStats().size(), equalTo(2)); // total @@ -1567,14 +1465,7 @@ public void testStats() throws Exception { clusterState = executePut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); indexRequest.setPipeline("_id1"); - ingestService.executeBulkRequest( - 1, - Collections.singletonList(indexRequest), - indexReq -> {}, - failureHandler, - completionHandler, - Names.WRITE - ); + ingestService.executeBulkRequest(1, List.of(indexRequest), indexReq -> {}, failureHandler, completionHandler, Names.WRITE); final IngestStats afterThirdRequestStats = ingestService.stats(); assertThat(afterThirdRequestStats.getPipelineStats().size(), equalTo(2)); // total @@ -1597,14 +1488,7 @@ public void testStats() throws Exception { clusterState = executePut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); indexRequest.setPipeline("_id1"); - ingestService.executeBulkRequest( - 1, - Collections.singletonList(indexRequest), - indexReq -> {}, - failureHandler, - completionHandler, - Names.WRITE - ); + ingestService.executeBulkRequest(1, List.of(indexRequest), indexReq -> {}, failureHandler, completionHandler, Names.WRITE); final IngestStats afterForthRequestStats = ingestService.stats(); assertThat(afterForthRequestStats.getPipelineStats().size(), equalTo(2)); // total @@ -1678,15 +1562,12 @@ public String getDescription() { BulkRequest bulkRequest = new BulkRequest(); final IndexRequest indexRequest1 = new IndexRequest("_index").id("_id1") - .source(Collections.emptyMap()) + .source(Map.of()) .setPipeline("_none") .setFinalPipeline("_none"); bulkRequest.add(indexRequest1); - IndexRequest indexRequest2 = new IndexRequest("_index").id("_id2") - .source(Collections.emptyMap()) - .setPipeline("_id") - .setFinalPipeline("_none"); + IndexRequest indexRequest2 = new IndexRequest("_index").id("_id2").source(Map.of()).setPipeline("_id").setFinalPipeline("_none"); bulkRequest.add(indexRequest2); @SuppressWarnings("unchecked") @@ -1718,7 +1599,7 @@ public void testIngestClusterStateListeners_orderOfExecution() { IngestPlugin testPlugin = new IngestPlugin() { @Override public Map getProcessors(Processor.Parameters parameters) { - return Collections.singletonMap("test", (factories, tag, description, config) -> { + return Map.of("test", (factories, tag, description, config) -> { assertThat(counter.compareAndSet(1, 2), is(true)); return new FakeProcessor("test", tag, description, ingestDocument -> {}); }); @@ -1757,7 +1638,7 @@ public void testCBORParsing() throws Exception { AtomicReference reference = new AtomicReference<>(); Consumer executor = doc -> reference.set(doc.getFieldValueAsBytes("data")); final IngestService ingestService = createWithProcessors( - Collections.singletonMap("foo", (factories, tag, description, config) -> new FakeProcessor("foo", tag, description, executor)) + Map.of("foo", (factories, tag, description, config) -> new FakeProcessor("foo", tag, description, executor)) ); ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); @@ -1781,14 +1662,7 @@ public void testCBORParsing() throws Exception { .setPipeline("_id") .setFinalPipeline("_none"); - ingestService.executeBulkRequest( - 1, - Collections.singletonList(indexRequest), - indexReq -> {}, - (integer, e) -> {}, - (thread, e) -> {}, - Names.WRITE - ); + ingestService.executeBulkRequest(1, List.of(indexRequest), indexReq -> {}, (integer, e) -> {}, (thread, e) -> {}, Names.WRITE); } assertThat(reference.get(), is(instanceOf(byte[].class))); @@ -1796,7 +1670,7 @@ public void testCBORParsing() throws Exception { public void testPostIngest() { IngestService ingestService = createWithProcessors( - Collections.singletonMap("mock", (factories, tag, description, config) -> mockCompoundProcessor()) + Map.of("mock", (factories, tag, description, config) -> mockCompoundProcessor()) ); PutPipelineRequest putRequest = new PutPipelineRequest( @@ -1991,15 +1865,7 @@ private void testUpdatingPipeline(String pipelineString) throws Exception { Client client = mock(Client.class); ClusterService clusterService = mock(ClusterService.class); when(clusterService.state()).thenReturn(clusterState); - IngestService ingestService = new IngestService( - clusterService, - threadPool, - null, - null, - null, - Collections.singletonList(DUMMY_PLUGIN), - client - ); + IngestService ingestService = new IngestService(clusterService, threadPool, null, null, null, List.of(DUMMY_PLUGIN), client); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, clusterState)); CountDownLatch latch = new CountDownLatch(1); @@ -2196,7 +2062,7 @@ private static IngestService createWithProcessors(Map ThreadPool threadPool = mock(ThreadPool.class); when(threadPool.generic()).thenReturn(EsExecutors.DIRECT_EXECUTOR_SERVICE); when(threadPool.executor(anyString())).thenReturn(EsExecutors.DIRECT_EXECUTOR_SERVICE); - return new IngestService(mock(ClusterService.class), threadPool, null, null, null, Collections.singletonList(new IngestPlugin() { + return new IngestService(mock(ClusterService.class), threadPool, null, null, null, List.of(new IngestPlugin() { @Override public Map getProcessors(final Processor.Parameters parameters) { return processors; diff --git a/server/src/test/java/org/elasticsearch/ingest/PipelineFactoryTests.java b/server/src/test/java/org/elasticsearch/ingest/PipelineFactoryTests.java index 574c631d0917..12a44e652f5f 100644 --- a/server/src/test/java/org/elasticsearch/ingest/PipelineFactoryTests.java +++ b/server/src/test/java/org/elasticsearch/ingest/PipelineFactoryTests.java @@ -13,14 +13,12 @@ import org.elasticsearch.script.ScriptService; import org.elasticsearch.test.ESTestCase; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.mock; @@ -42,11 +40,8 @@ public void testCreate() throws Exception { if (metadata != null) { pipelineConfig.put(Pipeline.META_KEY, metadata); } - pipelineConfig.put( - Pipeline.PROCESSORS_KEY, - Arrays.asList(Collections.singletonMap("test", processorConfig0), Collections.singletonMap("test", processorConfig1)) - ); - Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); + pipelineConfig.put(Pipeline.PROCESSORS_KEY, List.of(Map.of("test", processorConfig0), Map.of("test", processorConfig1))); + Map processorRegistry = Map.of("test", new TestProcessor.Factory()); Pipeline pipeline = Pipeline.create("_id", pipelineConfig, processorRegistry, scriptService); assertThat(pipeline.getId(), equalTo("_id")); assertThat(pipeline.getDescription(), equalTo("_description")); @@ -66,7 +61,7 @@ public void testCreateWithNoProcessorsField() throws Exception { pipelineConfig.put(Pipeline.META_KEY, metadata); } try { - Pipeline.create("_id", pipelineConfig, Collections.emptyMap(), scriptService); + Pipeline.create("_id", pipelineConfig, Map.of(), scriptService); fail("should fail, missing required [processors] field"); } catch (ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[processors] required property is missing")); @@ -80,7 +75,7 @@ public void testCreateWithEmptyProcessorsField() throws Exception { if (metadata != null) { pipelineConfig.put(Pipeline.META_KEY, metadata); } - pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.emptyList()); + pipelineConfig.put(Pipeline.PROCESSORS_KEY, List.of()); Pipeline pipeline = Pipeline.create("_id", pipelineConfig, null, scriptService); assertThat(pipeline.getId(), equalTo("_id")); assertThat(pipeline.getDescription(), equalTo("_description")); @@ -96,9 +91,9 @@ public void testCreateWithPipelineOnFailure() throws Exception { if (metadata != null) { pipelineConfig.put(Pipeline.META_KEY, metadata); } - pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); - pipelineConfig.put(Pipeline.ON_FAILURE_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); - Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); + pipelineConfig.put(Pipeline.PROCESSORS_KEY, List.of(Map.of("test", processorConfig))); + pipelineConfig.put(Pipeline.ON_FAILURE_KEY, List.of(Map.of("test", processorConfig))); + Map processorRegistry = Map.of("test", new TestProcessor.Factory()); Pipeline pipeline = Pipeline.create("_id", pipelineConfig, processorRegistry, scriptService); assertThat(pipeline.getId(), equalTo("_id")); assertThat(pipeline.getDescription(), equalTo("_description")); @@ -117,9 +112,9 @@ public void testCreateWithPipelineEmptyOnFailure() throws Exception { if (metadata != null) { pipelineConfig.put(Pipeline.META_KEY, metadata); } - pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); - pipelineConfig.put(Pipeline.ON_FAILURE_KEY, Collections.emptyList()); - Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); + pipelineConfig.put(Pipeline.PROCESSORS_KEY, List.of(Map.of("test", processorConfig))); + pipelineConfig.put(Pipeline.ON_FAILURE_KEY, List.of()); + Map processorRegistry = Map.of("test", new TestProcessor.Factory()); Exception e = expectThrows( ElasticsearchParseException.class, () -> Pipeline.create("_id", pipelineConfig, processorRegistry, scriptService) @@ -129,15 +124,15 @@ public void testCreateWithPipelineEmptyOnFailure() throws Exception { public void testCreateWithPipelineEmptyOnFailureInProcessor() throws Exception { Map processorConfig = new HashMap<>(); - processorConfig.put(Pipeline.ON_FAILURE_KEY, Collections.emptyList()); + processorConfig.put(Pipeline.ON_FAILURE_KEY, List.of()); Map pipelineConfig = new HashMap<>(); pipelineConfig.put(Pipeline.DESCRIPTION_KEY, "_description"); pipelineConfig.put(Pipeline.VERSION_KEY, versionString); if (metadata != null) { pipelineConfig.put(Pipeline.META_KEY, metadata); } - pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); - Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); + pipelineConfig.put(Pipeline.PROCESSORS_KEY, List.of(Map.of("test", processorConfig))); + Map processorRegistry = Map.of("test", new TestProcessor.Factory()); Exception e = expectThrows( ElasticsearchParseException.class, () -> Pipeline.create("_id", pipelineConfig, processorRegistry, scriptService) @@ -149,14 +144,14 @@ public void testCreateWithPipelineIgnoreFailure() throws Exception { Map processorConfig = new HashMap<>(); processorConfig.put("ignore_failure", true); - Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); + Map processorRegistry = Map.of("test", new TestProcessor.Factory()); Map pipelineConfig = new HashMap<>(); pipelineConfig.put(Pipeline.DESCRIPTION_KEY, "_description"); pipelineConfig.put(Pipeline.VERSION_KEY, versionString); if (metadata != null) { pipelineConfig.put(Pipeline.META_KEY, metadata); } - pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); + pipelineConfig.put(Pipeline.PROCESSORS_KEY, List.of(Map.of("test", processorConfig))); Pipeline pipeline = Pipeline.create("_id", pipelineConfig, processorRegistry, scriptService); assertThat(pipeline.getId(), equalTo("_id")); @@ -179,8 +174,8 @@ public void testCreateUnusedProcessorOptions() throws Exception { if (metadata != null) { pipelineConfig.put(Pipeline.META_KEY, metadata); } - pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); - Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); + pipelineConfig.put(Pipeline.PROCESSORS_KEY, List.of(Map.of("test", processorConfig))); + Map processorRegistry = Map.of("test", new TestProcessor.Factory()); Exception e = expectThrows( ElasticsearchParseException.class, () -> Pipeline.create("_id", pipelineConfig, processorRegistry, scriptService) @@ -190,7 +185,7 @@ public void testCreateUnusedProcessorOptions() throws Exception { public void testCreateProcessorsWithOnFailureProperties() throws Exception { Map processorConfig = new HashMap<>(); - processorConfig.put(Pipeline.ON_FAILURE_KEY, Collections.singletonList(Collections.singletonMap("test", new HashMap<>()))); + processorConfig.put(Pipeline.ON_FAILURE_KEY, List.of(Map.of("test", new HashMap<>()))); Map pipelineConfig = new HashMap<>(); pipelineConfig.put(Pipeline.DESCRIPTION_KEY, "_description"); @@ -198,8 +193,8 @@ public void testCreateProcessorsWithOnFailureProperties() throws Exception { if (metadata != null) { pipelineConfig.put(Pipeline.META_KEY, metadata); } - pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); - Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); + pipelineConfig.put(Pipeline.PROCESSORS_KEY, List.of(Map.of("test", processorConfig))); + Map processorRegistry = Map.of("test", new TestProcessor.Factory()); Pipeline pipeline = Pipeline.create("_id", pipelineConfig, processorRegistry, scriptService); assertThat(pipeline.getId(), equalTo("_id")); assertThat(pipeline.getDescription(), equalTo("_description")); @@ -211,11 +206,7 @@ public void testCreateProcessorsWithOnFailureProperties() throws Exception { public void testFlattenProcessors() throws Exception { TestProcessor testProcessor = new TestProcessor(ingestDocument -> {}); CompoundProcessor processor1 = new CompoundProcessor(testProcessor, testProcessor); - CompoundProcessor processor2 = new CompoundProcessor( - false, - Collections.singletonList(testProcessor), - Collections.singletonList(testProcessor) - ); + CompoundProcessor processor2 = new CompoundProcessor(false, List.of(testProcessor), List.of(testProcessor)); Pipeline pipeline = new Pipeline("_id", "_description", version, null, new CompoundProcessor(processor1, processor2)); List flattened = pipeline.flattenAllProcessors(); assertThat(flattened.size(), equalTo(4)); diff --git a/server/src/test/java/org/elasticsearch/ingest/PipelineProcessorTests.java b/server/src/test/java/org/elasticsearch/ingest/PipelineProcessorTests.java index 68ec92118659..b15d37fdbc16 100644 --- a/server/src/test/java/org/elasticsearch/ingest/PipelineProcessorTests.java +++ b/server/src/test/java/org/elasticsearch/ingest/PipelineProcessorTests.java @@ -13,8 +13,6 @@ import org.elasticsearch.test.ESTestCase; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -22,8 +20,8 @@ import java.util.concurrent.TimeUnit; import java.util.function.LongSupplier; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -61,7 +59,7 @@ public String getDescription() { PipelineProcessor.Factory factory = new PipelineProcessor.Factory(ingestService); Map config = new HashMap<>(); config.put("name", pipelineId); - factory.create(Collections.emptyMap(), null, null, config).execute(testIngestDocument, (result, e) -> {}); + factory.create(Map.of(), null, null, config).execute(testIngestDocument, (result, e) -> {}); assertEquals(testIngestDocument, invoked.get()); } @@ -72,8 +70,7 @@ public void testThrowsOnMissingPipeline() throws Exception { Map config = new HashMap<>(); config.put("name", "missingPipelineId"); IllegalStateException[] e = new IllegalStateException[1]; - factory.create(Collections.emptyMap(), null, null, config) - .execute(testIngestDocument, (result, e1) -> e[0] = (IllegalStateException) e1); + factory.create(Map.of(), null, null, config).execute(testIngestDocument, (result, e1) -> e[0] = (IllegalStateException) e1); assertEquals("Pipeline processor configured for non-existent pipeline [missingPipelineId]", e[0].getMessage()); } @@ -87,7 +84,7 @@ public void testIgnoreMissingPipeline() throws Exception { var r = new IngestDocument[1]; var e = new Exception[1]; - var processor = factory.create(Collections.emptyMap(), null, null, config); + var processor = factory.create(Map.of(), null, null, config); processor.execute(testIngestDocument, (result, e1) -> { r[0] = result; e[0] = e1; @@ -109,7 +106,7 @@ public void testThrowsOnRecursivePipelineInvocations() throws Exception { null, null, null, - new CompoundProcessor(factory.create(Collections.emptyMap(), null, null, outerConfig)) + new CompoundProcessor(factory.create(Map.of(), null, null, outerConfig)) ); Map innerConfig = new HashMap<>(); innerConfig.put("name", outerPipelineId); @@ -118,14 +115,13 @@ public void testThrowsOnRecursivePipelineInvocations() throws Exception { null, null, null, - new CompoundProcessor(factory.create(Collections.emptyMap(), null, null, innerConfig)) + new CompoundProcessor(factory.create(Map.of(), null, null, innerConfig)) ); when(ingestService.getPipeline(outerPipelineId)).thenReturn(outer); when(ingestService.getPipeline(innerPipelineId)).thenReturn(inner); outerConfig.put("name", innerPipelineId); ElasticsearchException[] e = new ElasticsearchException[1]; - factory.create(Collections.emptyMap(), null, null, outerConfig) - .execute(testIngestDocument, (result, e1) -> e[0] = (ElasticsearchException) e1); + factory.create(Map.of(), null, null, outerConfig).execute(testIngestDocument, (result, e1) -> e[0] = (ElasticsearchException) e1); assertEquals("Cycle detected for pipeline: inner", e[0].getRootCause().getMessage()); } @@ -138,7 +134,7 @@ public void testAllowsRepeatedPipelineInvocations() throws Exception { PipelineProcessor.Factory factory = new PipelineProcessor.Factory(ingestService); Pipeline inner = new Pipeline(innerPipelineId, null, null, null, new CompoundProcessor()); when(ingestService.getPipeline(innerPipelineId)).thenReturn(inner); - Processor outerProc = factory.create(Collections.emptyMap(), null, null, outerConfig); + Processor outerProc = factory.create(Map.of(), null, null, outerConfig); outerProc.execute(testIngestDocument, (result, e) -> {}); outerProc.execute(testIngestDocument, (result, e) -> {}); } @@ -152,11 +148,11 @@ public void testPipelineProcessorWithPipelineChain() throws Exception { Map pipeline1ProcessorConfig = new HashMap<>(); pipeline1ProcessorConfig.put("name", pipeline2Id); - PipelineProcessor pipeline1Processor = factory.create(Collections.emptyMap(), null, null, pipeline1ProcessorConfig); + PipelineProcessor pipeline1Processor = factory.create(Map.of(), null, null, pipeline1ProcessorConfig); Map pipeline2ProcessorConfig = new HashMap<>(); pipeline2ProcessorConfig.put("name", pipeline3Id); - PipelineProcessor pipeline2Processor = factory.create(Collections.emptyMap(), null, null, pipeline2ProcessorConfig); + PipelineProcessor pipeline2Processor = factory.create(Map.of(), null, null, pipeline2ProcessorConfig); LongSupplier relativeTimeProvider = mock(LongSupplier.class); when(relativeTimeProvider.getAsLong()).thenReturn(0L); @@ -172,11 +168,8 @@ public void testPipelineProcessorWithPipelineChain() throws Exception { null, new CompoundProcessor( true, - Arrays.asList( - new TestProcessor(ingestDocument -> { ingestDocument.setFieldValue(key1, randomInt()); }), - pipeline2Processor - ), - Collections.emptyList() + List.of(new TestProcessor(ingestDocument -> { ingestDocument.setFieldValue(key1, randomInt()); }), pipeline2Processor), + List.of() ), relativeTimeProvider ); diff --git a/server/src/test/java/org/elasticsearch/ingest/TrackingResultProcessorTests.java b/server/src/test/java/org/elasticsearch/ingest/TrackingResultProcessorTests.java index 7054778b8528..dfeb418918fd 100644 --- a/server/src/test/java/org/elasticsearch/ingest/TrackingResultProcessorTests.java +++ b/server/src/test/java/org/elasticsearch/ingest/TrackingResultProcessorTests.java @@ -20,8 +20,6 @@ import org.mockito.Mockito; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -31,11 +29,11 @@ import static org.elasticsearch.ingest.CompoundProcessor.ON_FAILURE_PROCESSOR_TYPE_FIELD; import static org.elasticsearch.ingest.PipelineProcessorTests.createIngestService; import static org.elasticsearch.ingest.TrackingResultProcessor.decorate; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; import static org.mockito.Mockito.verify; @@ -103,14 +101,8 @@ public void testActualCompoundProcessorWithOnFailure() throws Exception { TestProcessor onFailureProcessor = new TestProcessor("success", "test", null, ingestDocument -> {}); CompoundProcessor actualProcessor = new CompoundProcessor( false, - Arrays.asList( - new CompoundProcessor( - false, - Arrays.asList(failProcessor, onFailureProcessor), - Arrays.asList(onFailureProcessor, failProcessor) - ) - ), - Arrays.asList(onFailureProcessor) + List.of(new CompoundProcessor(false, List.of(failProcessor, onFailureProcessor), List.of(onFailureProcessor, failProcessor))), + List.of(onFailureProcessor) ); CompoundProcessor trackingProcessor = decorate(actualProcessor, null, resultList); trackingProcessor.execute(ingestDocument, (result, e) -> {}); @@ -161,10 +153,7 @@ public void testActualCompoundProcessorWithOnFailureAndTrueCondition() throws Ex String scriptName = "conditionalScript"; ScriptService scriptService = new ScriptService( Settings.builder().build(), - Collections.singletonMap( - Script.DEFAULT_SCRIPT_LANG, - new MockScriptEngine(Script.DEFAULT_SCRIPT_LANG, Collections.singletonMap(scriptName, ctx -> true), Collections.emptyMap()) - ), + Map.of(Script.DEFAULT_SCRIPT_LANG, new MockScriptEngine(Script.DEFAULT_SCRIPT_LANG, Map.of(scriptName, ctx -> true), Map.of())), new HashMap<>(ScriptModule.CORE_CONTEXTS), () -> 1L ); @@ -173,16 +162,12 @@ public void testActualCompoundProcessorWithOnFailureAndTrueCondition() throws Ex ConditionalProcessor conditionalProcessor = new ConditionalProcessor( randomAlphaOfLength(10), null, - new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, scriptName, Collections.emptyMap()), + new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, scriptName, Map.of()), scriptService, failProcessor ); TestProcessor onFailureProcessor = new TestProcessor("success", "test", null, ingestDocument -> {}); - CompoundProcessor actualProcessor = new CompoundProcessor( - false, - Arrays.asList(conditionalProcessor), - Arrays.asList(onFailureProcessor) - ); + CompoundProcessor actualProcessor = new CompoundProcessor(false, List.of(conditionalProcessor), List.of(onFailureProcessor)); CompoundProcessor trackingProcessor = decorate(actualProcessor, null, resultList); trackingProcessor.execute(ingestDocument, (result, e) -> {}); @@ -223,7 +208,7 @@ public void testActualCompoundProcessorWithOnFailureAndTrueCondition() throws Ex public void testActualCompoundProcessorWithIgnoreFailure() throws Exception { RuntimeException exception = new RuntimeException("processor failed"); TestProcessor testProcessor = new TestProcessor(ingestDocument -> { throw exception; }); - CompoundProcessor actualProcessor = new CompoundProcessor(true, Collections.singletonList(testProcessor), Collections.emptyList()); + CompoundProcessor actualProcessor = new CompoundProcessor(true, List.of(testProcessor), List.of()); CompoundProcessor trackingProcessor = decorate(actualProcessor, null, resultList); trackingProcessor.execute(ingestDocument, (result, e) -> {}); @@ -250,9 +235,9 @@ public void testActualCompoundProcessorWithFalseConditional() throws Exception { String scriptName = "conditionalScript"; ScriptService scriptService = new ScriptService( Settings.builder().build(), - Collections.singletonMap( + Map.of( Script.DEFAULT_SCRIPT_LANG, - new MockScriptEngine(Script.DEFAULT_SCRIPT_LANG, Collections.singletonMap(scriptName, ctx -> false), Collections.emptyMap()) + new MockScriptEngine(Script.DEFAULT_SCRIPT_LANG, Map.of(scriptName, ctx -> false), Map.of()) ), new HashMap<>(ScriptModule.CORE_CONTEXTS), () -> 1L @@ -263,7 +248,7 @@ public void testActualCompoundProcessorWithFalseConditional() throws Exception { new ConditionalProcessor( randomAlphaOfLength(10), null, - new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, scriptName, Collections.emptyMap()), + new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, scriptName, Map.of()), scriptService, new TestProcessor(ingestDocument -> { ingestDocument.setFieldValue(key2, randomInt()); }) ), @@ -322,7 +307,7 @@ public void testActualPipelineProcessor() throws Exception { ); when(ingestService.getPipeline(pipelineId)).thenReturn(pipeline); - PipelineProcessor pipelineProcessor = factory.create(Collections.emptyMap(), null, null, pipelineConfig); + PipelineProcessor pipelineProcessor = factory.create(Map.of(), null, null, pipelineConfig); CompoundProcessor actualProcessor = new CompoundProcessor(pipelineProcessor); CompoundProcessor trackingProcessor = decorate(actualProcessor, null, resultList); @@ -378,10 +363,7 @@ public void testActualPipelineProcessorWithTrueConditional() throws Exception { ScriptService scriptService = new ScriptService( Settings.builder().build(), - Collections.singletonMap( - Script.DEFAULT_SCRIPT_LANG, - new MockScriptEngine(Script.DEFAULT_SCRIPT_LANG, Collections.singletonMap(scriptName, ctx -> true), Collections.emptyMap()) - ), + Map.of(Script.DEFAULT_SCRIPT_LANG, new MockScriptEngine(Script.DEFAULT_SCRIPT_LANG, Map.of(scriptName, ctx -> true), Map.of())), new HashMap<>(ScriptModule.CORE_CONTEXTS), () -> 1L ); @@ -396,9 +378,9 @@ public void testActualPipelineProcessorWithTrueConditional() throws Exception { new ConditionalProcessor( randomAlphaOfLength(10), null, - new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, scriptName, Collections.emptyMap()), + new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, scriptName, Map.of()), scriptService, - factory.create(Collections.emptyMap(), "pipeline1", null, pipelineConfig2) + factory.create(Map.of(), "pipeline1", null, pipelineConfig2) ), new TestProcessor(ingestDocument -> { ingestDocument.setFieldValue(key3, randomInt()); }) ) @@ -415,7 +397,7 @@ public void testActualPipelineProcessorWithTrueConditional() throws Exception { when(ingestService.getPipeline(pipelineId1)).thenReturn(pipeline1); when(ingestService.getPipeline(pipelineId2)).thenReturn(pipeline2); - PipelineProcessor pipelineProcessor = factory.create(Collections.emptyMap(), "pipeline0", null, pipelineConfig0); + PipelineProcessor pipelineProcessor = factory.create(Map.of(), "pipeline0", null, pipelineConfig0); CompoundProcessor actualProcessor = new CompoundProcessor(pipelineProcessor); CompoundProcessor trackingProcessor = decorate(actualProcessor, null, resultList); @@ -478,9 +460,9 @@ public void testActualPipelineProcessorWithFalseConditional() throws Exception { ScriptService scriptService = new ScriptService( Settings.builder().build(), - Collections.singletonMap( + Map.of( Script.DEFAULT_SCRIPT_LANG, - new MockScriptEngine(Script.DEFAULT_SCRIPT_LANG, Collections.singletonMap(scriptName, ctx -> false), Collections.emptyMap()) + new MockScriptEngine(Script.DEFAULT_SCRIPT_LANG, Map.of(scriptName, ctx -> false), Map.of()) ), new HashMap<>(ScriptModule.CORE_CONTEXTS), () -> 1L @@ -496,9 +478,9 @@ public void testActualPipelineProcessorWithFalseConditional() throws Exception { new ConditionalProcessor( randomAlphaOfLength(10), null, - new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, scriptName, Collections.emptyMap()), + new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, scriptName, Map.of()), scriptService, - factory.create(Collections.emptyMap(), null, null, pipelineConfig2) + factory.create(Map.of(), null, null, pipelineConfig2) ), new TestProcessor(ingestDocument -> { ingestDocument.setFieldValue(key3, randomInt()); }) ) @@ -515,7 +497,7 @@ public void testActualPipelineProcessorWithFalseConditional() throws Exception { when(ingestService.getPipeline(pipelineId1)).thenReturn(pipeline1); when(ingestService.getPipeline(pipelineId2)).thenReturn(pipeline2); - PipelineProcessor pipelineProcessor = factory.create(Collections.emptyMap(), null, null, pipelineConfig0); + PipelineProcessor pipelineProcessor = factory.create(Map.of(), null, null, pipelineConfig0); CompoundProcessor actualProcessor = new CompoundProcessor(pipelineProcessor); CompoundProcessor trackingProcessor = decorate(actualProcessor, null, resultList); @@ -573,15 +555,15 @@ public void testActualPipelineProcessorWithHandledFailure() throws Exception { new TestProcessor(ingestDocument -> { ingestDocument.setFieldValue(key1, randomInt()); }), new CompoundProcessor( false, - Collections.singletonList(new TestProcessor(ingestDocument -> { throw exception; })), - Collections.singletonList(new TestProcessor(ingestDocument -> { ingestDocument.setFieldValue(key2, randomInt()); })) + List.of(new TestProcessor(ingestDocument -> { throw exception; })), + List.of(new TestProcessor(ingestDocument -> { ingestDocument.setFieldValue(key2, randomInt()); })) ), new TestProcessor(ingestDocument -> { ingestDocument.setFieldValue(key3, randomInt()); }) ) ); when(ingestService.getPipeline(pipelineId)).thenReturn(pipeline); - PipelineProcessor pipelineProcessor = factory.create(Collections.emptyMap(), null, null, pipelineConfig); + PipelineProcessor pipelineProcessor = factory.create(Map.of(), null, null, pipelineConfig); CompoundProcessor actualProcessor = new CompoundProcessor(pipelineProcessor); CompoundProcessor trackingProcessor = decorate(actualProcessor, null, resultList); @@ -642,7 +624,7 @@ public void testActualPipelineProcessorWithUnhandledFailure() throws Exception { ); when(ingestService.getPipeline(pipelineId)).thenReturn(pipeline); - PipelineProcessor pipelineProcessor = factory.create(Collections.emptyMap(), null, null, pipelineConfig); + PipelineProcessor pipelineProcessor = factory.create(Map.of(), null, null, pipelineConfig); CompoundProcessor actualProcessor = new CompoundProcessor(pipelineProcessor); CompoundProcessor trackingProcessor = decorate(actualProcessor, null, resultList); @@ -684,7 +666,7 @@ public void testActualPipelineProcessorWithCycle() throws Exception { null, null, null, - new CompoundProcessor(factory.create(Collections.emptyMap(), null, null, pipelineConfig2)) + new CompoundProcessor(factory.create(Map.of(), null, null, pipelineConfig2)) ); Pipeline pipeline2 = new Pipeline( @@ -692,13 +674,13 @@ public void testActualPipelineProcessorWithCycle() throws Exception { null, null, null, - new CompoundProcessor(factory.create(Collections.emptyMap(), null, null, pipelineConfig1)) + new CompoundProcessor(factory.create(Map.of(), null, null, pipelineConfig1)) ); when(ingestService.getPipeline(pipelineId1)).thenReturn(pipeline1); when(ingestService.getPipeline(pipelineId2)).thenReturn(pipeline2); - PipelineProcessor pipelineProcessor = factory.create(Collections.emptyMap(), null, null, pipelineConfig0); + PipelineProcessor pipelineProcessor = factory.create(Map.of(), null, null, pipelineConfig0); CompoundProcessor actualProcessor = new CompoundProcessor(pipelineProcessor); CompoundProcessor trackingProcessor = decorate(actualProcessor, null, resultList); @@ -718,7 +700,7 @@ public void testActualPipelineProcessorRepeatedInvocation() throws Exception { PipelineProcessor.Factory factory = new PipelineProcessor.Factory(ingestService); String key1 = randomAlphaOfLength(10); - PipelineProcessor pipelineProcessor = factory.create(Collections.emptyMap(), null, null, pipelineConfig); + PipelineProcessor pipelineProcessor = factory.create(Map.of(), null, null, pipelineConfig); Pipeline pipeline = new Pipeline( pipelineId, null, diff --git a/server/src/test/java/org/elasticsearch/ingest/ValueSourceTests.java b/server/src/test/java/org/elasticsearch/ingest/ValueSourceTests.java index f774bc3ed77e..84b7db730159 100644 --- a/server/src/test/java/org/elasticsearch/ingest/ValueSourceTests.java +++ b/server/src/test/java/org/elasticsearch/ingest/ValueSourceTests.java @@ -12,7 +12,6 @@ import org.elasticsearch.test.ESTestCase; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -20,8 +19,8 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.sameInstance; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -34,7 +33,7 @@ public void testDeepCopy() { for (int i = 0; i < iterations; i++) { Map map = RandomDocumentPicks.randomSource(random()); ValueSource valueSource = ValueSource.wrap(map, TestTemplateService.instance()); - Object copy = valueSource.copyAndResolve(Collections.emptyMap()); + Object copy = valueSource.copyAndResolve(Map.of()); assertThat("iteration: " + i, copy, equalTo(map)); assertThat("iteration: " + i, copy, not(sameInstance(map))); } @@ -86,7 +85,7 @@ public void testScriptShouldCompile() { String compiledValue = randomAlphaOfLength(10); when(scriptService.compile(any(), any())).thenReturn(new TestTemplateService.MockTemplateScript.Factory(compiledValue)); ValueSource result = ValueSource.wrap(propertyValue, scriptService); - assertThat(result.copyAndResolve(Collections.emptyMap()), equalTo(compiledValue)); + assertThat(result.copyAndResolve(Map.of()), equalTo(compiledValue)); verify(scriptService, times(1)).compile(any(), any()); } } From 555aa78e158f2ef1d031f8ac89b65059ba205bdb Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 13 Dec 2022 09:52:35 +1100 Subject: [PATCH 244/919] [Test] Ensure singleValue requirement is correctly set (#92288) The validator should be configured accordingly depending on whether incoming value is single valued. Resolves: #92282 --- .../xpack/security/authc/jwt/JwtStringClaimValidatorTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidatorTests.java index fe49a6196be7..41e666db39a5 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtStringClaimValidatorTests.java @@ -103,7 +103,7 @@ public void testMatchingClaimValues() throws ParseException { // fallback claim is ignored validJwtClaimsSet = JWTClaimsSet.parse(Map.of(claimName, incomingClaimValue, fallbackClaimName, List.of(42))); } else { - validator = new JwtStringClaimValidator(claimName, Map.of(claimName, fallbackClaimName), allowedClaimValues, randomBoolean()); + validator = new JwtStringClaimValidator(claimName, Map.of(claimName, fallbackClaimName), allowedClaimValues, singleValuedClaim); validJwtClaimsSet = JWTClaimsSet.parse(Map.of(fallbackClaimName, incomingClaimValue)); } From 34ddaa7478a261a9e4a1df81b985924de4aa18b5 Mon Sep 17 00:00:00 2001 From: Lee Hinman Date: Mon, 12 Dec 2022 16:02:37 -0700 Subject: [PATCH 245/919] Tweak lang-mustache factory (#92211) This commits makes a few very minor tweaks to our Mustache scripting capabilities. 1. Switches from `DefaultMustacheFactory` to `SafeMustacheFactory` In the event that the security manager were disabled (or removed, as is threatened in subsequent JDK releases), Mustache's "partial template" (`{{>partial}}`) support is a security risk because it would allow reading from an arbitrary URL or file on disk. Switching to the "Safe" version and passing in an empty set into the parent constructor disallows using partial Mustache templates. This also switches from `Function` to the `TemplateFunction` value for the built-in functions. 3. Minor internal optimization This removes useless grouping for one of the built-in mustache functions, and removes the call to `CollectionUtils.ensureNoSelfReferences` used in `CustomReflectionObjectHandler`. This check during stringification should not be necessary because Mustache templates without "partial" support cannot be self-referencing. --- .../mustache/CustomMustacheFactory.java | 17 ++++---- .../CustomReflectionObjectHandler.java | 7 ---- .../xpack/core/watcher/watch/Payload.java | 2 + x-pack/plugin/watcher/qa/rest/build.gradle | 1 + .../test/painless/40_exception.yml | 39 +++---------------- 5 files changed, 18 insertions(+), 48 deletions(-) diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/CustomMustacheFactory.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/CustomMustacheFactory.java index 37abffed2fed..6c432c7306d3 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/CustomMustacheFactory.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/CustomMustacheFactory.java @@ -14,7 +14,9 @@ import com.github.mustachejava.Mustache; import com.github.mustachejava.MustacheException; import com.github.mustachejava.MustacheVisitor; +import com.github.mustachejava.SafeMustacheFactory; import com.github.mustachejava.TemplateContext; +import com.github.mustachejava.TemplateFunction; import com.github.mustachejava.codes.DefaultMustache; import com.github.mustachejava.codes.IterableCode; import com.github.mustachejava.codes.WriteCode; @@ -34,12 +36,11 @@ import java.util.Map; import java.util.Objects; import java.util.StringJoiner; -import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; -public class CustomMustacheFactory extends DefaultMustacheFactory { +public class CustomMustacheFactory extends SafeMustacheFactory { static final String V7_JSON_MEDIA_TYPE_WITH_CHARSET = "application/json; charset=UTF-8"; static final String JSON_MEDIA_TYPE_WITH_CHARSET = "application/json;charset=utf-8"; static final String JSON_MEDIA_TYPE = "application/json"; @@ -64,7 +65,7 @@ public class CustomMustacheFactory extends DefaultMustacheFactory { private final Encoder encoder; public CustomMustacheFactory(String mediaType) { - super(); + super(Collections.emptySet(), "."); setObjectHandler(new CustomReflectionObjectHandler()); this.encoder = createEncoder(mediaType); } @@ -145,7 +146,7 @@ protected void tag(Writer writer, String tag) throws IOException { writer.write(tc.endChars()); } - protected abstract Function createFunction(Object resolved); + protected abstract TemplateFunction createFunction(Object resolved); /** * At compile time, this function extracts the name of the variable: @@ -188,7 +189,7 @@ static class ToJsonCode extends CustomCode { @Override @SuppressWarnings("unchecked") - protected Function createFunction(Object resolved) { + protected TemplateFunction createFunction(Object resolved) { return s -> { if (resolved == null) { return null; @@ -238,7 +239,7 @@ static class JoinerCode extends CustomCode { } @Override - protected Function createFunction(Object resolved) { + protected TemplateFunction createFunction(Object resolved) { return s -> { if (s == null) { return null; @@ -260,7 +261,7 @@ static boolean match(String variable) { static class CustomJoinerCode extends JoinerCode { - private static final Pattern PATTERN = Pattern.compile("^(?:" + CODE + " delimiter='(.*)')$"); + private static final Pattern PATTERN = Pattern.compile("^" + CODE + " delimiter='(.*)'$"); CustomJoinerCode(TemplateContext tc, DefaultMustacheFactory df, Mustache mustache, String variable) { super(tc, df, mustache, extractDelimiter(variable)); @@ -357,7 +358,7 @@ static class UrlEncoder implements Encoder { @Override public void encode(String s, Writer writer) throws IOException { - writer.write(URLEncoder.encode(s, StandardCharsets.UTF_8.name())); + writer.write(URLEncoder.encode(s, StandardCharsets.UTF_8)); } } } diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/CustomReflectionObjectHandler.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/CustomReflectionObjectHandler.java index d3ec96f68af5..12634c053412 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/CustomReflectionObjectHandler.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/CustomReflectionObjectHandler.java @@ -10,7 +10,6 @@ import com.github.mustachejava.reflect.ReflectionObjectHandler; -import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.common.util.Maps; import org.elasticsearch.common.util.iterable.Iterables; @@ -144,10 +143,4 @@ public Iterator iterator() { return col.iterator(); } } - - @Override - public String stringify(Object object) { - CollectionUtils.ensureNoSelfReferences(object, "CustomReflectionObjectHandler stringify"); - return super.stringify(object); - } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/watch/Payload.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/watch/Payload.java index efe22fcab9d7..46803619f562 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/watch/Payload.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/watch/Payload.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.core.watcher.watch; import org.elasticsearch.common.collect.MapBuilder; +import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; @@ -37,6 +38,7 @@ public Simple(String key, Object value) { } public Simple(Map data) { + CollectionUtils.ensureNoSelfReferences(data, "watcher action payload"); this.data = data; } diff --git a/x-pack/plugin/watcher/qa/rest/build.gradle b/x-pack/plugin/watcher/qa/rest/build.gradle index 007c11c1d4b6..026ec7edb0fb 100644 --- a/x-pack/plugin/watcher/qa/rest/build.gradle +++ b/x-pack/plugin/watcher/qa/rest/build.gradle @@ -40,6 +40,7 @@ tasks.named("yamlRestTestV7CompatTransform").configure{ task -> task.skipTest("mustache/30_search_input/Test search input mustache integration (using request body and rest_total_hits_as_int)", "remove JodaCompatibleDateTime -- ZonedDateTime doesn't output millis/nanos if they're 0 (#78417)") task.skipTest("mustache/30_search_input/Test search input mustache integration (using request body)", "remove JodaCompatibleDateTime -- ZonedDateTime doesn't output millis/nanos if they're 0 (#78417)") task.skipTest("mustache/40_search_transform/Test search transform mustache integration (using request body)", "remove JodaCompatibleDateTime -- ZonedDateTime doesn't output millis/nanos if they're 0 (#78417)") + task.skipTest("painless/40_exception/Test painless exceptions are returned when logging a broken response", "Exceptions are no longer thrown from Mustache, but from the transform action itself") task.replaceKeyInDo("watcher.ack_watch", "xpack-watcher.ack_watch") task.replaceKeyInDo("watcher.activate_watch", "xpack-watcher.activate_watch") task.replaceKeyInDo("watcher.deactivate_watch", "xpack-watcher.deactivate_watch") diff --git a/x-pack/plugin/watcher/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/painless/40_exception.yml b/x-pack/plugin/watcher/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/painless/40_exception.yml index 411ef8426552..e65d7280e117 100644 --- a/x-pack/plugin/watcher/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/painless/40_exception.yml +++ b/x-pack/plugin/watcher/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/painless/40_exception.yml @@ -37,6 +37,10 @@ --- "Test painless exceptions are returned when logging a broken response": + - skip: + version: " - 8.6.99" + reason: "self-referencing objects were fixed to be in Painless instead of Mustache in 8.7" + - do: cluster.health: wait_for_status: green @@ -75,36 +79,5 @@ - match: { watch_record.trigger_event.type: "manual" } - match: { watch_record.state: "executed" } - match: { watch_record.result.actions.0.status: "failure" } - - match: { watch_record.result.actions.0.error.caused_by.caused_by.type: "illegal_argument_exception" } - - match: { watch_record.result.actions.0.error.caused_by.caused_by.reason: "Iterable object is self-referencing itself (CustomReflectionObjectHandler stringify)" } - - - do: - catch: bad_request - watcher.execute_watch: - body: > - { - "watch": { - "trigger": { - "schedule": { - "interval": "10s" - } - }, - "input": { - "simple": { - "foo": "bar" - } - }, - "actions": { - "my-logging": { - "transform": { - "script": { - "source": "def x = [:] ; def y = [:] ; x.a = y ; y.a = x ; return x" - } - }, - "logging": { - "text": "{{#join}}ctx.payload{{/join}}" - } - } - } - } - } + - match: { watch_record.result.actions.0.reason: "Failed to transform payload" } + - match: { watch_record.result.actions.0.transform.error.reason: "Iterable object is self-referencing itself (watcher action payload)" } From cdfb93fa8fdab686032eeb015d4f8070d46926d6 Mon Sep 17 00:00:00 2001 From: Daniel Mitterdorfer Date: Tue, 13 Dec 2022 07:54:00 +0100 Subject: [PATCH 246/919] Access term dictionary more efficiently (#92269) With this commit we sort all ids that are used in `mget` calls in lexicographically ascending order. As the term dictionary (which is used to lookup `_id`) is also sorted lexicographically, we minimize the number of blocks that need to read, reducing page faults. --- docs/changelog/92269.yaml | 5 +++++ .../profiler/TransportGetProfilingAction.java | 16 +++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 docs/changelog/92269.yaml diff --git a/docs/changelog/92269.yaml b/docs/changelog/92269.yaml new file mode 100644 index 000000000000..5e1381248bb8 --- /dev/null +++ b/docs/changelog/92269.yaml @@ -0,0 +1,5 @@ +pr: 92269 +summary: Access term dictionary more efficiently +area: Search +type: enhancement +issues: [] diff --git a/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/TransportGetProfilingAction.java b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/TransportGetProfilingAction.java index 8a09e2f0a4ec..54709effaaf7 100644 --- a/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/TransportGetProfilingAction.java +++ b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/TransportGetProfilingAction.java @@ -18,7 +18,6 @@ import org.elasticsearch.client.internal.ParentTaskAssigningClient; import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.util.Maps; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.search.aggregations.bucket.terms.StringTerms; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; @@ -30,10 +29,11 @@ import java.util.Arrays; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Random; import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; public class TransportGetProfilingAction extends HandledTransportAction { private static final Logger log = LogManager.getLogger(TransportGetProfilingAction.class); @@ -108,7 +108,10 @@ public void onResponse(SearchResponse searchResponse) { long totalCount = Math.round(totalCountAgg.value()); Resampler resampler = new Resampler(request, eventsIndex.getSampleRate(), totalCount); StringTerms stacktraces = searchResponse.getAggregations().get("group_by"); - Map stackTraceEvents = Maps.newHashMapWithExpectedSize(stacktraces.getBuckets().size()); + // sort items lexicographically to access Lucene's term dictionary more efficiently when issuing an mget request. + // The term dictionary is lexicographically sorted and using the same order reduces the number of page faults + // needed to load it. + Map stackTraceEvents = new TreeMap<>(); for (StringTerms.Bucket bucket : stacktraces.getBuckets()) { Sum count = bucket.getAggregations().get("count"); int finalCount = resampler.adjustSampleCount((int) count.value()); @@ -143,8 +146,11 @@ private void retrieveStackTraces( @Override public void onResponse(MultiGetResponse multiGetItemResponses) { Map stackTracePerId = new HashMap<>(); - Set stackFrameIds = new HashSet<>(); - Set executableIds = new HashSet<>(); + // sort items lexicographically to access Lucene's term dictionary more efficiently when issuing an mget request. + // The term dictionary is lexicographically sorted and using the same order reduces the number of page faults + // needed to load it. + Set stackFrameIds = new TreeSet<>(); + Set executableIds = new TreeSet<>(); int totalFrames = 0; for (MultiGetItemResponse trace : multiGetItemResponses) { if (trace.isFailed() == false && trace.getResponse().isExists()) { From 9fe9020dfcd6412a0f65c69f8247ede02a16671a Mon Sep 17 00:00:00 2001 From: David Roberts Date: Tue, 13 Dec 2022 09:22:49 +0000 Subject: [PATCH 247/919] [ML] Adding a known issue for the ML datafeed auth header problem (#92274) Due to #41185 datafeeds created prior to 7.0 and not updated since then have unparseable authorization headers in 8.x. In 8.0-8.3 this could easily be a non-issue, as such datafeeds were likely forgotten leftovers and never run. Even if it was a problem, only the datafeed of interest would need updating with any urgency. Due to #87884 datafeeds with authorization headers older than 7.0 prevent _all_ datafeeds being listed in 8.4 and 8.5. This in turn breaks the anomaly detection job management section of the ML UI. The problem is alleviated by #92168 and fixed by #92221, but we should warn users that the problem exists in 8.4.0-8.5.3 inclusive. --- docs/reference/release-notes/8.4.0.asciidoc | 17 +++++++++++++++++ docs/reference/release-notes/8.4.1.asciidoc | 2 ++ docs/reference/release-notes/8.4.2.asciidoc | 1 + docs/reference/release-notes/8.4.3.asciidoc | 2 ++ docs/reference/release-notes/8.5.0.asciidoc | 2 ++ docs/reference/release-notes/8.5.1.asciidoc | 6 ++++++ docs/reference/release-notes/8.5.2.asciidoc | 6 ++++++ docs/reference/release-notes/8.5.3.asciidoc | 6 ++++++ 8 files changed, 42 insertions(+) diff --git a/docs/reference/release-notes/8.4.0.asciidoc b/docs/reference/release-notes/8.4.0.asciidoc index e83f9fb83472..9894c81608cb 100644 --- a/docs/reference/release-notes/8.4.0.asciidoc +++ b/docs/reference/release-notes/8.4.0.asciidoc @@ -3,6 +3,23 @@ Also see <>. +[[known-issues-8.4.0]] +[float] +=== Known issues + +// tag::ml-pre-7-datafeeds-known-issue[] +* {ml-cap} {dfeeds} cannot be listed if any are not modified since version 6.x ++ +If you have a {dfeed} that was created in version 5.x or 6.x and has not +been updated since 7.0, it is not possible to list {dfeeds} in +8.4 and 8.5. This means that {anomaly-jobs} cannot be managed using +{kib}. This issue is fixed in 8.6. ++ +If you upgrade to 8.4 or 8.5 with such a {dfeed}, you need to +work around the problem by updating each {dfeed}'s authorization information +using https://support.elastic.dev/knowledge/view/b5a879db[these steps]. +// end::ml-pre-7-datafeeds-known-issue[] + [[bug-8.4.0]] [float] === Bug fixes diff --git a/docs/reference/release-notes/8.4.1.asciidoc b/docs/reference/release-notes/8.4.1.asciidoc index c0ce679621fb..450e6d283013 100644 --- a/docs/reference/release-notes/8.4.1.asciidoc +++ b/docs/reference/release-notes/8.4.1.asciidoc @@ -18,3 +18,5 @@ Machine Learning:: * When using date range search with format that does not have all date fields (missing month or day) an incorrectly parsed date could be used. The workaround is to use date pattern with all date fields (year, month, day) (issue: {es-issue}90187[#90187]) + +include::8.4.0.asciidoc[tag=ml-pre-7-datafeeds-known-issue] diff --git a/docs/reference/release-notes/8.4.2.asciidoc b/docs/reference/release-notes/8.4.2.asciidoc index 2f6481876bd1..a510fba8f561 100644 --- a/docs/reference/release-notes/8.4.2.asciidoc +++ b/docs/reference/release-notes/8.4.2.asciidoc @@ -20,6 +20,7 @@ This regression was fixed in version 8.4.3. an incorrectly parsed date could be used. The workaround is to use date pattern with all date fields (year, month, day) (issue: {es-issue}90187[#90187]) +include::8.4.0.asciidoc[tag=ml-pre-7-datafeeds-known-issue] [[bug-8.4.2]] [float] diff --git a/docs/reference/release-notes/8.4.3.asciidoc b/docs/reference/release-notes/8.4.3.asciidoc index e16c5006e7ff..5014ba33e8bd 100644 --- a/docs/reference/release-notes/8.4.3.asciidoc +++ b/docs/reference/release-notes/8.4.3.asciidoc @@ -28,3 +28,5 @@ Ranking:: * When using date range search with format that does not have all date fields (missing month or day) an incorrectly parsed date could be used. The workaround is to use date pattern with all date fields (year, month, day) (issue: {es-issue}90187[#90187]) + +include::8.4.0.asciidoc[tag=ml-pre-7-datafeeds-known-issue] diff --git a/docs/reference/release-notes/8.5.0.asciidoc b/docs/reference/release-notes/8.5.0.asciidoc index 277ab3fbea16..00d2c03ef61d 100644 --- a/docs/reference/release-notes/8.5.0.asciidoc +++ b/docs/reference/release-notes/8.5.0.asciidoc @@ -14,6 +14,8 @@ to restart nodes while in this state. Upgrade to 8.5.1 as soon as possible to avoid the risk of this occurring ({es-pull}91456[#91456]). If your cluster is affected by this issue, upgrade to 8.5.3 to repair it ({es-pull}91887[#91887]). +include::8.4.0.asciidoc[tag=ml-pre-7-datafeeds-known-issue] + [[breaking-8.5.0]] [float] === Breaking changes diff --git a/docs/reference/release-notes/8.5.1.asciidoc b/docs/reference/release-notes/8.5.1.asciidoc index 4bc3b4c54e41..03c9ba46f235 100644 --- a/docs/reference/release-notes/8.5.1.asciidoc +++ b/docs/reference/release-notes/8.5.1.asciidoc @@ -4,6 +4,12 @@ Also see <>. +[[known-issues-8.5.1]] +[float] +=== Known issues + +include::8.4.0.asciidoc[tag=ml-pre-7-datafeeds-known-issue] + [[bug-8.5.1]] [float] === Bug fixes diff --git a/docs/reference/release-notes/8.5.2.asciidoc b/docs/reference/release-notes/8.5.2.asciidoc index 473cc7625374..dc0f9cca601e 100644 --- a/docs/reference/release-notes/8.5.2.asciidoc +++ b/docs/reference/release-notes/8.5.2.asciidoc @@ -4,6 +4,12 @@ Also see <>. +[[known-issues-8.5.2]] +[float] +=== Known issues + +include::8.4.0.asciidoc[tag=ml-pre-7-datafeeds-known-issue] + [[bug-8.5.2]] [float] === Bug fixes diff --git a/docs/reference/release-notes/8.5.3.asciidoc b/docs/reference/release-notes/8.5.3.asciidoc index 41c1b4090b88..81b85fc2c6ac 100644 --- a/docs/reference/release-notes/8.5.3.asciidoc +++ b/docs/reference/release-notes/8.5.3.asciidoc @@ -3,6 +3,12 @@ Also see <>. +[[known-issues-8.5.3]] +[float] +=== Known issues + +include::8.4.0.asciidoc[tag=ml-pre-7-datafeeds-known-issue] + [[bug-8.5.3]] [float] === Bug fixes From 93bc07e7c43c08e5ae7d6591fe9db675fd4db223 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 13 Dec 2022 09:32:51 +0000 Subject: [PATCH 248/919] Tidy up CoordinationState (#92294) Extracted from #92259 --- .../coordination/CoordinationState.java | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/coordination/CoordinationState.java b/server/src/main/java/org/elasticsearch/cluster/coordination/CoordinationState.java index cb3e5ac8edfd..1bb83fc80a3f 100644 --- a/server/src/main/java/org/elasticsearch/cluster/coordination/CoordinationState.java +++ b/server/src/main/java/org/elasticsearch/cluster/coordination/CoordinationState.java @@ -556,27 +556,18 @@ public interface PersistedState extends Closeable { * marked as committed. */ default void markLastAcceptedStateAsCommitted() { - final ClusterState lastAcceptedState = getLastAcceptedState(); - Metadata.Builder metadataBuilder = null; - if (lastAcceptedState.getLastAcceptedConfiguration().equals(lastAcceptedState.getLastCommittedConfiguration()) == false) { - final CoordinationMetadata coordinationMetadata = CoordinationMetadata.builder(lastAcceptedState.coordinationMetadata()) - .lastCommittedConfiguration(lastAcceptedState.getLastAcceptedConfiguration()) - .build(); - metadataBuilder = Metadata.builder(lastAcceptedState.metadata()); - metadataBuilder.coordinationMetadata(coordinationMetadata); - } + final var lastAcceptedState = getLastAcceptedState(); assert lastAcceptedState.metadata().clusterUUID().equals(Metadata.UNKNOWN_CLUSTER_UUID) == false : "received cluster state with empty cluster uuid: " + lastAcceptedState; - if (lastAcceptedState.metadata().clusterUUID().equals(Metadata.UNKNOWN_CLUSTER_UUID) == false - && lastAcceptedState.metadata().clusterUUIDCommitted() == false) { - if (metadataBuilder == null) { - metadataBuilder = Metadata.builder(lastAcceptedState.metadata()); - } - metadataBuilder.clusterUUIDCommitted(true); + + if (lastAcceptedState.metadata().clusterUUIDCommitted() == false) { logger.info("cluster UUID set to [{}]", lastAcceptedState.metadata().clusterUUID()); } - if (metadataBuilder != null) { - setLastAcceptedState(ClusterState.builder(lastAcceptedState).metadata(metadataBuilder).build()); + + final var adjustedMetadata = lastAcceptedState.metadata() + .withLastCommittedValues(true, lastAcceptedState.getLastAcceptedConfiguration()); + if (adjustedMetadata != lastAcceptedState.metadata()) { + setLastAcceptedState(ClusterState.builder(lastAcceptedState).metadata(adjustedMetadata).build()); } } From 51fe506f68d1ec044c65f23b3f0cbb89a0cd7b24 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 13 Dec 2022 10:13:11 +0000 Subject: [PATCH 249/919] Execute blackhole response on correct transport (#92312) We simulate sending traffic along a blackholed connection with a disconnection exception that we withold until far into the future. Today this exception is delivered using the transport of the responding node, but it should be the requesting node. This commit fixes that. Not sure how this _ever_ worked, but we made things stricter in #92245 and that meant this bug led to test failures. Closes #92307 --- .../CoordinationDiagnosticsServiceTests.java | 2 -- .../cluster/coordination/CoordinatorTests.java | 2 -- .../StableMasterHealthIndicatorServiceTests.java | 2 -- .../transport/DisruptableMockTransport.java | 12 ++++++------ .../transport/DisruptableMockTransportTests.java | 3 ++- .../votingonly/VotingOnlyNodeCoordinatorTests.java | 2 -- 6 files changed, 8 insertions(+), 15 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinationDiagnosticsServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinationDiagnosticsServiceTests.java index 507865d26b3b..452f285ec684 100644 --- a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinationDiagnosticsServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinationDiagnosticsServiceTests.java @@ -8,7 +8,6 @@ package org.elasticsearch.cluster.coordination; -import org.apache.lucene.tests.util.LuceneTestCase; import org.elasticsearch.Version; import org.elasticsearch.cluster.ClusterChangedEvent; import org.elasticsearch.cluster.ClusterName; @@ -63,7 +62,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -@LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/92307") public class CoordinationDiagnosticsServiceTests extends AbstractCoordinatorTestCase { DiscoveryNode node1; DiscoveryNode node2; diff --git a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java index df30724a83e8..eb275cdc487e 100644 --- a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java @@ -11,7 +11,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.LogEvent; -import org.apache.lucene.tests.util.LuceneTestCase; import org.apache.lucene.util.Constants; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.Version; @@ -98,7 +97,6 @@ import static org.hamcrest.Matchers.startsWith; @TestLogging(reason = "these tests do a lot of log-worthy things but we usually don't care", value = "org.elasticsearch:FATAL") -@LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/92307") public class CoordinatorTests extends AbstractCoordinatorTestCase { public void testCanUpdateClusterStateAfterStabilisation() { diff --git a/server/src/test/java/org/elasticsearch/cluster/coordination/StableMasterHealthIndicatorServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/coordination/StableMasterHealthIndicatorServiceTests.java index 4d4e2cd4f455..83ba8fb64459 100644 --- a/server/src/test/java/org/elasticsearch/cluster/coordination/StableMasterHealthIndicatorServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/coordination/StableMasterHealthIndicatorServiceTests.java @@ -8,7 +8,6 @@ package org.elasticsearch.cluster.coordination; -import org.apache.lucene.tests.util.LuceneTestCase; import org.elasticsearch.Version; import org.elasticsearch.cluster.ClusterChangedEvent; import org.elasticsearch.cluster.ClusterName; @@ -52,7 +51,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -@LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/92307") public class StableMasterHealthIndicatorServiceTests extends AbstractCoordinatorTestCase { DiscoveryNode node1; DiscoveryNode node2; diff --git a/test/framework/src/main/java/org/elasticsearch/transport/DisruptableMockTransport.java b/test/framework/src/main/java/org/elasticsearch/transport/DisruptableMockTransport.java index a4d42b0306f9..6c5abb9a8de2 100644 --- a/test/framework/src/main/java/org/elasticsearch/transport/DisruptableMockTransport.java +++ b/test/framework/src/main/java/org/elasticsearch/transport/DisruptableMockTransport.java @@ -194,7 +194,7 @@ private String internalToString() { }); } - protected Runnable getDisconnectException(long requestId, String action, DiscoveryNode destination) { + private Runnable getDisconnectException(long requestId, String action, DiscoveryNode destination) { return new RebootSensitiveRunnable() { @Override public void ifRebooted() { @@ -213,11 +213,11 @@ public String toString() { }; } - protected String getRequestDescription(long requestId, String action, DiscoveryNode destination) { + private String getRequestDescription(long requestId, String action, DiscoveryNode destination) { return format("[%s][%s] from %s to %s", requestId, action, getLocalNode(), destination); } - protected void onBlackholedDuringSend(long requestId, String action, DisruptableMockTransport destinationTransport) { + private void onBlackholedDuringSend(long requestId, String action, DisruptableMockTransport destinationTransport) { logger.trace("dropping {}", getRequestDescription(requestId, action, destinationTransport.getLocalNode())); // Delaying the response until explicitly instructed, to simulate a very long delay blackholedRequests.add(new Runnable() { @@ -233,11 +233,11 @@ public String toString() { }); } - protected void onDisconnectedDuringSend(long requestId, String action, DisruptableMockTransport destinationTransport) { - destinationTransport.execute(getDisconnectException(requestId, action, destinationTransport.getLocalNode())); + private void onDisconnectedDuringSend(long requestId, String action, DisruptableMockTransport destinationTransport) { + execute(getDisconnectException(requestId, action, destinationTransport.getLocalNode())); } - protected void onConnectedDuringSend( + private void onConnectedDuringSend( long requestId, String action, TransportRequest request, diff --git a/test/framework/src/test/java/org/elasticsearch/transport/DisruptableMockTransportTests.java b/test/framework/src/test/java/org/elasticsearch/transport/DisruptableMockTransportTests.java index ca2644c22d34..460e3de541c4 100644 --- a/test/framework/src/test/java/org/elasticsearch/transport/DisruptableMockTransportTests.java +++ b/test/framework/src/test/java/org/elasticsearch/transport/DisruptableMockTransportTests.java @@ -504,7 +504,7 @@ public void testResponseWithReboots() { for (Runnable runnable : Stream.concat( Stream.of(reboot(node1), reboot(node2), blockTestAction), - Stream.of(Tuple.tuple(node1, node2), Tuple.tuple(node2, node2)).map(link -> { + Stream.of(Tuple.tuple(node1, node2), Tuple.tuple(node2, node1)).map(link -> { final var disruption = randomFrom(linkDisruptions); return new Runnable() { @Override @@ -554,6 +554,7 @@ public String toString() { deterministicTaskQueue.runAllRunnableTasks(); assertTrue(responseHandlerReleased.get()); + assertTrue(rebootedNodes.contains(node1) || responseHandlerCalled.get()); } public void testBrokenLinkFailsToConnect() { diff --git a/x-pack/plugin/voting-only-node/src/test/java/org/elasticsearch/cluster/coordination/votingonly/VotingOnlyNodeCoordinatorTests.java b/x-pack/plugin/voting-only-node/src/test/java/org/elasticsearch/cluster/coordination/votingonly/VotingOnlyNodeCoordinatorTests.java index bc00accb3812..915f913cc144 100644 --- a/x-pack/plugin/voting-only-node/src/test/java/org/elasticsearch/cluster/coordination/votingonly/VotingOnlyNodeCoordinatorTests.java +++ b/x-pack/plugin/voting-only-node/src/test/java/org/elasticsearch/cluster/coordination/votingonly/VotingOnlyNodeCoordinatorTests.java @@ -6,7 +6,6 @@ */ package org.elasticsearch.cluster.coordination.votingonly; -import org.apache.lucene.tests.util.LuceneTestCase; import org.elasticsearch.Version; import org.elasticsearch.cluster.coordination.AbstractCoordinatorTestCase; import org.elasticsearch.cluster.coordination.ElectionStrategy; @@ -23,7 +22,6 @@ import static java.util.Collections.emptySet; -@LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/92307") public class VotingOnlyNodeCoordinatorTests extends AbstractCoordinatorTestCase { @Override From 149341162ff404f06996ea65a9086013f2e894da Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Tue, 13 Dec 2022 12:49:39 +0100 Subject: [PATCH 250/919] Remove a log statement that points to a resolved issue (#92281) --- .../org/elasticsearch/index/engine/InternalEngine.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java index 2cd30e4dcb2d..9deb51cb6188 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -1965,15 +1965,6 @@ public boolean flush(boolean force, boolean waitIfOngoing) throws EngineExceptio logger.trace("starting commit for flush; commitTranslog=true"); commitIndexWriter(indexWriter, translog); logger.trace("finished commit for flush"); - - // a temporary debugging to investigate test failure - issue#32827. Remove when the issue is resolved - logger.debug( - "new commit on flush, hasUncommittedChanges:{}, force:{}, shouldPeriodicallyFlush:{}", - hasUncommittedChanges, - force, - shouldPeriodicallyFlush - ); - // we need to refresh in order to clear older version values refresh("version_table_flush", SearcherScope.INTERNAL, true); translog.trimUnreferencedReaders(); From 0fbc4007a043708abd4d1e9a3c143914301d2620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Witek?= Date: Tue, 13 Dec 2022 13:28:54 +0100 Subject: [PATCH 251/919] [Transform] Add integration tests for aggregate_metric_double field type used as unique_key in latest transform (#91454) --- .../test/transform/preview_transforms.yml | 20 ++++++++ .../integration/TransformLatestRestIT.java | 46 ++++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/transform/preview_transforms.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/transform/preview_transforms.yml index 70656a3f1ff7..c5a450be4fe9 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/transform/preview_transforms.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/transform/preview_transforms.yml @@ -11,6 +11,10 @@ setup: type: keyword responsetime: type: float + agg_metric: + type: aggregate_metric_double + metrics: [ "min", "max" ] + default_metric: "max" event_rate: type: integer _isDeleted: @@ -638,3 +642,19 @@ setup: "unattended": true } } + +--- +"Test preview transform latest with unique key field of aggregate_metric_double type": + - do: + catch: /Field \[agg_metric\] of type \[aggregate_metric_double\] is not supported for aggregation \[terms\]/ + transform.preview_transform: + body: > + { + "source": { + "index": "airline-data" + }, + "latest": { + "unique_key": ["agg_metric"], + "sort": "time" + } + } diff --git a/x-pack/plugin/transform/qa/single-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformLatestRestIT.java b/x-pack/plugin/transform/qa/single-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformLatestRestIT.java index 579cc549ed03..2a26c2dd25af 100644 --- a/x-pack/plugin/transform/qa/single-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformLatestRestIT.java +++ b/x-pack/plugin/transform/qa/single-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformLatestRestIT.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.Map; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -53,7 +54,6 @@ public void createIndexes() throws IOException { public void testLatestWithAggregateMetricDouble() throws Exception { String transformId = "aggregate_metric_double_latest_transform"; String transformIndex = "aggregate_metric_double_latest_reviews"; - String statsField = "stars_stats"; setupDataAccessRole(DATA_ACCESS_ROLE, REVIEWS_INDEX_NAME, transformIndex); String config = formatted(""" @@ -102,4 +102,48 @@ public void testLatestWithAggregateMetricDouble() throws Exception { assertThat(((List) XContentMapValues.extractValue("hits.hits._source.stars_stats.max", searchResult)).get(0), is(equalTo(3))); assertThat(((List) XContentMapValues.extractValue("hits.hits._source.stars_stats.sum", searchResult)).get(0), is(equalTo(20))); } + + public void testLatestWithAggregateMetricDoubleAsUniqueKey() throws Exception { + String transformId = "aggregate_metric_double_latest_transform"; + String transformIndex = "aggregate_metric_double_latest_reviews"; + String statsField = "stars_stats"; + setupDataAccessRole(DATA_ACCESS_ROLE, REVIEWS_INDEX_NAME, transformIndex); + + String config = formatted(""" + { + "source": { + "index": "%s" + }, + "dest": { + "index": "%s" + }, + "latest": { + "unique_key": [ "%s" ], + "sort": "@timestamp" + } + }""", REVIEWS_INDEX_NAME, transformIndex, statsField); + + { + final Request createPreviewRequest = createRequestWithAuth("POST", getTransformEndpoint() + "_preview", null); + createPreviewRequest.setJsonEntity(config); + Exception e = expectThrows(Exception.class, () -> client().performRequest(createPreviewRequest)); + assertThat( + e.getMessage(), + containsString("Field [stars_stats] of type [aggregate_metric_double] is not supported for aggregation [terms]") + ); + } + { + final Request createTransformRequest = createRequestWithAuth( + "PUT", + getTransformEndpoint() + transformId, + BASIC_AUTH_VALUE_TRANSFORM_ADMIN_WITH_SOME_DATA_ACCESS + ); + createTransformRequest.setJsonEntity(config); + Exception e = expectThrows(Exception.class, () -> client().performRequest(createTransformRequest)); + assertThat( + e.getMessage(), + containsString("Field [stars_stats] of type [aggregate_metric_double] is not supported for aggregation [terms]") + ); + } + } } From 998f0b7984f20807e85f29e2cf96c0750d3480b5 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Tue, 13 Dec 2022 15:06:05 +0000 Subject: [PATCH 252/919] Use primitive types rather than boxing/unboxing for iterating over primitive arrays from defs (#92025) This eliminates boxing in the cases where we're iterating over a primitive array from a def value --- docs/changelog/92025.yaml | 6 + .../java/org/elasticsearch/painless/Def.java | 700 +++++++++++------- .../painless/WriterConstants.java | 10 + .../painless/api/ValueIterator.java | 32 + .../phase/DefaultIRTreeToASMBytesPhase.java | 35 +- .../phase/DefaultUserTreeToIRTreePhase.java | 16 +- .../elasticsearch/painless/ArrayTests.java | 125 ++++ .../painless/DefOptimizationTests.java | 35 + 8 files changed, 691 insertions(+), 268 deletions(-) create mode 100644 docs/changelog/92025.yaml create mode 100644 modules/lang-painless/src/main/java/org/elasticsearch/painless/api/ValueIterator.java diff --git a/docs/changelog/92025.yaml b/docs/changelog/92025.yaml new file mode 100644 index 000000000000..a1ce0d2ee863 --- /dev/null +++ b/docs/changelog/92025.yaml @@ -0,0 +1,6 @@ +pr: 92025 +summary: Use primitive types rather than boxing/unboxing for iterating over primitive + arrays from defs +area: Infra/Scripting +type: enhancement +issues: [] diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Def.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Def.java index 8c424ad1d022..8b45e7d0edd5 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Def.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Def.java @@ -8,6 +8,8 @@ package org.elasticsearch.painless; +import org.elasticsearch.core.Strings; +import org.elasticsearch.painless.api.ValueIterator; import org.elasticsearch.painless.lookup.PainlessLookup; import org.elasticsearch.painless.lookup.PainlessLookupUtility; import org.elasticsearch.painless.lookup.PainlessMethod; @@ -24,6 +26,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -54,8 +57,8 @@ public final class Def { private static final MethodHandle LIST_GET; /** pointer to List.set(int,Object) */ private static final MethodHandle LIST_SET; - /** pointer to Iterable.iterator() */ - private static final MethodHandle ITERATOR; + /** pointer to new ObjectIterator(Iterable.iterator()) */ + private static final MethodHandle OBJECT_ITERATOR; /** pointer to {@link Def#mapIndexNormalize}. */ private static final MethodHandle MAP_INDEX_NORMALIZE; /** pointer to {@link Def#listIndexNormalize}. */ @@ -66,14 +69,17 @@ public final class Def { public static final Map, MethodHandle> DEF_TO_BOXED_TYPE_IMPLICIT_CAST; static { - final MethodHandles.Lookup methodHandlesLookup = MethodHandles.publicLookup(); + final MethodHandles.Lookup methodHandlesLookup = MethodHandles.lookup(); try { MAP_GET = methodHandlesLookup.findVirtual(Map.class, "get", MethodType.methodType(Object.class, Object.class)); MAP_PUT = methodHandlesLookup.findVirtual(Map.class, "put", MethodType.methodType(Object.class, Object.class, Object.class)); LIST_GET = methodHandlesLookup.findVirtual(List.class, "get", MethodType.methodType(Object.class, int.class)); LIST_SET = methodHandlesLookup.findVirtual(List.class, "set", MethodType.methodType(Object.class, int.class, Object.class)); - ITERATOR = methodHandlesLookup.findVirtual(Iterable.class, "iterator", MethodType.methodType(Iterator.class)); + OBJECT_ITERATOR = MethodHandles.filterReturnValue( + methodHandlesLookup.findVirtual(Iterable.class, "iterator", MethodType.methodType(Iterator.class)), + methodHandlesLookup.findConstructor(ObjectIterator.class, MethodType.methodType(void.class, Iterator.class)) + ); MAP_INDEX_NORMALIZE = methodHandlesLookup.findStatic( Def.class, "mapIndexNormalize", @@ -561,9 +567,129 @@ static MethodHandle lookupArrayLoad(Class receiverClass) { ); } + private static ClassCastException castException(Class sourceClass, Class targetClass, Boolean implicit) { + return new ClassCastException( + Strings.format( + "cannot %scast def [%s] to %s", + implicit != null ? (implicit ? "implicitly " : "explicitly ") : "", + PainlessLookupUtility.typeToUnboxedType(sourceClass).getCanonicalName(), + targetClass.getCanonicalName() + ) + ); + } + + private abstract static class BaseIterator implements ValueIterator { + @Override + public boolean nextBoolean() { + Object next = next(); + try { + return (boolean) next; + } catch (ClassCastException e) { + throw castException(next.getClass(), boolean.class, null); + } + } + + @Override + public byte nextByte() { + Object next = next(); + try { + return ((Number) next).byteValue(); + } catch (ClassCastException e) { + throw castException(next.getClass(), byte.class, null); + } + } + + @Override + public short nextShort() { + Object next = next(); + try { + return ((Number) next).shortValue(); + } catch (ClassCastException e) { + throw castException(next.getClass(), short.class, null); + } + } + + @Override + public char nextChar() { + Object next = next(); + try { + return (char) next; + } catch (ClassCastException e) { + throw castException(next.getClass(), char.class, null); + } + } + + @Override + public int nextInt() { + Object next = next(); + try { + return ((Number) next).intValue(); + } catch (ClassCastException e) { + throw castException(next.getClass(), int.class, null); + } + } + + @Override + public long nextLong() { + Object next = next(); + try { + return ((Number) next).longValue(); + } catch (ClassCastException e) { + throw castException(next.getClass(), long.class, null); + } + } + + @Override + public float nextFloat() { + Object next = next(); + try { + return ((Number) next).floatValue(); + } catch (ClassCastException e) { + throw castException(next.getClass(), float.class, null); + } + } + + @Override + public double nextDouble() { + Object next = next(); + try { + return ((Number) next).doubleValue(); + } catch (ClassCastException e) { + throw castException(next.getClass(), double.class, null); + } + } + } + + private static class ObjectIterator extends BaseIterator { + private final Iterator iterator; + + ObjectIterator(Iterator iterator) { + this.iterator = iterator; + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public T next() { + return iterator.next(); + } + + @Override + public void remove() { + iterator.remove(); + } + + @Override + public void forEachRemaining(Consumer action) { + iterator.forEachRemaining(action); + } + } + /** Helper class for isolating MethodHandles and methods to get iterators over arrays - * (to emulate "enhanced for loop" using MethodHandles). These cause boxing, and are not as efficient - * as they could be, but works. + * (to emulate "enhanced for loop" using MethodHandles). */ @SuppressWarnings("unused") // iterator() methods are are actually used, javac just does not know :) private static final class ArrayIteratorHelper { @@ -585,7 +711,7 @@ private static final class ArrayIteratorHelper { return PRIVATE_METHOD_HANDLES_LOOKUP.findStatic( PRIVATE_METHOD_HANDLES_LOOKUP.lookupClass(), "iterator", - MethodType.methodType(Iterator.class, type) + MethodType.methodType(ValueIterator.class, type) ); } catch (ReflectiveOperationException e) { throw new AssertionError(e); @@ -595,8 +721,8 @@ private static final class ArrayIteratorHelper { private static final MethodHandle OBJECT_ARRAY_MH = ARRAY_TYPE_MH_MAPPING.get(Object[].class); - static Iterator iterator(final boolean[] array) { - return new Iterator() { + static ValueIterator iterator(final boolean[] array) { + return new BaseIterator() { int index = 0; @Override @@ -605,14 +731,19 @@ public boolean hasNext() { } @Override - public Boolean next() { + public boolean nextBoolean() { return array[index++]; } + + @Override + public Boolean next() { + return nextBoolean(); + } }; } - static Iterator iterator(final byte[] array) { - return new Iterator() { + static ValueIterator iterator(final byte[] array) { + return new BaseIterator() { int index = 0; @Override @@ -621,14 +752,49 @@ public boolean hasNext() { } @Override - public Byte next() { + public byte nextByte() { return array[index++]; } + + @Override + public short nextShort() { + return nextByte(); + } + + @Override + public char nextChar() { + return (char) nextByte(); + } + + @Override + public int nextInt() { + return nextByte(); + } + + @Override + public long nextLong() { + return nextByte(); + } + + @Override + public float nextFloat() { + return nextByte(); + } + + @Override + public double nextDouble() { + return nextByte(); + } + + @Override + public Byte next() { + return nextByte(); + } }; } - static Iterator iterator(final short[] array) { - return new Iterator() { + static ValueIterator iterator(final short[] array) { + return new BaseIterator() { int index = 0; @Override @@ -637,14 +803,49 @@ public boolean hasNext() { } @Override - public Short next() { + public byte nextByte() { + return (byte) nextShort(); + } + + @Override + public short nextShort() { return array[index++]; } + + @Override + public char nextChar() { + return (char) nextShort(); + } + + @Override + public int nextInt() { + return nextShort(); + } + + @Override + public long nextLong() { + return nextShort(); + } + + @Override + public float nextFloat() { + return nextShort(); + } + + @Override + public double nextDouble() { + return nextShort(); + } + + @Override + public Short next() { + return nextShort(); + } }; } - static Iterator iterator(final int[] array) { - return new Iterator() { + static ValueIterator iterator(final int[] array) { + return new BaseIterator() { int index = 0; @Override @@ -653,14 +854,49 @@ public boolean hasNext() { } @Override - public Integer next() { + public byte nextByte() { + return (byte) nextInt(); + } + + @Override + public short nextShort() { + return (short) nextInt(); + } + + @Override + public char nextChar() { + return (char) nextInt(); + } + + @Override + public int nextInt() { return array[index++]; } + + @Override + public long nextLong() { + return nextInt(); + } + + @Override + public float nextFloat() { + return nextInt(); + } + + @Override + public double nextDouble() { + return nextInt(); + } + + @Override + public Integer next() { + return nextInt(); + } }; } - static Iterator iterator(final long[] array) { - return new Iterator() { + static ValueIterator iterator(final long[] array) { + return new BaseIterator() { int index = 0; @Override @@ -669,14 +905,49 @@ public boolean hasNext() { } @Override - public Long next() { + public byte nextByte() { + return (byte) nextLong(); + } + + @Override + public short nextShort() { + return (short) nextLong(); + } + + @Override + public char nextChar() { + return (char) nextLong(); + } + + @Override + public int nextInt() { + return (int) nextLong(); + } + + @Override + public long nextLong() { return array[index++]; } + + @Override + public float nextFloat() { + return nextLong(); + } + + @Override + public double nextDouble() { + return nextLong(); + } + + @Override + public Long next() { + return nextLong(); + } }; } - static Iterator iterator(final char[] array) { - return new Iterator() { + static ValueIterator iterator(final char[] array) { + return new BaseIterator() { int index = 0; @Override @@ -685,14 +956,49 @@ public boolean hasNext() { } @Override - public Character next() { + public byte nextByte() { + return (byte) nextChar(); + } + + @Override + public short nextShort() { + return (short) nextChar(); + } + + @Override + public char nextChar() { return array[index++]; } + + @Override + public int nextInt() { + return nextChar(); + } + + @Override + public long nextLong() { + return nextChar(); + } + + @Override + public float nextFloat() { + return nextChar(); + } + + @Override + public double nextDouble() { + return nextChar(); + } + + @Override + public Character next() { + return nextChar(); + } }; } - static Iterator iterator(final float[] array) { - return new Iterator() { + static ValueIterator iterator(final float[] array) { + return new BaseIterator() { int index = 0; @Override @@ -701,14 +1007,49 @@ public boolean hasNext() { } @Override - public Float next() { + public byte nextByte() { + return (byte) nextFloat(); + } + + @Override + public short nextShort() { + return (short) nextFloat(); + } + + @Override + public char nextChar() { + return (char) nextFloat(); + } + + @Override + public int nextInt() { + return (int) nextFloat(); + } + + @Override + public long nextLong() { + return (long) nextFloat(); + } + + @Override + public float nextFloat() { return array[index++]; } + + @Override + public double nextDouble() { + return nextFloat(); + } + + @Override + public Float next() { + return nextFloat(); + } }; } - static Iterator iterator(final double[] array) { - return new Iterator() { + static ValueIterator iterator(final double[] array) { + return new BaseIterator() { int index = 0; @Override @@ -717,14 +1058,49 @@ public boolean hasNext() { } @Override - public Double next() { + public byte nextByte() { + return (byte) nextDouble(); + } + + @Override + public short nextShort() { + return (short) nextDouble(); + } + + @Override + public char nextChar() { + return (char) nextDouble(); + } + + @Override + public int nextInt() { + return (int) nextDouble(); + } + + @Override + public long nextLong() { + return (long) nextDouble(); + } + + @Override + public float nextFloat() { + return (float) nextDouble(); + } + + @Override + public double nextDouble() { return array[index++]; } + + @Override + public Double next() { + return nextDouble(); + } }; } - static Iterator iterator(final Object[] array) { - return new Iterator() { + static ValueIterator iterator(final Object[] array) { + return new BaseIterator() { int index = 0; @Override @@ -757,7 +1133,7 @@ private ArrayIteratorHelper() {} */ static MethodHandle lookupIterator(Class receiverClass) { if (Iterable.class.isAssignableFrom(receiverClass)) { - return ITERATOR; + return OBJECT_ITERATOR; } else if (receiverClass.isArray()) { return ArrayIteratorHelper.newIterator(receiverClass); } else { @@ -771,13 +1147,7 @@ public static boolean defToboolean(final Object value) { if (value instanceof Boolean) { return (boolean) value; } else { - throw new ClassCastException( - "cannot cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + boolean.class.getCanonicalName() - ); + throw castException(value.getClass(), boolean.class, null); } } @@ -785,13 +1155,7 @@ public static byte defTobyteImplicit(final Object value) { if (value instanceof Byte) { return (byte) value; } else { - throw new ClassCastException( - "cannot implicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + byte.class.getCanonicalName() - ); + throw castException(value.getClass(), byte.class, true); } } @@ -801,13 +1165,7 @@ public static short defToshortImplicit(final Object value) { } else if (value instanceof Short) { return (short) value; } else { - throw new ClassCastException( - "cannot implicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + short.class.getCanonicalName() - ); + throw castException(value.getClass(), short.class, true); } } @@ -815,13 +1173,7 @@ public static char defTocharImplicit(final Object value) { if (value instanceof Character) { return (char) value; } else { - throw new ClassCastException( - "cannot implicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + char.class.getCanonicalName() - ); + throw castException(value.getClass(), char.class, true); } } @@ -835,13 +1187,7 @@ public static int defTointImplicit(final Object value) { } else if (value instanceof Integer) { return (int) value; } else { - throw new ClassCastException( - "cannot implicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + int.class.getCanonicalName() - ); + throw castException(value.getClass(), int.class, true); } } @@ -857,13 +1203,7 @@ public static long defTolongImplicit(final Object value) { } else if (value instanceof Long) { return (long) value; } else { - throw new ClassCastException( - "cannot implicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + long.class.getCanonicalName() - ); + throw castException(value.getClass(), long.class, true); } } @@ -881,13 +1221,7 @@ public static float defTofloatImplicit(final Object value) { } else if (value instanceof Float) { return (float) value; } else { - throw new ClassCastException( - "cannot implicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + float.class.getCanonicalName() - ); + throw castException(value.getClass(), float.class, true); } } @@ -907,13 +1241,7 @@ public static double defTodoubleImplicit(final Object value) { } else if (value instanceof Double) { return (double) value; } else { - throw new ClassCastException( - "cannot implicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + double.class.getCanonicalName() - ); + throw castException(value.getClass(), double.class, true); } } @@ -928,13 +1256,7 @@ public static byte defTobyteExplicit(final Object value) { || value instanceof Double) { return ((Number) value).byteValue(); } else { - throw new ClassCastException( - "cannot explicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + byte.class.getCanonicalName() - ); + throw castException(value.getClass(), byte.class, false); } } @@ -949,13 +1271,7 @@ public static short defToshortExplicit(final Object value) { || value instanceof Double) { return ((Number) value).shortValue(); } else { - throw new ClassCastException( - "cannot explicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + short.class.getCanonicalName() - ); + throw castException(value.getClass(), short.class, false); } } @@ -972,13 +1288,7 @@ public static char defTocharExplicit(final Object value) { || value instanceof Double) { return (char) ((Number) value).intValue(); } else { - throw new ClassCastException( - "cannot explicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + char.class.getCanonicalName() - ); + throw castException(value.getClass(), char.class, false); } } @@ -993,13 +1303,7 @@ public static int defTointExplicit(final Object value) { || value instanceof Double) { return ((Number) value).intValue(); } else { - throw new ClassCastException( - "cannot explicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + int.class.getCanonicalName() - ); + throw castException(value.getClass(), int.class, false); } } @@ -1014,13 +1318,7 @@ public static long defTolongExplicit(final Object value) { || value instanceof Double) { return ((Number) value).longValue(); } else { - throw new ClassCastException( - "cannot explicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + long.class.getCanonicalName() - ); + throw castException(value.getClass(), long.class, false); } } @@ -1035,13 +1333,7 @@ public static float defTofloatExplicit(final Object value) { || value instanceof Double) { return ((Number) value).floatValue(); } else { - throw new ClassCastException( - "cannot explicitly cast " - + "float [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + byte.class.getCanonicalName() - ); + throw castException(value.getClass(), float.class, false); } } @@ -1056,13 +1348,7 @@ public static double defTodoubleExplicit(final Object value) { || value instanceof Double) { return ((Number) value).doubleValue(); } else { - throw new ClassCastException( - "cannot explicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + byte.class.getCanonicalName() - ); + throw castException(value.getClass(), byte.class, false); } } @@ -1074,13 +1360,7 @@ public static Boolean defToBoolean(final Object value) { } else if (value instanceof Boolean) { return (Boolean) value; } else { - throw new ClassCastException( - "cannot implicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + Boolean.class.getCanonicalName() - ); + throw castException(value.getClass(), Boolean.class, false); } } @@ -1090,13 +1370,7 @@ public static Byte defToByteImplicit(final Object value) { } else if (value instanceof Byte) { return (Byte) value; } else { - throw new ClassCastException( - "cannot implicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + Byte.class.getCanonicalName() - ); + throw castException(value.getClass(), Byte.class, false); } } @@ -1108,13 +1382,7 @@ public static Short defToShortImplicit(final Object value) { } else if (value instanceof Short) { return (Short) value; } else { - throw new ClassCastException( - "cannot implicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + Short.class.getCanonicalName() - ); + throw castException(value.getClass(), Short.class, false); } } @@ -1124,13 +1392,7 @@ public static Character defToCharacterImplicit(final Object value) { } else if (value instanceof Character) { return (Character) value; } else { - throw new ClassCastException( - "cannot implicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + Character.class.getCanonicalName() - ); + throw castException(value.getClass(), Character.class, false); } } @@ -1146,13 +1408,7 @@ public static Integer defToIntegerImplicit(final Object value) { } else if (value instanceof Integer) { return (Integer) value; } else { - throw new ClassCastException( - "cannot implicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + Integer.class.getCanonicalName() - ); + throw castException(value.getClass(), Integer.class, false); } } @@ -1170,13 +1426,7 @@ public static Long defToLongImplicit(final Object value) { } else if (value instanceof Long) { return (Long) value; } else { - throw new ClassCastException( - "cannot implicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + Long.class.getCanonicalName() - ); + throw castException(value.getClass(), Long.class, false); } } @@ -1196,13 +1446,7 @@ public static Float defToFloatImplicit(final Object value) { } else if (value instanceof Float) { return (Float) value; } else { - throw new ClassCastException( - "cannot implicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + Float.class.getCanonicalName() - ); + throw castException(value.getClass(), Float.class, false); } } @@ -1224,13 +1468,7 @@ public static Double defToDoubleImplicit(final Object value) { } else if (value instanceof Double) { return (Double) value; } else { - throw new ClassCastException( - "cannot implicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + Double.class.getCanonicalName() - ); + throw castException(value.getClass(), Double.class, false); } } @@ -1247,13 +1485,7 @@ public static Byte defToByteExplicit(final Object value) { || value instanceof Double) { return ((Number) value).byteValue(); } else { - throw new ClassCastException( - "cannot explicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + Byte.class.getCanonicalName() - ); + throw castException(value.getClass(), Byte.class, false); } } @@ -1270,13 +1502,7 @@ public static Short defToShortExplicit(final Object value) { || value instanceof Double) { return ((Number) value).shortValue(); } else { - throw new ClassCastException( - "cannot explicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + Short.class.getCanonicalName() - ); + throw castException(value.getClass(), Short.class, false); } } @@ -1295,13 +1521,7 @@ public static Character defToCharacterExplicit(final Object value) { || value instanceof Double) { return (char) ((Number) value).intValue(); } else { - throw new ClassCastException( - "cannot explicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + Character.class.getCanonicalName() - ); + throw castException(value.getClass(), Character.class, false); } } @@ -1318,13 +1538,7 @@ public static Integer defToIntegerExplicit(final Object value) { || value instanceof Double) { return ((Number) value).intValue(); } else { - throw new ClassCastException( - "cannot explicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + Integer.class.getCanonicalName() - ); + throw castException(value.getClass(), Integer.class, false); } } @@ -1341,13 +1555,7 @@ public static Long defToLongExplicit(final Object value) { || value instanceof Double) { return ((Number) value).longValue(); } else { - throw new ClassCastException( - "cannot explicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + Long.class.getCanonicalName() - ); + throw castException(value.getClass(), Long.class, false); } } @@ -1364,13 +1572,7 @@ public static Float defToFloatExplicit(final Object value) { || value instanceof Double) { return ((Number) value).floatValue(); } else { - throw new ClassCastException( - "cannot explicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + Float.class.getCanonicalName() - ); + throw castException(value.getClass(), Float.class, false); } } @@ -1387,13 +1589,7 @@ public static Double defToDoubleExplicit(final Object value) { || value instanceof Double) { return ((Number) value).doubleValue(); } else { - throw new ClassCastException( - "cannot explicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + Double.class.getCanonicalName() - ); + throw castException(value.getClass(), Double.class, false); } } @@ -1403,13 +1599,7 @@ public static String defToStringImplicit(final Object value) { } else if (value instanceof String) { return (String) value; } else { - throw new ClassCastException( - "cannot implicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + String.class.getCanonicalName() - ); + throw castException(value.getClass(), String.class, true); } } @@ -1421,13 +1611,7 @@ public static String defToStringExplicit(final Object value) { } else if (value instanceof String) { return (String) value; } else { - throw new ClassCastException( - "cannot explicitly cast " - + "def [" - + PainlessLookupUtility.typeToUnboxedType(value.getClass()).getCanonicalName() - + "] to " - + String.class.getCanonicalName() - ); + throw castException(value.getClass(), String.class, false); } } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterConstants.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterConstants.java index 26ead4361605..9207e0a55673 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterConstants.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/WriterConstants.java @@ -8,6 +8,7 @@ package org.elasticsearch.painless; +import org.elasticsearch.painless.api.ValueIterator; import org.objectweb.asm.Handle; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; @@ -48,8 +49,17 @@ public final class WriterConstants { public static final MethodType NEEDS_PARAMETER_METHOD_TYPE = MethodType.methodType(boolean.class); public static final Type ITERATOR_TYPE = Type.getType(Iterator.class); + public static final Type VALUE_ITERATOR_TYPE = Type.getType(ValueIterator.class); public static final Method ITERATOR_HASNEXT = getAsmMethod(boolean.class, "hasNext"); public static final Method ITERATOR_NEXT = getAsmMethod(Object.class, "next"); + public static final Method VALUE_ITERATOR_NEXT_BOOLEAN = getAsmMethod(boolean.class, "nextBoolean"); + public static final Method VALUE_ITERATOR_NEXT_BYTE = getAsmMethod(byte.class, "nextByte"); + public static final Method VALUE_ITERATOR_NEXT_SHORT = getAsmMethod(short.class, "nextShort"); + public static final Method VALUE_ITERATOR_NEXT_CHAR = getAsmMethod(char.class, "nextChar"); + public static final Method VALUE_ITERATOR_NEXT_INT = getAsmMethod(int.class, "nextInt"); + public static final Method VALUE_ITERATOR_NEXT_LONG = getAsmMethod(long.class, "nextLong"); + public static final Method VALUE_ITERATOR_NEXT_FLOAT = getAsmMethod(float.class, "nextFloat"); + public static final Method VALUE_ITERATOR_NEXT_DOUBLE = getAsmMethod(double.class, "nextDouble"); public static final Type UTILITY_TYPE = Type.getType(Utility.class); public static final Method STRING_TO_CHAR = getAsmMethod(char.class, "StringTochar", String.class); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/api/ValueIterator.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/api/ValueIterator.java new file mode 100644 index 000000000000..7f366bc3284a --- /dev/null +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/api/ValueIterator.java @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.painless.api; + +import java.util.Iterator; + +/** + * An {@link Iterator} that can return primitive values + */ +public interface ValueIterator extends Iterator { + boolean nextBoolean(); + + byte nextByte(); + + short nextShort(); + + char nextChar(); + + int nextInt(); + + long nextLong(); + + float nextFloat(); + + double nextDouble(); +} diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultIRTreeToASMBytesPhase.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultIRTreeToASMBytesPhase.java index f0c5f4fe8b14..084d5ee8e6c3 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultIRTreeToASMBytesPhase.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultIRTreeToASMBytesPhase.java @@ -17,6 +17,7 @@ import org.elasticsearch.painless.ScriptClassInfo; import org.elasticsearch.painless.WriterConstants; import org.elasticsearch.painless.api.Augmentation; +import org.elasticsearch.painless.api.ValueIterator; import org.elasticsearch.painless.ir.BinaryImplNode; import org.elasticsearch.painless.ir.BinaryMathNode; import org.elasticsearch.painless.ir.BlockNode; @@ -164,7 +165,6 @@ import java.util.ArrayList; import java.util.BitSet; import java.util.Collections; -import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; @@ -175,6 +175,15 @@ import static org.elasticsearch.painless.WriterConstants.ITERATOR_NEXT; import static org.elasticsearch.painless.WriterConstants.ITERATOR_TYPE; import static org.elasticsearch.painless.WriterConstants.OBJECTS_TYPE; +import static org.elasticsearch.painless.WriterConstants.VALUE_ITERATOR_NEXT_BOOLEAN; +import static org.elasticsearch.painless.WriterConstants.VALUE_ITERATOR_NEXT_BYTE; +import static org.elasticsearch.painless.WriterConstants.VALUE_ITERATOR_NEXT_CHAR; +import static org.elasticsearch.painless.WriterConstants.VALUE_ITERATOR_NEXT_DOUBLE; +import static org.elasticsearch.painless.WriterConstants.VALUE_ITERATOR_NEXT_FLOAT; +import static org.elasticsearch.painless.WriterConstants.VALUE_ITERATOR_NEXT_INT; +import static org.elasticsearch.painless.WriterConstants.VALUE_ITERATOR_NEXT_LONG; +import static org.elasticsearch.painless.WriterConstants.VALUE_ITERATOR_NEXT_SHORT; +import static org.elasticsearch.painless.WriterConstants.VALUE_ITERATOR_TYPE; public class DefaultIRTreeToASMBytesPhase implements IRTreeVisitor { @@ -552,6 +561,8 @@ public void visitForEachSubIterableLoop(ForEachSubIterableNode irForEachSubItera MethodWriter methodWriter = writeScope.getMethodWriter(); methodWriter.writeStatementOffset(irForEachSubIterableNode.getLocation()); + PainlessMethod painlessMethod = irForEachSubIterableNode.getDecorationValue(IRDMethod.class); + Variable variable = writeScope.defineVariable( irForEachSubIterableNode.getDecorationValue(IRDVariableType.class), irForEachSubIterableNode.getDecorationValue(IRDVariableName.class) @@ -563,10 +574,8 @@ public void visitForEachSubIterableLoop(ForEachSubIterableNode irForEachSubItera visit(irForEachSubIterableNode.getConditionNode(), writeScope); - PainlessMethod painlessMethod = irForEachSubIterableNode.getDecorationValue(IRDMethod.class); - if (painlessMethod == null) { - Type methodType = Type.getMethodType(Type.getType(Iterator.class), Type.getType(Object.class)); + Type methodType = Type.getMethodType(Type.getType(ValueIterator.class), Type.getType(Object.class)); methodWriter.invokeDefCall("iterator", methodType, DefBootstrap.ITERATOR); } else { methodWriter.invokeMethodCall(painlessMethod); @@ -584,8 +593,22 @@ public void visitForEachSubIterableLoop(ForEachSubIterableNode irForEachSubItera methodWriter.ifZCmp(MethodWriter.EQ, end); methodWriter.visitVarInsn(iterator.getAsmType().getOpcode(Opcodes.ILOAD), iterator.getSlot()); - methodWriter.invokeInterface(ITERATOR_TYPE, ITERATOR_NEXT); - methodWriter.writeCast(irForEachSubIterableNode.getDecorationValue(IRDCast.class)); + if (painlessMethod != null || variable.getType().isPrimitive() == false) { + methodWriter.invokeInterface(ITERATOR_TYPE, ITERATOR_NEXT); + methodWriter.writeCast(irForEachSubIterableNode.getDecorationValue(IRDCast.class)); + } else { + switch (variable.getAsmType().getSort()) { + case Type.BOOLEAN -> methodWriter.invokeInterface(VALUE_ITERATOR_TYPE, VALUE_ITERATOR_NEXT_BOOLEAN); + case Type.BYTE -> methodWriter.invokeInterface(VALUE_ITERATOR_TYPE, VALUE_ITERATOR_NEXT_BYTE); + case Type.SHORT -> methodWriter.invokeInterface(VALUE_ITERATOR_TYPE, VALUE_ITERATOR_NEXT_SHORT); + case Type.CHAR -> methodWriter.invokeInterface(VALUE_ITERATOR_TYPE, VALUE_ITERATOR_NEXT_CHAR); + case Type.INT -> methodWriter.invokeInterface(VALUE_ITERATOR_TYPE, VALUE_ITERATOR_NEXT_INT); + case Type.LONG -> methodWriter.invokeInterface(VALUE_ITERATOR_TYPE, VALUE_ITERATOR_NEXT_LONG); + case Type.FLOAT -> methodWriter.invokeInterface(VALUE_ITERATOR_TYPE, VALUE_ITERATOR_NEXT_FLOAT); + case Type.DOUBLE -> methodWriter.invokeInterface(VALUE_ITERATOR_TYPE, VALUE_ITERATOR_NEXT_DOUBLE); + default -> throw new IllegalArgumentException("Unknown primitive iteration variable type " + variable.getAsmType()); + } + } methodWriter.visitVarInsn(variable.getAsmType().getOpcode(Opcodes.ISTORE), variable.getSlot()); visit(irForEachSubIterableNode.getBlockNode(), writeScope.newLoopScope(begin, end)); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultUserTreeToIRTreePhase.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultUserTreeToIRTreePhase.java index 4d34b68c1eaf..4739f7682ef3 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultUserTreeToIRTreePhase.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultUserTreeToIRTreePhase.java @@ -15,6 +15,7 @@ import org.elasticsearch.painless.MethodWriter; import org.elasticsearch.painless.Operation; import org.elasticsearch.painless.WriterConstants; +import org.elasticsearch.painless.api.ValueIterator; import org.elasticsearch.painless.ir.BinaryImplNode; import org.elasticsearch.painless.ir.BinaryMathNode; import org.elasticsearch.painless.ir.BlockNode; @@ -759,17 +760,24 @@ public void visitEach(SEach userEachNode, ScriptScope scriptScope) { irForEachSubIterableNode.setBlockNode(irBlockNode); irForEachSubIterableNode.attachDecoration(new IRDVariableType(variable.type())); irForEachSubIterableNode.attachDecoration(new IRDVariableName(variable.name())); - irForEachSubIterableNode.attachDecoration(new IRDIterableType(Iterator.class)); irForEachSubIterableNode.attachDecoration(new IRDIterableName("#itr" + userEachNode.getLocation().getOffset())); if (iterableValueType != def.class) { + irForEachSubIterableNode.attachDecoration(new IRDIterableType(Iterator.class)); irForEachSubIterableNode.attachDecoration( new IRDMethod(scriptScope.getDecoration(userEachNode, IterablePainlessMethod.class).iterablePainlessMethod()) ); - } - if (painlessCast != null) { - irForEachSubIterableNode.attachDecoration(new IRDCast(painlessCast)); + if (painlessCast != null) { + irForEachSubIterableNode.attachDecoration(new IRDCast(painlessCast)); + } + } else { + // use ValueIterator as we could be iterating over an array directly + irForEachSubIterableNode.attachDecoration(new IRDIterableType(ValueIterator.class)); + + if (painlessCast != null && variable.type().isPrimitive() == false) { + irForEachSubIterableNode.attachDecoration(new IRDCast(painlessCast)); + } } irConditionNode = irForEachSubIterableNode; diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/ArrayTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/ArrayTests.java index fbdce63036d5..5ab1a154ade5 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/ArrayTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/ArrayTests.java @@ -79,4 +79,129 @@ public void testForLoop() { public void testDivideArray() { assertEquals(1, exec("def[] x = new def[1]; x[0] = 2; return x[0] / 2")); } + + public void testPrimitiveIteration() { + assertEquals(true, exec("def x = new boolean[] { true, false }; boolean s = false; for (boolean l : x) s |= l; return s")); + expectScriptThrows( + ClassCastException.class, + () -> exec("def x = new boolean[] { true, false }; short s = 0; for (short l : x) s += l; return s") + ); + expectScriptThrows( + ClassCastException.class, + () -> exec("def x = new boolean[] { true, false }; char s = 0; for (char l : x) s = l; return s") + ); + expectScriptThrows( + ClassCastException.class, + () -> exec("def x = new boolean[] { true, false }; int s = 0; for (int l : x) s += l; return s") + ); + expectScriptThrows( + ClassCastException.class, + () -> exec("def x = new boolean[] { true, false }; long s = 0; for (long l : x) s += l; return s") + ); + expectScriptThrows( + ClassCastException.class, + () -> exec("def x = new boolean[] { true, false }; float s = 0; for (float l : x) s += l; return s") + ); + expectScriptThrows( + ClassCastException.class, + () -> exec("def x = new boolean[] { true, false }; double s = 0; for (double l : x) s += l; return s") + ); + assertEquals(true, exec("def x = new boolean[] { true, false }; boolean s = false; for (def l : x) s |= l; return s")); + + assertEquals((byte) 30, exec("def x = new byte[] { (byte)10, (byte)20 }; byte s = 0; for (byte l : x) s += l; return s")); + assertEquals((short) 30, exec("def x = new byte[] { (byte)10, (byte)20 }; short s = 0; for (short l : x) s += l; return s")); + assertEquals((char) 20, exec("def x = new byte[] { (byte)10, (byte)20 }; char s = 0; for (char l : x) s = l; return s")); + assertEquals(30, exec("def x = new byte[] { (byte)10, (byte)20 }; int s = 0; for (int l : x) s += l; return s")); + assertEquals(30L, exec("def x = new byte[] { (byte)10, (byte)20 }; long s = 0; for (long l : x) s += l; return s")); + assertEquals(30f, exec("def x = new byte[] { (byte)10, (byte)20 }; float s = 0; for (float l : x) s += l; return s")); + assertEquals(30d, exec("def x = new byte[] { (byte)10, (byte)20 }; double s = 0; for (double l : x) s += l; return s")); + assertEquals((byte) 30, exec("def x = new byte[] { (byte)10, (byte)20 }; byte s = 0; for (def l : x) s += l; return s")); + + assertEquals((byte) 30, exec("def x = new short[] { (short)10, (short)20 }; byte s = 0; for (byte l : x) s += l; return s")); + assertEquals((short) 300, exec("def x = new short[] { (short)100, (short)200 }; short s = 0; for (short l : x) s += l; return s")); + assertEquals((char) 200, exec("def x = new short[] { (short)100, (short)200 }; char s = 0; for (char l : x) s = l; return s")); + assertEquals(300, exec("def x = new short[] { (short)100, (short)200 }; int s = 0; for (int l : x) s += l; return s")); + assertEquals(300L, exec("def x = new short[] { (short)100, (short)200 }; long s = 0; for (long l : x) s += l; return s")); + assertEquals(300f, exec("def x = new short[] { (short)100, (short)200 }; float s = 0; for (float l : x) s += l; return s")); + assertEquals(300d, exec("def x = new short[] { (short)100, (short)200 }; double s = 0; for (double l : x) s += l; return s")); + assertEquals((short) 300, exec("def x = new short[] { (short)100, (short)200 }; short s = 0; for (def l : x) s += l; return s")); + + assertEquals((byte) 'b', exec("def x = new char[] { (char)'a', (char)'b' }; byte s = 0; for (byte l : x) s = l; return s")); + assertEquals((short) 'b', exec("def x = new char[] { (char)'a', (char)'b' }; short s = 0; for (short l : x) s = l; return s")); + assertEquals('b', exec("def x = new char[] { (char)'a', (char)'b' }; char s = 0; for (char l : x) s = l; return s")); + assertEquals((int) 'b', exec("def x = new char[] { (char)'a', (char)'b' }; int s = 0; for (int l : x) s = l; return s")); + assertEquals((long) 'b', exec("def x = new char[] { (char)'a', (char)'b' }; long s = 0; for (long l : x) s = l; return s")); + assertEquals((float) 'b', exec("def x = new char[] { (char)'a', (char)'b' }; float s = 0; for (float l : x) s = l; return s")); + assertEquals((double) 'b', exec("def x = new char[] { (char)'a', (char)'b' }; double s = 0; for (double l : x) s = l; return s")); + assertEquals('b', exec("def x = new char[] { (char)'a', (char)'b' }; char s = 0; for (def l : x) s = l; return s")); + + assertEquals((byte) 30, exec("def x = new int[] { 10, 20 }; byte s = 0; for (byte l : x) s += l; return s")); + assertEquals((short) 300, exec("def x = new int[] { 100, 200 }; short s = 0; for (short l : x) s += l; return s")); + assertEquals((char) 200, exec("def x = new int[] { 100, 200 }; char s = 0; for (char l : x) s = l; return s")); + assertEquals(300, exec("def x = new int[] { 100, 200 }; int s = 0; for (int l : x) s += l; return s")); + assertEquals(300L, exec("def x = new int[] { 100, 200 }; long s = 0; for (long l : x) s += l; return s")); + assertEquals(300f, exec("def x = new int[] { 100, 200 }; float s = 0; for (float l : x) s += l; return s")); + assertEquals(300d, exec("def x = new int[] { 100, 200 }; double s = 0; for (double l : x) s += l; return s")); + assertEquals(300, exec("def x = new int[] { 100, 200 }; int s = 0; for (def l : x) s += l; return s")); + + assertEquals((byte) 30, exec("def x = new long[] { 10, 20 }; byte s = 0; for (byte l : x) s += l; return s")); + assertEquals((short) 300, exec("def x = new long[] { 100, 200 }; short s = 0; for (short l : x) s += l; return s")); + assertEquals((char) 200, exec("def x = new long[] { 100, 200 }; char s = 0; for (char l : x) s = l; return s")); + assertEquals(300, exec("def x = new long[] { 100, 200 }; int s = 0; for (int l : x) s += l; return s")); + assertEquals(300L, exec("def x = new long[] { 100, 200 }; long s = 0; for (long l : x) s += l; return s")); + assertEquals(300f, exec("def x = new long[] { 100, 200 }; float s = 0; for (float l : x) s += l; return s")); + assertEquals(300d, exec("def x = new long[] { 100, 200 }; double s = 0; for (double l : x) s += l; return s")); + assertEquals(300L, exec("def x = new long[] { 100, 200 }; long s = 0; for (def l : x) s += l; return s")); + + assertEquals((byte) 30, exec("def x = new float[] { 10, 20 }; byte s = 0; for (byte l : x) s += l; return s")); + assertEquals((short) 300, exec("def x = new float[] { 100, 200 }; short s = 0; for (short l : x) s += l; return s")); + assertEquals((char) 200, exec("def x = new float[] { 100, 200 }; char s = 0; for (char l : x) s = l; return s")); + assertEquals(300, exec("def x = new float[] { 100, 200 }; int s = 0; for (int l : x) s += l; return s")); + assertEquals(300L, exec("def x = new float[] { 100, 200 }; long s = 0; for (long l : x) s += l; return s")); + assertEquals(300f, exec("def x = new float[] { 100, 200 }; float s = 0; for (float l : x) s += l; return s")); + assertEquals(300d, exec("def x = new float[] { 100, 200 }; double s = 0; for (double l : x) s += l; return s")); + assertEquals(300f, exec("def x = new float[] { 100, 200 }; float s = 0; for (def l : x) s += l; return s")); + + assertEquals((byte) 30, exec("def x = new double[] { 10, 20 }; byte s = 0; for (byte l : x) s += l; return s")); + assertEquals((short) 300, exec("def x = new double[] { 100, 200 }; short s = 0; for (short l : x) s += l; return s")); + assertEquals((char) 200, exec("def x = new double[] { 100, 200 }; char s = 0; for (char l : x) s = l; return s")); + assertEquals(300, exec("def x = new double[] { 100, 200 }; int s = 0; for (int l : x) s += l; return s")); + assertEquals(300L, exec("def x = new double[] { 100, 200 }; long s = 0; for (long l : x) s += l; return s")); + assertEquals(300f, exec("def x = new double[] { 100, 200 }; float s = 0; for (float l : x) s += l; return s")); + assertEquals(300d, exec("def x = new double[] { 100, 200 }; double s = 0; for (double l : x) s += l; return s")); + assertEquals(300d, exec("def x = new double[] { 100, 200 }; double s = 0; for (def l : x) s += l; return s")); + + expectScriptThrows( + ClassCastException.class, + () -> exec("def x = new String[] { 'foo', 'bar' }; boolean s = false; for (boolean l : x) s |= l; return s") + ); + expectScriptThrows( + ClassCastException.class, + () -> exec("def x = new String[] { 'foo', 'bar' }; byte s = 0; for (byte l : x) s += l; return s") + ); + expectScriptThrows( + ClassCastException.class, + () -> exec("def x = new String[] { 'foo', 'bar' }; short s = 0; for (short l : x) s += l; return s") + ); + expectScriptThrows( + ClassCastException.class, + () -> exec("def x = new String[] { 'foo', 'bar' }; char s = 0; for (char l : x) s = l; return s") + ); + expectScriptThrows( + ClassCastException.class, + () -> exec("def x = new String[] { 'foo', 'bar' }; int s = 0; for (int l : x) s += l; return s") + ); + expectScriptThrows( + ClassCastException.class, + () -> exec("def x = new String[] { 'foo', 'bar' }; long s = 0; for (long l : x) s += l; return s") + ); + expectScriptThrows( + ClassCastException.class, + () -> exec("def x = new String[] { 'foo', 'bar' }; float s = 0; for (float l : x) s += l; return s") + ); + expectScriptThrows( + ClassCastException.class, + () -> exec("def x = new String[] { 'foo', 'bar' }; double s = 0; for (double l : x) s += l; return s") + ); + } } diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/DefOptimizationTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/DefOptimizationTests.java index 5973cf8b3728..ffbe91bd4603 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/DefOptimizationTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/DefOptimizationTests.java @@ -416,4 +416,39 @@ public void testLambdaArguments() { "synthetic lambda$synthetic$0(D)D" ); } + + public void testPrimitiveArrayIteration() { + assertBytecodeExists( + "def x = new boolean[] { true, false }; boolean s = false; for (boolean l : x) s |= l; return s", + "INVOKEINTERFACE org/elasticsearch/painless/api/ValueIterator.nextBoolean ()Z" + ); + assertBytecodeExists( + "def x = new byte[] { (byte)10, (byte)20 }; byte s = 0; for (byte l : x) s += l; return s", + "INVOKEINTERFACE org/elasticsearch/painless/api/ValueIterator.nextByte ()B" + ); + assertBytecodeExists( + "def x = new short[] { (short)100, (short)200 }; short s = 0; for (short l : x) s += l; return s", + "INVOKEINTERFACE org/elasticsearch/painless/api/ValueIterator.nextShort ()S" + ); + assertBytecodeExists( + "def x = new char[] { (char)'a', (char)'b' }; char s = 0; for (char l : x) s = l; return s", + "INVOKEINTERFACE org/elasticsearch/painless/api/ValueIterator.nextChar ()C" + ); + assertBytecodeExists( + "def x = new int[] { 100, 200 }; int s = 0; for (int l : x) s += l; return s", + "INVOKEINTERFACE org/elasticsearch/painless/api/ValueIterator.nextInt ()I" + ); + assertBytecodeExists( + "def x = new long[] { 100, 200 }; long s = 0; for (long l : x) s += l; return s", + "INVOKEINTERFACE org/elasticsearch/painless/api/ValueIterator.nextLong ()J" + ); + assertBytecodeExists( + "def x = new float[] { 100, 200 }; float s = 0; for (float l : x) s += l; return s", + "INVOKEINTERFACE org/elasticsearch/painless/api/ValueIterator.nextFloat ()F" + ); + assertBytecodeExists( + "def x = new double[] { 100, 200 }; double s = 0; for (double l : x) s += l; return s", + "INVOKEINTERFACE org/elasticsearch/painless/api/ValueIterator.nextDouble ()D" + ); + } } From 3ba16aff6aafea691a388db72133ad474b4591c8 Mon Sep 17 00:00:00 2001 From: Keith Massey Date: Tue, 13 Dec 2022 09:25:25 -0600 Subject: [PATCH 253/919] Preventing ILM and SLM runtime state from being stored in a snapshot (#92252) --- docs/changelog/92252.yaml | 5 + .../xpack/core/XPackClientPlugin.java | 7 + .../core/ilm/IndexLifecycleMetadata.java | 4 + .../core/ilm/LifecycleOperationMetadata.java | 202 ++++++++++++++++++ .../core/slm/SnapshotLifecycleMetadata.java | 5 + .../core/slm/action/GetSLMStatusAction.java | 6 +- .../ilm/LifecycleOperationMetadataTests.java | 67 ++++++ .../IndexLifecycleInitialisationTests.java | 11 +- .../SnapshotLifecycleInitialisationTests.java | 5 +- ...adataMigrateToDataTiersRoutingService.java | 7 +- .../xpack/ilm/IlmHealthIndicatorService.java | 20 +- .../xpack/ilm/IndexLifecycle.java | 6 + .../xpack/ilm/IndexLifecycleService.java | 23 +- .../xpack/ilm/OperationModeUpdateTask.java | 47 ++-- .../TransportDeleteLifecycleAction.java | 4 +- .../ilm/action/TransportGetStatusAction.java | 14 +- .../TransportMigrateToDataTiersAction.java | 7 +- .../action/TransportPutLifecycleAction.java | 3 +- .../xpack/slm/SlmHealthIndicatorService.java | 21 +- .../xpack/slm/SnapshotLifecycleService.java | 15 +- .../xpack/slm/SnapshotLifecycleTask.java | 7 +- .../slm/UpdateSnapshotLifecycleStatsTask.java | 3 +- ...ransportDeleteSnapshotLifecycleAction.java | 4 +- .../action/TransportGetSLMStatusAction.java | 14 +- .../TransportPutSnapshotLifecycleAction.java | 7 +- .../ilm/LifecycleOperationSnapshotTests.java | 137 ++++++++++++ .../ilm/OperationModeUpdateTaskTests.java | 111 ++++++++-- 27 files changed, 637 insertions(+), 125 deletions(-) create mode 100644 docs/changelog/92252.yaml create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleOperationMetadata.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecycleOperationMetadataTests.java create mode 100644 x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/LifecycleOperationSnapshotTests.java diff --git a/docs/changelog/92252.yaml b/docs/changelog/92252.yaml new file mode 100644 index 000000000000..5c45a46a5d3a --- /dev/null +++ b/docs/changelog/92252.yaml @@ -0,0 +1,5 @@ +pr: 92252 +summary: Preventing ILM and SLM runtime state from being stored in a snapshot +area: ILM+SLM +type: bug +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java index a793c2006551..8fa90f319509 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java @@ -55,6 +55,7 @@ import org.elasticsearch.xpack.core.ilm.IndexLifecycleFeatureSetUsage; import org.elasticsearch.xpack.core.ilm.IndexLifecycleMetadata; import org.elasticsearch.xpack.core.ilm.LifecycleAction; +import org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata; import org.elasticsearch.xpack.core.ilm.LifecycleType; import org.elasticsearch.xpack.core.ilm.MigrateAction; import org.elasticsearch.xpack.core.ilm.ReadOnlyAction; @@ -481,6 +482,12 @@ public List getNamedWriteables() { IndexLifecycleMetadata.TYPE, IndexLifecycleMetadata.IndexLifecycleMetadataDiff::new ), + new NamedWriteableRegistry.Entry(Metadata.Custom.class, LifecycleOperationMetadata.TYPE, LifecycleOperationMetadata::new), + new NamedWriteableRegistry.Entry( + NamedDiff.class, + LifecycleOperationMetadata.TYPE, + LifecycleOperationMetadata.LifecycleOperationMetadataDiff::new + ), new NamedWriteableRegistry.Entry(Metadata.Custom.class, SnapshotLifecycleMetadata.TYPE, SnapshotLifecycleMetadata::new), new NamedWriteableRegistry.Entry( NamedDiff.class, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleMetadata.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleMetadata.java index 449998e4307f..08b04cd431fc 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleMetadata.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleMetadata.java @@ -86,6 +86,10 @@ public Map getPolicyMetadatas() { return policyMetadatas; } + /** + * @deprecated use {@link LifecycleOperationMetadata#getILMOperationMode()} instead. This may be incorrect. + */ + @Deprecated(since = "8.7.0") public OperationMode getOperationMode() { return operationMode; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleOperationMetadata.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleOperationMetadata.java new file mode 100644 index 000000000000..6f608cb7e336 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleOperationMetadata.java @@ -0,0 +1,202 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.ilm; + +import org.elasticsearch.Version; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.Diff; +import org.elasticsearch.cluster.NamedDiff; +import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Iterators; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xcontent.ConstructingObjectParser; +import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xpack.core.slm.SnapshotLifecycleMetadata; + +import java.io.IOException; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.Objects; +import java.util.Optional; + +/** + * Class that encapsulates the running operation mode of Index Lifecycle + * Management and Snapshot Lifecycle Management + */ +public class LifecycleOperationMetadata implements Metadata.Custom { + public static final String TYPE = "lifecycle_operation"; + public static final ParseField ILM_OPERATION_MODE_FIELD = new ParseField("ilm_operation_mode"); + public static final ParseField SLM_OPERATION_MODE_FIELD = new ParseField("slm_operation_mode"); + public static final LifecycleOperationMetadata EMPTY = new LifecycleOperationMetadata(OperationMode.RUNNING, OperationMode.RUNNING); + + @SuppressWarnings("unchecked") + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + TYPE, + a -> new LifecycleOperationMetadata(OperationMode.valueOf((String) a[0]), OperationMode.valueOf((String) a[1])) + ); + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), ILM_OPERATION_MODE_FIELD); + PARSER.declareString(ConstructingObjectParser.constructorArg(), SLM_OPERATION_MODE_FIELD); + } + + private final OperationMode ilmOperationMode; + private final OperationMode slmOperationMode; + + public LifecycleOperationMetadata(OperationMode ilmOperationMode, OperationMode slmOperationMode) { + this.ilmOperationMode = ilmOperationMode; + this.slmOperationMode = slmOperationMode; + } + + public LifecycleOperationMetadata(StreamInput in) throws IOException { + this.ilmOperationMode = in.readEnum(OperationMode.class); + this.slmOperationMode = in.readEnum(OperationMode.class); + } + + /** + * Returns the current ILM mode based on the given cluster state. It first checks the newer + * storage mechanism ({@link LifecycleOperationMetadata#getILMOperationMode()}) before falling + * back to {@link IndexLifecycleMetadata#getOperationMode()}. If neither exist, the default + * value for an empty state is used. + */ + @SuppressWarnings("deprecated") + public static OperationMode currentILMMode(final ClusterState state) { + IndexLifecycleMetadata oldMetadata = state.metadata().custom(IndexLifecycleMetadata.TYPE); + LifecycleOperationMetadata currentMetadata = state.metadata().custom(LifecycleOperationMetadata.TYPE); + return Optional.ofNullable(currentMetadata) + .map(LifecycleOperationMetadata::getILMOperationMode) + .orElse( + Optional.ofNullable(oldMetadata) + .map(IndexLifecycleMetadata::getOperationMode) + .orElseGet(LifecycleOperationMetadata.EMPTY::getILMOperationMode) + ); + } + + /** + * Returns the current ILM mode based on the given cluster state. It first checks the newer + * storage mechanism ({@link LifecycleOperationMetadata#getSLMOperationMode()}) before falling + * back to {@link SnapshotLifecycleMetadata#getOperationMode()}. If neither exist, the default + * value for an empty state is used. + */ + @SuppressWarnings("deprecated") + public static OperationMode currentSLMMode(final ClusterState state) { + SnapshotLifecycleMetadata oldMetadata = state.metadata().custom(SnapshotLifecycleMetadata.TYPE); + LifecycleOperationMetadata currentMetadata = state.metadata().custom(LifecycleOperationMetadata.TYPE); + return Optional.ofNullable(currentMetadata) + .map(LifecycleOperationMetadata::getSLMOperationMode) + .orElse( + Optional.ofNullable(oldMetadata) + .map(SnapshotLifecycleMetadata::getOperationMode) + .orElseGet(LifecycleOperationMetadata.EMPTY::getSLMOperationMode) + ); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeEnum(ilmOperationMode); + out.writeEnum(slmOperationMode); + } + + public OperationMode getILMOperationMode() { + return ilmOperationMode; + } + + public OperationMode getSLMOperationMode() { + return slmOperationMode; + } + + @Override + public Diff diff(Metadata.Custom previousState) { + return new LifecycleOperationMetadata.LifecycleOperationMetadataDiff((LifecycleOperationMetadata) previousState, this); + } + + @Override + public Iterator toXContentChunked(ToXContent.Params params) { + ToXContent ilmModeField = ((builder, params2) -> builder.field(ILM_OPERATION_MODE_FIELD.getPreferredName(), ilmOperationMode)); + ToXContent slmModeField = ((builder, params2) -> builder.field(SLM_OPERATION_MODE_FIELD.getPreferredName(), slmOperationMode)); + return Iterators.forArray(new ToXContent[] { ilmModeField, slmModeField }); + } + + @Override + public Version getMinimalSupportedVersion() { + return Version.V_8_7_0; + } + + @Override + public String getWriteableName() { + return TYPE; + } + + @Override + public EnumSet context() { + // We do not store the lifecycle operation mode for ILM and SLM into a snapshot. This is so + // ILM and SLM can be operated independently from restoring snapshots. + return EnumSet.of(Metadata.XContentContext.API, Metadata.XContentContext.GATEWAY); + } + + @Override + public int hashCode() { + return Objects.hash(ilmOperationMode, slmOperationMode); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj.getClass() != getClass()) { + return false; + } + LifecycleOperationMetadata other = (LifecycleOperationMetadata) obj; + return Objects.equals(ilmOperationMode, other.ilmOperationMode) && Objects.equals(slmOperationMode, other.slmOperationMode); + } + + @Override + public String toString() { + return Strings.toString(this, true, true); + } + + public static class LifecycleOperationMetadataDiff implements NamedDiff { + + final OperationMode ilmOperationMode; + final OperationMode slmOperationMode; + + LifecycleOperationMetadataDiff(LifecycleOperationMetadata before, LifecycleOperationMetadata after) { + this.ilmOperationMode = after.ilmOperationMode; + this.slmOperationMode = after.slmOperationMode; + } + + public LifecycleOperationMetadataDiff(StreamInput in) throws IOException { + this.ilmOperationMode = in.readEnum(OperationMode.class); + this.slmOperationMode = in.readEnum(OperationMode.class); + } + + @Override + public Metadata.Custom apply(Metadata.Custom part) { + return new LifecycleOperationMetadata(this.ilmOperationMode, this.slmOperationMode); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeEnum(ilmOperationMode); + out.writeEnum(slmOperationMode); + } + + @Override + public String getWriteableName() { + return TYPE; + } + + @Override + public Version getMinimalSupportedVersion() { + return Version.V_8_7_0; + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SnapshotLifecycleMetadata.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SnapshotLifecycleMetadata.java index 0398382f4d8a..eea252a8fb0c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SnapshotLifecycleMetadata.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SnapshotLifecycleMetadata.java @@ -21,6 +21,7 @@ import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata; import org.elasticsearch.xpack.core.ilm.OperationMode; import java.io.IOException; @@ -99,6 +100,10 @@ public Map getSnapshotConfigurations() return Collections.unmodifiableMap(this.snapshotConfigurations); } + /** + * @deprecated use {@link LifecycleOperationMetadata#getSLMOperationMode()} instead. This may be incorrect. + */ + @Deprecated(since = "8.7.0") public OperationMode getOperationMode() { return operationMode; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/action/GetSLMStatusAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/action/GetSLMStatusAction.java index a2a4831cde94..3008e8f2763d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/action/GetSLMStatusAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/action/GetSLMStatusAction.java @@ -29,7 +29,7 @@ protected GetSLMStatusAction() { public static class Response extends ActionResponse implements ToXContentObject { - private OperationMode mode; + private final OperationMode mode; public Response(StreamInput in) throws IOException { super(in); @@ -45,6 +45,10 @@ public void writeTo(StreamOutput out) throws IOException { out.writeEnum(this.mode); } + public OperationMode getOperationMode() { + return this.mode; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecycleOperationMetadataTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecycleOperationMetadataTests.java new file mode 100644 index 000000000000..efc771d245d9 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecycleOperationMetadataTests.java @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.ilm; + +import org.elasticsearch.Version; +import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractChunkedSerializingTestCase; +import org.elasticsearch.test.VersionUtils; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; + +import static org.hamcrest.Matchers.containsInAnyOrder; + +public class LifecycleOperationMetadataTests extends AbstractChunkedSerializingTestCase { + + @Override + protected LifecycleOperationMetadata createTestInstance() { + return new LifecycleOperationMetadata(randomFrom(OperationMode.values()), randomFrom(OperationMode.values())); + } + + @Override + protected LifecycleOperationMetadata doParseInstance(XContentParser parser) throws IOException { + return LifecycleOperationMetadata.PARSER.apply(parser, null); + } + + @Override + protected Writeable.Reader instanceReader() { + return LifecycleOperationMetadata::new; + } + + @Override + protected Metadata.Custom mutateInstance(Metadata.Custom instance) { + LifecycleOperationMetadata metadata = (LifecycleOperationMetadata) instance; + if (randomBoolean()) { + return new LifecycleOperationMetadata( + randomValueOtherThan(metadata.getILMOperationMode(), () -> randomFrom(OperationMode.values())), + metadata.getSLMOperationMode() + ); + } else { + return new LifecycleOperationMetadata( + metadata.getILMOperationMode(), + randomValueOtherThan(metadata.getSLMOperationMode(), () -> randomFrom(OperationMode.values())) + ); + } + } + + @Override + protected boolean isFragment() { + return true; + } + + public void testMinimumSupportedVersion() { + Version min = createTestInstance().getMinimalSupportedVersion(); + assertTrue(min.onOrBefore(VersionUtils.randomVersionBetween(random(), Version.V_8_7_0, Version.CURRENT))); + } + + public void testcontext() { + assertThat(createTestInstance().context(), containsInAnyOrder(Metadata.XContentContext.API, Metadata.XContentContext.GATEWAY)); + } +} diff --git a/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/IndexLifecycleInitialisationTests.java b/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/IndexLifecycleInitialisationTests.java index a35eacfcc1d6..aed1a3b253e1 100644 --- a/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/IndexLifecycleInitialisationTests.java +++ b/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/IndexLifecycleInitialisationTests.java @@ -478,12 +478,11 @@ public void testCreatePolicyWhenStopped() throws Exception { final String node1 = getLocalNodeId(server_1); assertAcked(client().execute(StopILMAction.INSTANCE, new StopILMRequest()).get()); - assertBusy( - () -> assertThat( - client().execute(GetStatusAction.INSTANCE, new GetStatusAction.Request()).get().getMode(), - equalTo(OperationMode.STOPPED) - ) - ); + assertBusy(() -> { + OperationMode mode = client().execute(GetStatusAction.INSTANCE, new GetStatusAction.Request()).get().getMode(); + logger.info("--> waiting for STOPPED, currently: {}", mode); + assertThat(mode, equalTo(OperationMode.STOPPED)); + }); logger.info("Creating lifecycle [test_lifecycle]"); PutLifecycleAction.Request putLifecycleRequest = new PutLifecycleAction.Request(lifecyclePolicy); diff --git a/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/slm/SnapshotLifecycleInitialisationTests.java b/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/slm/SnapshotLifecycleInitialisationTests.java index 9dc38cccedfb..fa312cdac8e1 100644 --- a/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/slm/SnapshotLifecycleInitialisationTests.java +++ b/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/slm/SnapshotLifecycleInitialisationTests.java @@ -17,7 +17,6 @@ import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.ilm.OperationMode; -import org.elasticsearch.xpack.core.slm.SnapshotLifecycleMetadata; import org.elasticsearch.xpack.core.slm.SnapshotLifecyclePolicy; import org.elasticsearch.xpack.core.slm.SnapshotRetentionConfiguration; import org.elasticsearch.xpack.core.slm.action.PutSnapshotLifecycleAction; @@ -31,6 +30,7 @@ import java.util.Collections; import java.util.concurrent.TimeUnit; +import static org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata.currentSLMMode; import static org.hamcrest.core.Is.is; public class SnapshotLifecycleInitialisationTests extends ESSingleNodeTestCase { @@ -82,7 +82,6 @@ public void testSLMIsInRunningModeWhenILMIsDisabled() throws Exception { ).get(10, TimeUnit.SECONDS); ClusterState state = getInstanceFromNode(ClusterService.class).state(); - SnapshotLifecycleMetadata snapMeta = state.metadata().custom(SnapshotLifecycleMetadata.TYPE); - assertThat(snapMeta.getOperationMode(), is(OperationMode.RUNNING)); + assertThat(currentSLMMode(state), is(OperationMode.RUNNING)); } } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java index ed29727a9ff1..a0d64321041f 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java @@ -51,6 +51,7 @@ import static org.elasticsearch.cluster.metadata.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; import static org.elasticsearch.cluster.routing.allocation.DataTier.ENFORCE_DEFAULT_TIER_PREFERENCE; import static org.elasticsearch.cluster.routing.allocation.DataTier.TIER_PREFERENCE; +import static org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata.currentILMMode; import static org.elasticsearch.xpack.core.ilm.OperationMode.STOPPED; import static org.elasticsearch.xpack.core.ilm.PhaseCacheManagement.updateIndicesForPolicy; import static org.elasticsearch.xpack.ilm.IndexLifecycleTransition.moveStateToNextActionAndUpdateCachedPhase; @@ -182,9 +183,9 @@ public static Tuple migrateToDataTiersRouting( ) { if (dryRun == false) { IndexLifecycleMetadata currentMetadata = currentState.metadata().custom(IndexLifecycleMetadata.TYPE); - if (currentMetadata != null && currentMetadata.getOperationMode() != STOPPED) { + if (currentMetadata != null && currentILMMode(currentState) != STOPPED) { throw new IllegalStateException( - "stop ILM before migrating to data tiers, current state is [" + currentMetadata.getOperationMode() + "]" + "stop ILM before migrating to data tiers, current state is [" + currentILMMode(currentState) + "]" ); } } @@ -274,7 +275,7 @@ static List migrateIlmPolicies( } if (migratedPolicies.size() > 0) { - IndexLifecycleMetadata newMetadata = new IndexLifecycleMetadata(newPolicies, currentLifecycleMetadata.getOperationMode()); + IndexLifecycleMetadata newMetadata = new IndexLifecycleMetadata(newPolicies, currentILMMode(currentState)); mb.putCustom(IndexLifecycleMetadata.TYPE, newMetadata); } return migratedPolicies; diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IlmHealthIndicatorService.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IlmHealthIndicatorService.java index b6d8a3c22b19..6a0411a0296e 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IlmHealthIndicatorService.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IlmHealthIndicatorService.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.ilm; +import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.health.Diagnosis; import org.elasticsearch.health.HealthIndicatorDetails; @@ -25,6 +26,7 @@ import static org.elasticsearch.health.HealthStatus.GREEN; import static org.elasticsearch.health.HealthStatus.YELLOW; +import static org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata.currentILMMode; /** * This indicator reports health for index lifecycle management component. @@ -65,16 +67,18 @@ public String name() { @Override public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { - var ilmMetadata = clusterService.state().metadata().custom(IndexLifecycleMetadata.TYPE, IndexLifecycleMetadata.EMPTY); + final ClusterState currentState = clusterService.state(); + var ilmMetadata = currentState.metadata().custom(IndexLifecycleMetadata.TYPE, IndexLifecycleMetadata.EMPTY); + final OperationMode currentMode = currentILMMode(currentState); if (ilmMetadata.getPolicyMetadatas().isEmpty()) { return createIndicator( GREEN, "No Index Lifecycle Management policies configured", - createDetails(verbose, ilmMetadata), + createDetails(verbose, ilmMetadata, currentMode), Collections.emptyList(), Collections.emptyList() ); - } else if (ilmMetadata.getOperationMode() != OperationMode.RUNNING) { + } else if (currentMode != OperationMode.RUNNING) { List impacts = Collections.singletonList( new HealthIndicatorImpact( NAME, @@ -88,7 +92,7 @@ public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { return createIndicator( YELLOW, "Index Lifecycle Management is not running", - createDetails(verbose, ilmMetadata), + createDetails(verbose, ilmMetadata, currentMode), impacts, List.of(ILM_NOT_RUNNING) ); @@ -96,18 +100,16 @@ public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { return createIndicator( GREEN, "Index Lifecycle Management is running", - createDetails(verbose, ilmMetadata), + createDetails(verbose, ilmMetadata, currentMode), Collections.emptyList(), Collections.emptyList() ); } } - private static HealthIndicatorDetails createDetails(boolean verbose, IndexLifecycleMetadata metadata) { + private static HealthIndicatorDetails createDetails(boolean verbose, IndexLifecycleMetadata metadata, OperationMode mode) { if (verbose) { - return new SimpleHealthIndicatorDetails( - Map.of("ilm_status", metadata.getOperationMode(), "policies", metadata.getPolicies().size()) - ); + return new SimpleHealthIndicatorDetails(Map.of("ilm_status", mode, "policies", metadata.getPolicies().size())); } else { return HealthIndicatorDetails.EMPTY; } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java index aaf4b4da61ad..d369d3b7ad0a 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java @@ -54,6 +54,7 @@ import org.elasticsearch.xpack.core.ilm.FreezeAction; import org.elasticsearch.xpack.core.ilm.IndexLifecycleMetadata; import org.elasticsearch.xpack.core.ilm.LifecycleAction; +import org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata; import org.elasticsearch.xpack.core.ilm.LifecycleSettings; import org.elasticsearch.xpack.core.ilm.LifecycleType; import org.elasticsearch.xpack.core.ilm.MigrateAction; @@ -304,6 +305,11 @@ private static List xContentEntries() { new ParseField(SnapshotLifecycleMetadata.TYPE), parser -> SnapshotLifecycleMetadata.PARSER.parse(parser, null) ), + new NamedXContentRegistry.Entry( + Metadata.Custom.class, + new ParseField(LifecycleOperationMetadata.TYPE), + parser -> LifecycleOperationMetadata.PARSER.parse(parser, null) + ), // Lifecycle Types new NamedXContentRegistry.Entry( LifecycleType.class, diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java index fac7be2dc999..e57c8b331f31 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java @@ -61,6 +61,7 @@ import static org.elasticsearch.core.Strings.format; import static org.elasticsearch.xpack.core.ilm.IndexLifecycleOriginationDateParser.parseIndexNameAndExtractDate; import static org.elasticsearch.xpack.core.ilm.IndexLifecycleOriginationDateParser.shouldParseIndexName; +import static org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata.currentILMMode; /** * A service which runs the {@link LifecyclePolicy}s associated with indexes. @@ -167,7 +168,7 @@ void onMaster(ClusterState clusterState) { final IndexLifecycleMetadata currentMetadata = clusterState.metadata().custom(IndexLifecycleMetadata.TYPE); if (currentMetadata != null) { - OperationMode currentMode = currentMetadata.getOperationMode(); + OperationMode currentMode = currentILMMode(clusterState); if (OperationMode.STOPPED.equals(currentMode)) { return; } @@ -238,11 +239,15 @@ void onMaster(ClusterState clusterState) { } if (safeToStop && OperationMode.STOPPING == currentMode) { - submitUnbatchedTask("ilm_operation_mode_update[stopped]", OperationModeUpdateTask.ilmMode(OperationMode.STOPPED)); + stopILM(); } } } + private void stopILM() { + submitUnbatchedTask("ilm_operation_mode_update[stopped]", OperationModeUpdateTask.ilmMode(OperationMode.STOPPED)); + } + @Override public void beforeIndexAddedToCluster(Index index, Settings indexSettings) { if (shouldParseIndexName(indexSettings)) { @@ -318,10 +323,7 @@ public void clusterChanged(ClusterChangedEvent event) { }); } - final IndexLifecycleMetadata lifecycleMetadata = event.state().metadata().custom(IndexLifecycleMetadata.TYPE); - if (lifecycleMetadata != null) { - triggerPolicies(event.state(), true); - } + triggerPolicies(event.state(), true); } } @@ -370,12 +372,15 @@ public boolean policyExists(String policyId) { void triggerPolicies(ClusterState clusterState, boolean fromClusterStateChange) { IndexLifecycleMetadata currentMetadata = clusterState.metadata().custom(IndexLifecycleMetadata.TYPE); + OperationMode currentMode = currentILMMode(clusterState); if (currentMetadata == null) { + if (currentMode == OperationMode.STOPPING) { + // There are no policies and ILM is in stopping mode, so stop ILM and get out of here + stopILM(); + } return; } - OperationMode currentMode = currentMetadata.getOperationMode(); - if (OperationMode.STOPPED.equals(currentMode)) { return; } @@ -454,7 +459,7 @@ void triggerPolicies(ClusterState clusterState, boolean fromClusterStateChange) } if (safeToStop && OperationMode.STOPPING == currentMode) { - submitUnbatchedTask("ilm_operation_mode_update[stopped]", OperationModeUpdateTask.ilmMode(OperationMode.STOPPED)); + stopILM(); } } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/OperationModeUpdateTask.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/OperationModeUpdateTask.java index 80cc3834ce70..3162b232757a 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/OperationModeUpdateTask.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/OperationModeUpdateTask.java @@ -17,12 +17,14 @@ import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.Priority; import org.elasticsearch.core.Nullable; -import org.elasticsearch.xpack.core.ilm.IndexLifecycleMetadata; +import org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata; import org.elasticsearch.xpack.core.ilm.OperationMode; -import org.elasticsearch.xpack.core.slm.SnapshotLifecycleMetadata; import java.util.Objects; +import static org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata.currentILMMode; +import static org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata.currentSLMMode; + /** * This task updates the operation mode state for ILM. * @@ -91,27 +93,26 @@ private ClusterState updateILMState(final ClusterState currentState) { if (ilmMode == null) { return currentState; } - IndexLifecycleMetadata currentMetadata = currentState.metadata().custom(IndexLifecycleMetadata.TYPE); - if (currentMetadata != null && currentMetadata.getOperationMode().isValidChange(ilmMode) == false) { + + final OperationMode currentMode = currentILMMode(currentState); + if (currentMode.equals(ilmMode)) { + // No need for a new state return currentState; - } else if (currentMetadata == null) { - currentMetadata = IndexLifecycleMetadata.EMPTY; } final OperationMode newMode; - if (currentMetadata.getOperationMode().isValidChange(ilmMode)) { + if (currentMode.isValidChange(ilmMode)) { newMode = ilmMode; } else { - newMode = currentMetadata.getOperationMode(); + // The transition is invalid, return the current state + return currentState; } - if (newMode.equals(ilmMode) == false) { - logger.info("updating ILM operation mode to {}", newMode); - } + logger.info("updating ILM operation mode to {}", newMode); return ClusterState.builder(currentState) .metadata( Metadata.builder(currentState.metadata()) - .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata(currentMetadata.getPolicyMetadatas(), newMode)) + .putCustom(LifecycleOperationMetadata.TYPE, new LifecycleOperationMetadata(newMode, currentSLMMode(currentState))) ) .build(); } @@ -120,30 +121,26 @@ private ClusterState updateSLMState(final ClusterState currentState) { if (slmMode == null) { return currentState; } - SnapshotLifecycleMetadata currentMetadata = currentState.metadata().custom(SnapshotLifecycleMetadata.TYPE); - if (currentMetadata != null && currentMetadata.getOperationMode().isValidChange(slmMode) == false) { + + final OperationMode currentMode = currentSLMMode(currentState); + if (currentMode.equals(slmMode)) { + // No need for a new state return currentState; - } else if (currentMetadata == null) { - currentMetadata = SnapshotLifecycleMetadata.EMPTY; } final OperationMode newMode; - if (currentMetadata.getOperationMode().isValidChange(slmMode)) { + if (currentMode.isValidChange(slmMode)) { newMode = slmMode; } else { - newMode = currentMetadata.getOperationMode(); + // The transition is invalid, return the current state + return currentState; } - if (newMode.equals(slmMode) == false) { - logger.info("updating SLM operation mode to {}", newMode); - } + logger.info("updating SLM operation mode to {}", newMode); return ClusterState.builder(currentState) .metadata( Metadata.builder(currentState.metadata()) - .putCustom( - SnapshotLifecycleMetadata.TYPE, - new SnapshotLifecycleMetadata(currentMetadata.getSnapshotConfigurations(), newMode, currentMetadata.getStats()) - ) + .putCustom(LifecycleOperationMetadata.TYPE, new LifecycleOperationMetadata(currentILMMode(currentState), newMode)) ) .build(); } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleAction.java index 26f1f513ab55..39d87d26c4e9 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleAction.java @@ -37,6 +37,8 @@ import java.util.SortedMap; import java.util.TreeMap; +import static org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata.currentILMMode; + public class TransportDeleteLifecycleAction extends TransportMasterNodeAction { @Inject @@ -103,7 +105,7 @@ public ClusterState execute(ClusterState currentState) { } SortedMap newPolicies = new TreeMap<>(currentMetadata.getPolicyMetadatas()); newPolicies.remove(request.getPolicyName()); - IndexLifecycleMetadata newMetadata = new IndexLifecycleMetadata(newPolicies, currentMetadata.getOperationMode()); + IndexLifecycleMetadata newMetadata = new IndexLifecycleMetadata(newPolicies, currentILMMode(currentState)); newState.metadata(Metadata.builder(currentState.getMetadata()).putCustom(IndexLifecycleMetadata.TYPE, newMetadata).build()); return newState.build(); } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportGetStatusAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportGetStatusAction.java index 62c59c80b815..90533405c1fc 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportGetStatusAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportGetStatusAction.java @@ -19,12 +19,12 @@ import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; -import org.elasticsearch.xpack.core.ilm.IndexLifecycleMetadata; -import org.elasticsearch.xpack.core.ilm.OperationMode; import org.elasticsearch.xpack.core.ilm.action.GetStatusAction; import org.elasticsearch.xpack.core.ilm.action.GetStatusAction.Request; import org.elasticsearch.xpack.core.ilm.action.GetStatusAction.Response; +import static org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata.currentILMMode; + public class TransportGetStatusAction extends TransportMasterNodeAction { @Inject @@ -50,15 +50,7 @@ public TransportGetStatusAction( @Override protected void masterOperation(Task task, Request request, ClusterState state, ActionListener listener) { - IndexLifecycleMetadata metadata = state.metadata().custom(IndexLifecycleMetadata.TYPE); - final Response response; - if (metadata == null) { - // no need to actually install metadata just yet, but safe to say it is not stopped - response = new Response(OperationMode.RUNNING); - } else { - response = new Response(metadata.getOperationMode()); - } - listener.onResponse(response); + listener.onResponse(new Response(currentILMMode(state))); } @Override diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportMigrateToDataTiersAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportMigrateToDataTiersAction.java index fc6017dbb953..9a80e5d83ebc 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportMigrateToDataTiersAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportMigrateToDataTiersAction.java @@ -38,6 +38,7 @@ import org.elasticsearch.xpack.core.ilm.IndexLifecycleMetadata; import static org.elasticsearch.xpack.cluster.metadata.MetadataMigrateToDataTiersRoutingService.migrateToDataTiersRouting; +import static org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata.currentILMMode; import static org.elasticsearch.xpack.core.ilm.OperationMode.STOPPED; public class TransportMigrateToDataTiersAction extends TransportMasterNodeAction { @@ -107,11 +108,9 @@ protected void masterOperation( } IndexLifecycleMetadata currentMetadata = state.metadata().custom(IndexLifecycleMetadata.TYPE); - if (currentMetadata != null && currentMetadata.getOperationMode() != STOPPED) { + if (currentMetadata != null && currentILMMode(state) != STOPPED) { listener.onFailure( - new IllegalStateException( - "stop ILM before migrating to data tiers, current state is [" + currentMetadata.getOperationMode() + "]" - ) + new IllegalStateException("stop ILM before migrating to data tiers, current state is [" + currentILMMode(state) + "]") ); return; } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java index d4834bef5939..d681ba8e126b 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java @@ -52,6 +52,7 @@ import java.util.SortedMap; import java.util.TreeMap; +import static org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata.currentILMMode; import static org.elasticsearch.xpack.core.ilm.PhaseCacheManagement.updateIndicesForPolicy; import static org.elasticsearch.xpack.core.searchablesnapshots.SearchableSnapshotsConstants.SEARCHABLE_SNAPSHOT_FEATURE; @@ -190,7 +191,7 @@ public ClusterState execute(ClusterState currentState) throws Exception { logger.info("updating index lifecycle policy [{}]", request.getPolicy().getName()); } } - IndexLifecycleMetadata newMetadata = new IndexLifecycleMetadata(newPolicies, currentMetadata.getOperationMode()); + IndexLifecycleMetadata newMetadata = new IndexLifecycleMetadata(newPolicies, currentILMMode(currentState)); stateBuilder.metadata(Metadata.builder(currentState.getMetadata()).putCustom(IndexLifecycleMetadata.TYPE, newMetadata).build()); ClusterState nonRefreshedState = stateBuilder.build(); if (oldPolicy == null) { diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SlmHealthIndicatorService.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SlmHealthIndicatorService.java index 45ecd0a9c279..773052f84293 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SlmHealthIndicatorService.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SlmHealthIndicatorService.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.slm; +import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.health.Diagnosis; @@ -32,6 +33,7 @@ import static org.elasticsearch.health.HealthStatus.GREEN; import static org.elasticsearch.health.HealthStatus.YELLOW; +import static org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata.currentSLMMode; import static org.elasticsearch.xpack.core.ilm.LifecycleSettings.SLM_HEALTH_FAILED_SNAPSHOT_WARN_THRESHOLD_SETTING; /** @@ -98,16 +100,18 @@ public String name() { @Override public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { - var slmMetadata = clusterService.state().metadata().custom(SnapshotLifecycleMetadata.TYPE, SnapshotLifecycleMetadata.EMPTY); + final ClusterState currentState = clusterService.state(); + var slmMetadata = currentState.metadata().custom(SnapshotLifecycleMetadata.TYPE, SnapshotLifecycleMetadata.EMPTY); + final OperationMode currentMode = currentSLMMode(currentState); if (slmMetadata.getSnapshotConfigurations().isEmpty()) { return createIndicator( GREEN, "No Snapshot Lifecycle Management policies configured", - createDetails(verbose, Collections.emptyList(), slmMetadata), + createDetails(verbose, Collections.emptyList(), slmMetadata, currentMode), Collections.emptyList(), Collections.emptyList() ); - } else if (slmMetadata.getOperationMode() != OperationMode.RUNNING) { + } else if (currentMode != OperationMode.RUNNING) { List impacts = Collections.singletonList( new HealthIndicatorImpact( NAME, @@ -120,7 +124,7 @@ public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { return createIndicator( YELLOW, "Snapshot Lifecycle Management is not running", - createDetails(verbose, Collections.emptyList(), slmMetadata), + createDetails(verbose, Collections.emptyList(), slmMetadata, currentMode), impacts, List.of(SLM_NOT_RUNNING) ); @@ -169,7 +173,7 @@ public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { return createIndicator( YELLOW, "Encountered [" + unhealthyPolicies.size() + "] unhealthy snapshot lifecycle management policies.", - createDetails(verbose, unhealthyPolicies, slmMetadata), + createDetails(verbose, unhealthyPolicies, slmMetadata, currentMode), impacts, List.of( new Diagnosis( @@ -188,7 +192,7 @@ public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { return createIndicator( GREEN, "Snapshot Lifecycle Management is running", - createDetails(verbose, Collections.emptyList(), slmMetadata), + createDetails(verbose, Collections.emptyList(), slmMetadata, currentMode), Collections.emptyList(), Collections.emptyList() ); @@ -215,11 +219,12 @@ static boolean snapshotFailuresExceedWarningCount(long failedSnapshotWarnThresho private static HealthIndicatorDetails createDetails( boolean verbose, Collection unhealthyPolicies, - SnapshotLifecycleMetadata metadata + SnapshotLifecycleMetadata metadata, + OperationMode mode ) { if (verbose) { Map details = new LinkedHashMap<>(); - details.put("slm_status", metadata.getOperationMode()); + details.put("slm_status", mode); details.put("policies", metadata.getSnapshotConfigurations().size()); if (unhealthyPolicies.size() > 0) { details.put( diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SnapshotLifecycleService.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SnapshotLifecycleService.java index 3f3cb8ef7dba..f47985891ccd 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SnapshotLifecycleService.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SnapshotLifecycleService.java @@ -31,13 +31,14 @@ import java.io.Closeable; import java.time.Clock; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import java.util.regex.Pattern; import java.util.stream.Collectors; +import static org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata.currentSLMMode; + /** * {@code SnapshotLifecycleService} manages snapshot policy scheduling and triggering of the * {@link SnapshotLifecycleTask}. It reacts to new policies in the cluster state by scheduling a @@ -121,20 +122,16 @@ SchedulerEngine getScheduler() { * Returns true if SLM is in the stopping or stopped state */ static boolean slmStoppedOrStopping(ClusterState state) { - return Optional.ofNullable((SnapshotLifecycleMetadata) state.metadata().custom(SnapshotLifecycleMetadata.TYPE)) - .map(SnapshotLifecycleMetadata::getOperationMode) - .map(mode -> OperationMode.STOPPING == mode || OperationMode.STOPPED == mode) - .orElse(false); + OperationMode mode = currentSLMMode(state); + return OperationMode.STOPPING == mode || OperationMode.STOPPED == mode; } /** * Returns true if SLM is in the stopping state */ static boolean slmStopping(ClusterState state) { - return Optional.ofNullable((SnapshotLifecycleMetadata) state.metadata().custom(SnapshotLifecycleMetadata.TYPE)) - .map(SnapshotLifecycleMetadata::getOperationMode) - .map(mode -> OperationMode.STOPPING == mode) - .orElse(false); + OperationMode mode = currentSLMMode(state); + return OperationMode.STOPPING == mode; } /** diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SnapshotLifecycleTask.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SnapshotLifecycleTask.java index bd06e35a5b21..58e208d3d2fb 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SnapshotLifecycleTask.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SnapshotLifecycleTask.java @@ -41,6 +41,7 @@ import java.util.Optional; import static org.elasticsearch.core.Strings.format; +import static org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata.currentSLMMode; public class SnapshotLifecycleTask implements SchedulerEngine.Listener { @@ -284,7 +285,11 @@ public ClusterState execute(ClusterState currentState) throws Exception { } snapLifecycles.put(policyName, newPolicyMetadata.build()); - SnapshotLifecycleMetadata lifecycleMetadata = new SnapshotLifecycleMetadata(snapLifecycles, snapMeta.getOperationMode(), stats); + SnapshotLifecycleMetadata lifecycleMetadata = new SnapshotLifecycleMetadata( + snapLifecycles, + currentSLMMode(currentState), + stats + ); Metadata currentMeta = currentState.metadata(); return ClusterState.builder(currentState) .metadata(Metadata.builder(currentMeta).putCustom(SnapshotLifecycleMetadata.TYPE, lifecycleMetadata)) diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/UpdateSnapshotLifecycleStatsTask.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/UpdateSnapshotLifecycleStatsTask.java index f4b31de86828..11478e929c76 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/UpdateSnapshotLifecycleStatsTask.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/UpdateSnapshotLifecycleStatsTask.java @@ -16,6 +16,7 @@ import org.elasticsearch.xpack.core.slm.SnapshotLifecycleStats; import static org.elasticsearch.core.Strings.format; +import static org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata.currentSLMMode; /** * {@link UpdateSnapshotLifecycleStatsTask} is a cluster state update task that retrieves the @@ -45,7 +46,7 @@ public ClusterState execute(ClusterState currentState) { SnapshotLifecycleStats newMetrics = currentSlmMeta.getStats().merge(runStats); SnapshotLifecycleMetadata newSlmMeta = new SnapshotLifecycleMetadata( currentSlmMeta.getSnapshotConfigurations(), - currentSlmMeta.getOperationMode(), + currentSLMMode(currentState), newMetrics ); diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/action/TransportDeleteSnapshotLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/action/TransportDeleteSnapshotLifecycleAction.java index 4628560f6a73..4ccd025bab5a 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/action/TransportDeleteSnapshotLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/action/TransportDeleteSnapshotLifecycleAction.java @@ -26,6 +26,7 @@ import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata; import org.elasticsearch.xpack.core.slm.SnapshotLifecycleMetadata; import org.elasticsearch.xpack.core.slm.SnapshotLifecyclePolicyMetadata; import org.elasticsearch.xpack.core.slm.action.DeleteSnapshotLifecycleAction; @@ -101,6 +102,7 @@ public ClusterState execute(ClusterState currentState) { if (snapMeta == null) { throw new ResourceNotFoundException("snapshot lifecycle policy not found: {}", request.getLifecycleId()); } + var currentMode = LifecycleOperationMetadata.currentSLMMode(currentState); // Check that the policy exists in the first place snapMeta.getSnapshotConfigurations() .entrySet() @@ -123,7 +125,7 @@ public ClusterState execute(ClusterState currentState) { SnapshotLifecycleMetadata.TYPE, new SnapshotLifecycleMetadata( newConfigs, - snapMeta.getOperationMode(), + currentMode, snapMeta.getStats().removePolicy(request.getLifecycleId()) ) ) diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/action/TransportGetSLMStatusAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/action/TransportGetSLMStatusAction.java index fbb182824a1d..57b585395edc 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/action/TransportGetSLMStatusAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/action/TransportGetSLMStatusAction.java @@ -19,10 +19,10 @@ import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; -import org.elasticsearch.xpack.core.ilm.OperationMode; -import org.elasticsearch.xpack.core.slm.SnapshotLifecycleMetadata; import org.elasticsearch.xpack.core.slm.action.GetSLMStatusAction; +import static org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata.currentSLMMode; + public class TransportGetSLMStatusAction extends TransportMasterNodeAction { @Inject @@ -53,15 +53,7 @@ protected void masterOperation( ClusterState state, ActionListener listener ) { - SnapshotLifecycleMetadata metadata = state.metadata().custom(SnapshotLifecycleMetadata.TYPE); - final GetSLMStatusAction.Response response; - if (metadata == null) { - // no need to actually install metadata just yet, but safe to say it is not stopped - response = new GetSLMStatusAction.Response(OperationMode.RUNNING); - } else { - response = new GetSLMStatusAction.Response(metadata.getOperationMode()); - } - listener.onResponse(response); + listener.onResponse(new GetSLMStatusAction.Response(currentSLMMode(state))); } @Override diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/action/TransportPutSnapshotLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/action/TransportPutSnapshotLifecycleAction.java index 6ea63e6ef2c7..4ce91d2291b7 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/action/TransportPutSnapshotLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/action/TransportPutSnapshotLifecycleAction.java @@ -28,8 +28,8 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.ClientHelper; +import org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata; import org.elasticsearch.xpack.core.ilm.LifecyclePolicy; -import org.elasticsearch.xpack.core.ilm.OperationMode; import org.elasticsearch.xpack.core.slm.SnapshotLifecycleMetadata; import org.elasticsearch.xpack.core.slm.SnapshotLifecyclePolicyMetadata; import org.elasticsearch.xpack.core.slm.SnapshotLifecycleStats; @@ -129,6 +129,7 @@ public static class UpdateSnapshotPolicyTask extends AckedClusterStateUpdateTask @Override public ClusterState execute(ClusterState currentState) { SnapshotLifecycleMetadata snapMeta = currentState.metadata().custom(SnapshotLifecycleMetadata.TYPE); + var currentMode = LifecycleOperationMetadata.currentSLMMode(currentState); String id = request.getLifecycleId(); final SnapshotLifecycleMetadata lifecycleMetadata; @@ -140,7 +141,7 @@ public ClusterState execute(ClusterState currentState) { .build(); lifecycleMetadata = new SnapshotLifecycleMetadata( Collections.singletonMap(id, meta), - OperationMode.RUNNING, + currentMode, new SnapshotLifecycleStats() ); logger.info("adding new snapshot lifecycle [{}]", id); @@ -154,7 +155,7 @@ public ClusterState execute(ClusterState currentState) { .setModifiedDate(Instant.now().toEpochMilli()) .build(); snapLifecycles.put(id, newLifecycle); - lifecycleMetadata = new SnapshotLifecycleMetadata(snapLifecycles, snapMeta.getOperationMode(), snapMeta.getStats()); + lifecycleMetadata = new SnapshotLifecycleMetadata(snapLifecycles, currentMode, snapMeta.getStats()); if (oldLifecycle == null) { logger.info("adding new snapshot lifecycle [{}]", id); } else { diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/LifecycleOperationSnapshotTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/LifecycleOperationSnapshotTests.java new file mode 100644 index 000000000000..a0c9517db409 --- /dev/null +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/LifecycleOperationSnapshotTests.java @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.ilm; + +import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsAction; +import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest; +import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsResponse; +import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotAction; +import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.snapshots.SnapshotState; +import org.elasticsearch.test.ESSingleNodeTestCase; +import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; +import org.elasticsearch.xpack.core.ilm.LifecyclePolicy; +import org.elasticsearch.xpack.core.ilm.OperationMode; +import org.elasticsearch.xpack.core.ilm.Phase; +import org.elasticsearch.xpack.core.ilm.ReadOnlyAction; +import org.elasticsearch.xpack.core.ilm.StopILMRequest; +import org.elasticsearch.xpack.core.ilm.action.GetStatusAction; +import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction; +import org.elasticsearch.xpack.core.ilm.action.StopILMAction; +import org.elasticsearch.xpack.core.slm.SnapshotLifecyclePolicy; +import org.elasticsearch.xpack.core.slm.action.ExecuteSnapshotLifecycleAction; +import org.elasticsearch.xpack.core.slm.action.GetSLMStatusAction; +import org.elasticsearch.xpack.core.slm.action.PutSnapshotLifecycleAction; +import org.elasticsearch.xpack.core.slm.action.StopSLMAction; + +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.elasticsearch.xpack.core.slm.SnapshotLifecyclePolicyMetadataTests.randomRetention; +import static org.elasticsearch.xpack.core.slm.SnapshotLifecyclePolicyMetadataTests.randomSchedule; +import static org.hamcrest.Matchers.equalTo; + +public class LifecycleOperationSnapshotTests extends ESSingleNodeTestCase { + + @Override + protected Collection> getPlugins() { + return List.of(LocalStateCompositeXPackPlugin.class, IndexLifecycle.class); + } + + @Override + protected Settings nodeSettings() { + return Settings.builder().put(super.nodeSettings()).put("slm.history_index_enabled", false).build(); + } + + public void testModeSnapshotRestore() throws Exception { + client().admin() + .cluster() + .preparePutRepository("repo") + .setType("fs") + .setSettings(Settings.builder().put("location", "repo").build()) + .get(); + + client().execute( + PutSnapshotLifecycleAction.INSTANCE, + new PutSnapshotLifecycleAction.Request( + "slm-policy", + new SnapshotLifecyclePolicy( + "slm-policy", + randomAlphaOfLength(4).toLowerCase(Locale.ROOT), + randomSchedule(), + "repo", + null, + randomRetention() + ) + ) + ).get(); + + client().execute( + PutLifecycleAction.INSTANCE, + new PutLifecycleAction.Request( + new LifecyclePolicy( + "ilm-policy", + Map.of("warm", new Phase("warm", TimeValue.timeValueHours(1), Map.of("readonly", new ReadOnlyAction()))) + ) + ) + ); + + assertThat(ilmMode(), equalTo(OperationMode.RUNNING)); + assertThat(slmMode(), equalTo(OperationMode.RUNNING)); + + // Take snapshot + ExecuteSnapshotLifecycleAction.Response resp = client().execute( + ExecuteSnapshotLifecycleAction.INSTANCE, + new ExecuteSnapshotLifecycleAction.Request("slm-policy") + ).get(); + final String snapshotName = resp.getSnapshotName(); + // Wait for the snapshot to be successful + assertBusy(() -> { + logger.info("--> checking for snapshot success"); + try { + GetSnapshotsResponse getResp = client().execute( + GetSnapshotsAction.INSTANCE, + new GetSnapshotsRequest(new String[] { "repo" }, new String[] { snapshotName }) + ).get(); + assertThat(getResp.getSnapshots().size(), equalTo(1)); + assertThat(getResp.getSnapshots().get(0).state(), equalTo(SnapshotState.SUCCESS)); + } catch (Exception e) { + fail("snapshot does not yet exist"); + } + }); + + assertAcked(client().execute(StopILMAction.INSTANCE, new StopILMRequest()).get()); + assertAcked(client().execute(StopSLMAction.INSTANCE, new StopSLMAction.Request()).get()); + assertBusy(() -> assertThat(ilmMode(), equalTo(OperationMode.STOPPED))); + assertBusy(() -> assertThat(slmMode(), equalTo(OperationMode.STOPPED))); + + // Restore snapshot + client().execute( + RestoreSnapshotAction.INSTANCE, + new RestoreSnapshotRequest("repo", snapshotName).includeGlobalState(true).indices(Strings.EMPTY_ARRAY).waitForCompletion(true) + ).get(); + + assertBusy(() -> assertThat(ilmMode(), equalTo(OperationMode.STOPPED))); + assertBusy(() -> assertThat(slmMode(), equalTo(OperationMode.STOPPED))); + } + + private OperationMode ilmMode() throws Exception { + return client().execute(GetStatusAction.INSTANCE, new GetStatusAction.Request()).get().getMode(); + } + + private OperationMode slmMode() throws Exception { + return client().execute(GetSLMStatusAction.INSTANCE, new GetSLMStatusAction.Request()).get().getOperationMode(); + } +} diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/OperationModeUpdateTaskTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/OperationModeUpdateTaskTests.java index 632b93da6b92..05f66b821c55 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/OperationModeUpdateTaskTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/OperationModeUpdateTaskTests.java @@ -12,50 +12,89 @@ import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.ilm.IndexLifecycleMetadata; +import org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata; import org.elasticsearch.xpack.core.ilm.OperationMode; import org.elasticsearch.xpack.core.slm.SnapshotLifecycleMetadata; import org.elasticsearch.xpack.core.slm.SnapshotLifecycleStats; import java.util.Collections; import java.util.Map; +import java.util.Optional; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; public class OperationModeUpdateTaskTests extends ESTestCase { - public void testExecute() { - assertMove(OperationMode.RUNNING, OperationMode.STOPPING); - assertMove(OperationMode.STOPPING, randomFrom(OperationMode.RUNNING, OperationMode.STOPPED)); - assertMove(OperationMode.STOPPED, OperationMode.RUNNING); + public void testILMExecute() { + assertILMMove(OperationMode.RUNNING, OperationMode.STOPPING); + assertILMMove(OperationMode.STOPPING, OperationMode.RUNNING); + assertILMMove(OperationMode.STOPPING, OperationMode.STOPPED); + assertILMMove(OperationMode.STOPPED, OperationMode.RUNNING); OperationMode mode = randomFrom(OperationMode.values()); - assertNoMove(mode, mode); - assertNoMove(OperationMode.STOPPED, OperationMode.STOPPING); - assertNoMove(OperationMode.RUNNING, OperationMode.STOPPED); + assertNoILMMove(mode, mode); + assertNoILMMove(OperationMode.STOPPED, OperationMode.STOPPING); + assertNoILMMove(OperationMode.RUNNING, OperationMode.STOPPED); + } + + public void testSLMExecute() { + assertSLMMove(OperationMode.RUNNING, OperationMode.STOPPING); + assertSLMMove(OperationMode.STOPPING, OperationMode.RUNNING); + assertSLMMove(OperationMode.STOPPING, OperationMode.STOPPED); + assertSLMMove(OperationMode.STOPPED, OperationMode.RUNNING); + + OperationMode mode = randomFrom(OperationMode.values()); + assertNoSLMMove(mode, mode); + assertNoSLMMove(OperationMode.STOPPED, OperationMode.STOPPING); + assertNoSLMMove(OperationMode.RUNNING, OperationMode.STOPPED); } public void testExecuteWithEmptyMetadata() { OperationMode requestedMode = OperationMode.STOPPING; - OperationMode newMode = executeUpdate(false, IndexLifecycleMetadata.EMPTY.getOperationMode(), requestedMode, false); + OperationMode newMode = executeILMUpdate(false, LifecycleOperationMetadata.EMPTY.getILMOperationMode(), requestedMode, false); + assertThat(newMode, equalTo(requestedMode)); + + newMode = executeSLMUpdate(false, LifecycleOperationMetadata.EMPTY.getSLMOperationMode(), requestedMode, false); assertThat(newMode, equalTo(requestedMode)); - requestedMode = randomFrom(OperationMode.RUNNING, OperationMode.STOPPED); - newMode = executeUpdate(false, IndexLifecycleMetadata.EMPTY.getOperationMode(), requestedMode, false); + requestedMode = OperationMode.RUNNING; + newMode = executeILMUpdate(false, LifecycleOperationMetadata.EMPTY.getILMOperationMode(), requestedMode, true); + assertThat(newMode, equalTo(OperationMode.RUNNING)); + requestedMode = OperationMode.STOPPED; + newMode = executeILMUpdate(false, LifecycleOperationMetadata.EMPTY.getILMOperationMode(), requestedMode, true); + assertThat(newMode, equalTo(OperationMode.RUNNING)); + + requestedMode = OperationMode.RUNNING; + newMode = executeSLMUpdate(false, LifecycleOperationMetadata.EMPTY.getSLMOperationMode(), requestedMode, true); + assertThat(newMode, equalTo(OperationMode.RUNNING)); + requestedMode = OperationMode.STOPPED; + newMode = executeSLMUpdate(false, LifecycleOperationMetadata.EMPTY.getSLMOperationMode(), requestedMode, true); assertThat(newMode, equalTo(OperationMode.RUNNING)); } - private void assertMove(OperationMode currentMode, OperationMode requestedMode) { - OperationMode newMode = executeUpdate(true, currentMode, requestedMode, false); + private void assertILMMove(OperationMode currentMode, OperationMode requestedMode) { + OperationMode newMode = executeILMUpdate(true, currentMode, requestedMode, false); assertThat(newMode, equalTo(requestedMode)); } - private void assertNoMove(OperationMode currentMode, OperationMode requestedMode) { - OperationMode newMode = executeUpdate(true, currentMode, requestedMode, true); + private void assertSLMMove(OperationMode currentMode, OperationMode requestedMode) { + OperationMode newMode = executeSLMUpdate(true, currentMode, requestedMode, false); + assertThat(newMode, equalTo(requestedMode)); + } + + private void assertNoILMMove(OperationMode currentMode, OperationMode requestedMode) { + OperationMode newMode = executeILMUpdate(true, currentMode, requestedMode, true); + assertThat(newMode, equalTo(currentMode)); + } + + private void assertNoSLMMove(OperationMode currentMode, OperationMode requestedMode) { + OperationMode newMode = executeSLMUpdate(true, currentMode, requestedMode, true); assertThat(newMode, equalTo(currentMode)); } - private OperationMode executeUpdate( + @SuppressWarnings("deprecated") + private OperationMode executeILMUpdate( boolean metadataInstalled, OperationMode currentMode, OperationMode requestMode, @@ -76,13 +115,49 @@ private OperationMode executeUpdate( ClusterState state = ClusterState.builder(ClusterName.DEFAULT).metadata(metadata).build(); OperationModeUpdateTask task = OperationModeUpdateTask.ilmMode(requestMode); ClusterState newState = task.execute(state); + if (assertSameClusterState) { + assertSame("expected the same state instance but they were different", state, newState); + } else { + assertThat("expected a different state instance but they were the same", state, not(equalTo(newState))); + } + LifecycleOperationMetadata newMetadata = newState.metadata().custom(LifecycleOperationMetadata.TYPE); + IndexLifecycleMetadata oldMetadata = newState.metadata().custom(IndexLifecycleMetadata.TYPE, IndexLifecycleMetadata.EMPTY); + return Optional.ofNullable(newMetadata) + .map(LifecycleOperationMetadata::getILMOperationMode) + .orElseGet(oldMetadata::getOperationMode); + } + + @SuppressWarnings("deprecated") + private OperationMode executeSLMUpdate( + boolean metadataInstalled, + OperationMode currentMode, + OperationMode requestMode, + boolean assertSameClusterState + ) { + IndexLifecycleMetadata indexLifecycleMetadata = new IndexLifecycleMetadata(Collections.emptyMap(), currentMode); + SnapshotLifecycleMetadata snapshotLifecycleMetadata = new SnapshotLifecycleMetadata( + Collections.emptyMap(), + currentMode, + new SnapshotLifecycleStats() + ); + Metadata.Builder metadata = Metadata.builder().persistentSettings(settings(Version.CURRENT).build()); + if (metadataInstalled) { + metadata.customs( + Map.of(IndexLifecycleMetadata.TYPE, indexLifecycleMetadata, SnapshotLifecycleMetadata.TYPE, snapshotLifecycleMetadata) + ); + } + ClusterState state = ClusterState.builder(ClusterName.DEFAULT).metadata(metadata).build(); + OperationModeUpdateTask task = OperationModeUpdateTask.slmMode(requestMode); + ClusterState newState = task.execute(state); if (assertSameClusterState) { assertSame(state, newState); } else { assertThat(state, not(equalTo(newState))); } - IndexLifecycleMetadata newMetadata = newState.metadata().custom(IndexLifecycleMetadata.TYPE); - assertThat(newMetadata.getPolicyMetadatas(), equalTo(indexLifecycleMetadata.getPolicyMetadatas())); - return newMetadata.getOperationMode(); + LifecycleOperationMetadata newMetadata = newState.metadata().custom(LifecycleOperationMetadata.TYPE); + SnapshotLifecycleMetadata oldMetadata = newState.metadata().custom(SnapshotLifecycleMetadata.TYPE, SnapshotLifecycleMetadata.EMPTY); + return Optional.ofNullable(newMetadata) + .map(LifecycleOperationMetadata::getSLMOperationMode) + .orElseGet(oldMetadata::getOperationMode); } } From 38f3b634c5352b7ed02513372b6a6cefca80e664 Mon Sep 17 00:00:00 2001 From: Przemyslaw Gomulka Date: Tue, 13 Dec 2022 16:41:13 +0100 Subject: [PATCH 254/919] Settings api for stable plugins (#91467) Stable plugins do not have a dependency on server, therefore cannot access Settings, NodeSettings or IndexSettings classes. Plugins implementing new stable plugin api will use set of annotations to mark an interface that works a as a facade for settings used by their plugin. This will allow to validate the values provided against the restrictions defined in the plugin's settings interface This commit introduces set of annotations in libs/plugin-api that allow to annotate an interface in plugins that will be later injected into a plugin instance. These annotations can possibly be used not only by analysis plugins in the future. The implementation of the interface generated in server is using dynamic proxy mechanism. relates #88980 --- docs/changelog/91467.yaml | 5 + .../plugin-api/src/main/java/module-info.java | 1 + .../org/elasticsearch/plugin/api/Inject.java | 23 ++ .../plugin/api/settings/AnalysisSettings.java | 23 ++ .../plugin/api/settings/BooleanSetting.java | 31 ++ .../plugin/api/settings/IntSetting.java | 31 ++ .../plugin/api/settings/ListSetting.java | 26 ++ .../plugin/api/settings/LongSetting.java | 31 ++ .../plugin/api/settings/StringSetting.java | 31 ++ .../wrappers/SettingsInvocationHandler.java | 76 +++++ .../analysis/wrappers/StableApiWrappers.java | 47 ++- .../indices/TransportAnalyzeActionTests.java | 2 +- .../indices/analysis/AnalysisModuleTests.java | 263 +--------------- .../IncorrectSetupStablePluginsTests.java | 155 +++++++++ .../StableAnalysisPluginsNoSettingsTests.java | 232 ++++++++++++++ ...tableAnalysisPluginsWithSettingsTests.java | 296 ++++++++++++++++++ .../analysis/lucene/AppendCharFilter.java | 43 +++ .../analysis/lucene/AppendTokenFilter.java | 52 +++ .../analysis/lucene/ReplaceCharToNumber.java | 27 ++ .../analysis/lucene/SkipTokenFilter.java | 31 ++ .../analysis/lucene/TestTokenizer.java | 29 ++ .../wrappers/StableApiWrappersTests.java | 2 +- 22 files changed, 1190 insertions(+), 267 deletions(-) create mode 100644 docs/changelog/91467.yaml create mode 100644 libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/Inject.java create mode 100644 libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/AnalysisSettings.java create mode 100644 libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/BooleanSetting.java create mode 100644 libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/IntSetting.java create mode 100644 libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/ListSetting.java create mode 100644 libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/LongSetting.java create mode 100644 libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/StringSetting.java create mode 100644 server/src/main/java/org/elasticsearch/indices/analysis/wrappers/SettingsInvocationHandler.java create mode 100644 server/src/test/java/org/elasticsearch/indices/analysis/IncorrectSetupStablePluginsTests.java create mode 100644 server/src/test/java/org/elasticsearch/indices/analysis/StableAnalysisPluginsNoSettingsTests.java create mode 100644 server/src/test/java/org/elasticsearch/indices/analysis/StableAnalysisPluginsWithSettingsTests.java create mode 100644 server/src/test/java/org/elasticsearch/indices/analysis/lucene/AppendCharFilter.java create mode 100644 server/src/test/java/org/elasticsearch/indices/analysis/lucene/AppendTokenFilter.java create mode 100644 server/src/test/java/org/elasticsearch/indices/analysis/lucene/ReplaceCharToNumber.java create mode 100644 server/src/test/java/org/elasticsearch/indices/analysis/lucene/SkipTokenFilter.java create mode 100644 server/src/test/java/org/elasticsearch/indices/analysis/lucene/TestTokenizer.java diff --git a/docs/changelog/91467.yaml b/docs/changelog/91467.yaml new file mode 100644 index 000000000000..ebf7d2fcd248 --- /dev/null +++ b/docs/changelog/91467.yaml @@ -0,0 +1,5 @@ +pr: 91467 +summary: Settings api for stable plugins +area: Infra/Plugins +type: enhancement +issues: [] diff --git a/libs/plugin-api/src/main/java/module-info.java b/libs/plugin-api/src/main/java/module-info.java index 28a6c9319cef..1eadd7907a07 100644 --- a/libs/plugin-api/src/main/java/module-info.java +++ b/libs/plugin-api/src/main/java/module-info.java @@ -8,4 +8,5 @@ module org.elasticsearch.plugin.api { exports org.elasticsearch.plugin.api; + exports org.elasticsearch.plugin.api.settings; } diff --git a/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/Inject.java b/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/Inject.java new file mode 100644 index 000000000000..953670be5a39 --- /dev/null +++ b/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/Inject.java @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.plugin.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation used to mark constructor to inject plugin dependencies iee. settings. + * A constructor parameter has to be an interface marked with appropriate annotation (i.e AnalysisSetting) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.CONSTRUCTOR) +public @interface Inject { +} diff --git a/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/AnalysisSettings.java b/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/AnalysisSettings.java new file mode 100644 index 000000000000..c38c4e666657 --- /dev/null +++ b/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/AnalysisSettings.java @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.plugin.api.settings; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; + +/** + * An annotation used to mark analysis setting interface + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { TYPE }) +public @interface AnalysisSettings { +} diff --git a/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/BooleanSetting.java b/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/BooleanSetting.java new file mode 100644 index 000000000000..a3e6ccee64ba --- /dev/null +++ b/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/BooleanSetting.java @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.plugin.api.settings; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation used to mark a setting of type Boolean + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface BooleanSetting { + /** + * A name of a setting + */ + String path(); + + /** + * A default value of a boolean setting + */ + boolean defaultValue(); +} diff --git a/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/IntSetting.java b/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/IntSetting.java new file mode 100644 index 000000000000..3afd5290ef24 --- /dev/null +++ b/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/IntSetting.java @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.plugin.api.settings; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation used to mark a setting of type integer + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface IntSetting { + /** + * A name of a setting + */ + String path(); + + /** + * A default value of an int setting + */ + int defaultValue(); +} diff --git a/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/ListSetting.java b/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/ListSetting.java new file mode 100644 index 000000000000..16295fd12a50 --- /dev/null +++ b/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/ListSetting.java @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.plugin.api.settings; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation used to mark a setting of type list. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface ListSetting { + /** + * A name of a setting + */ + String path(); +} diff --git a/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/LongSetting.java b/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/LongSetting.java new file mode 100644 index 000000000000..b5d0ad43846b --- /dev/null +++ b/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/LongSetting.java @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.plugin.api.settings; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation used to mark a setting of type Long + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface LongSetting { + /** + * A name of a setting + */ + String path(); + + /** + * A default value of a long setting + */ + long defaultValue(); +} diff --git a/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/StringSetting.java b/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/StringSetting.java new file mode 100644 index 000000000000..8e916c2f1e13 --- /dev/null +++ b/libs/plugin-api/src/main/java/org/elasticsearch/plugin/api/settings/StringSetting.java @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.plugin.api.settings; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation used to mark a setting of type String + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface StringSetting { + /** + * A name of a setting + */ + String path(); + + /** + * A default value of a String setting + */ + String defaultValue(); +} diff --git a/server/src/main/java/org/elasticsearch/indices/analysis/wrappers/SettingsInvocationHandler.java b/server/src/main/java/org/elasticsearch/indices/analysis/wrappers/SettingsInvocationHandler.java new file mode 100644 index 000000000000..477514961286 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/indices/analysis/wrappers/SettingsInvocationHandler.java @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.indices.analysis.wrappers; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.plugin.api.settings.BooleanSetting; +import org.elasticsearch.plugin.api.settings.IntSetting; +import org.elasticsearch.plugin.api.settings.ListSetting; +import org.elasticsearch.plugin.api.settings.LongSetting; +import org.elasticsearch.plugin.api.settings.StringSetting; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Collections; +import java.util.function.Function; + +public class SettingsInvocationHandler implements InvocationHandler { + + private static Logger LOGGER = LogManager.getLogger(SettingsInvocationHandler.class); + private Settings settings; + private Environment environment; + + public SettingsInvocationHandler(Settings settings, Environment environment) { + this.settings = settings; + this.environment = environment; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static T create(Settings settings, Class parameterType, Environment environment) { + return (T) Proxy.newProxyInstance( + parameterType.getClassLoader(), + new Class[] { parameterType }, + new SettingsInvocationHandler(settings, environment) + ); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + assert method.getAnnotations().length == 1; + Annotation annotation = method.getAnnotations()[0]; + + if (annotation instanceof IntSetting setting) { + return getValue(Integer::valueOf, setting.path(), setting.defaultValue()); + } else if (annotation instanceof LongSetting setting) { + return getValue(Long::valueOf, setting.path(), setting.defaultValue()); + } else if (annotation instanceof BooleanSetting setting) { + return getValue(Boolean::valueOf, setting.path(), setting.defaultValue()); + } else if (annotation instanceof StringSetting setting) { + return getValue(String::valueOf, setting.path(), setting.defaultValue()); + } else if (annotation instanceof ListSetting setting) { + return settings.getAsList(setting.path(), Collections.emptyList()); + } else { + throw new IllegalArgumentException("Unrecognised annotation " + annotation); + } + } + + private T getValue(Function parser, String path, T defaultValue) { + String key = path; + if (settings.get(key) != null) { + return parser.apply(settings.get(key)); + } + return defaultValue; + } + +} diff --git a/server/src/main/java/org/elasticsearch/indices/analysis/wrappers/StableApiWrappers.java b/server/src/main/java/org/elasticsearch/indices/analysis/wrappers/StableApiWrappers.java index 771d3012a242..5e613b64270f 100644 --- a/server/src/main/java/org/elasticsearch/indices/analysis/wrappers/StableApiWrappers.java +++ b/server/src/main/java/org/elasticsearch/indices/analysis/wrappers/StableApiWrappers.java @@ -15,6 +15,8 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.indices.analysis.AnalysisModule; +import org.elasticsearch.plugin.api.Inject; +import org.elasticsearch.plugin.api.settings.AnalysisSettings; import org.elasticsearch.plugins.scanners.PluginInfo; import org.elasticsearch.plugins.scanners.StablePluginsRegistry; @@ -204,10 +206,47 @@ private static T createInstance( Environment environment ) { try { - Constructor constructor = clazz.getConstructor(); - return constructor.newInstance(); - } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new IllegalStateException("cannot create instance of " + clazz, e); + + Constructor[] constructors = clazz.getConstructors(); + if (constructors.length > 1) { + throw new IllegalStateException("Plugin can only have one public constructor."); + } + Constructor constructor = constructors[0]; + if (constructor.getParameterCount() == 0) { + return (T) constructor.newInstance(); + } else { + Inject inject = constructor.getAnnotation(Inject.class); + if (inject != null) { + Class[] parameterTypes = constructor.getParameterTypes(); + Object[] parameters = new Object[parameterTypes.length]; + for (int i = 0; i < parameterTypes.length; i++) { + Object settings = createSettings(parameterTypes[i], indexSettings, nodeSettings, analysisSettings, environment); + parameters[i] = settings; + } + return (T) constructor.newInstance(parameters); + } else { + throw new IllegalStateException("Missing @Inject annotation for constructor with settings."); + } + } + + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException("Cannot create instance of " + clazz, e); + } + + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static T createSettings( + Class settingsClass, + IndexSettings indexSettings, + Settings nodeSettings, + Settings analysisSettings, + Environment environment + ) { + if (settingsClass.getAnnotationsByType(AnalysisSettings.class).length > 0) { + return SettingsInvocationHandler.create(analysisSettings, settingsClass, environment); } + + throw new IllegalArgumentException("Parameter is not instance of a class annotated with settings annotation."); } } diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/TransportAnalyzeActionTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/TransportAnalyzeActionTests.java index f5a1340c17cb..b940d33cf824 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/TransportAnalyzeActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/TransportAnalyzeActionTests.java @@ -32,7 +32,7 @@ import org.elasticsearch.index.analysis.TokenizerFactory; import org.elasticsearch.indices.analysis.AnalysisModule; import org.elasticsearch.indices.analysis.AnalysisModule.AnalysisProvider; -import org.elasticsearch.indices.analysis.AnalysisModuleTests.AppendCharFilter; +import org.elasticsearch.indices.analysis.lucene.AppendCharFilter; import org.elasticsearch.plugins.AnalysisPlugin; import org.elasticsearch.plugins.scanners.StablePluginsRegistry; import org.elasticsearch.test.ESTestCase; diff --git a/server/src/test/java/org/elasticsearch/indices/analysis/AnalysisModuleTests.java b/server/src/test/java/org/elasticsearch/indices/analysis/AnalysisModuleTests.java index 68913d53c4f0..ef07c5efe43c 100644 --- a/server/src/test/java/org/elasticsearch/indices/analysis/AnalysisModuleTests.java +++ b/server/src/test/java/org/elasticsearch/indices/analysis/AnalysisModuleTests.java @@ -9,23 +9,14 @@ package org.elasticsearch.indices.analysis; import org.apache.lucene.analysis.Analyzer; -import org.apache.lucene.analysis.CharFilter; -import org.apache.lucene.analysis.FilteringTokenFilter; -import org.apache.lucene.analysis.TokenFilter; -import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.Tokenizer; -import org.apache.lucene.analysis.charfilter.MappingCharFilter; -import org.apache.lucene.analysis.charfilter.NormalizeCharMap; import org.apache.lucene.analysis.hunspell.Dictionary; import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; -import org.apache.lucene.analysis.util.CharTokenizer; import org.apache.lucene.store.Directory; import org.apache.lucene.tests.analysis.MockTokenizer; import org.elasticsearch.Version; import org.elasticsearch.cluster.metadata.IndexMetadata; -import org.elasticsearch.common.io.Streams; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.index.IndexSettings; @@ -43,12 +34,9 @@ import org.elasticsearch.index.analysis.TokenFilterFactory; import org.elasticsearch.index.analysis.TokenizerFactory; import org.elasticsearch.indices.analysis.AnalysisModule.AnalysisProvider; -import org.elasticsearch.plugin.analysis.api.AnalysisMode; -import org.elasticsearch.plugin.api.NamedComponent; +import org.elasticsearch.indices.analysis.lucene.AppendCharFilter; +import org.elasticsearch.indices.analysis.lucene.AppendTokenFilter; import org.elasticsearch.plugins.AnalysisPlugin; -import org.elasticsearch.plugins.scanners.NameToPluginInfo; -import org.elasticsearch.plugins.scanners.NamedComponentReader; -import org.elasticsearch.plugins.scanners.PluginInfo; import org.elasticsearch.plugins.scanners.StablePluginsRegistry; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.IndexSettingsModule; @@ -59,9 +47,6 @@ import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; -import java.io.Reader; -import java.io.StringReader; -import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -70,7 +55,6 @@ import java.util.Map; import java.util.Set; -import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents; @@ -476,247 +460,4 @@ public Map getHunspellDictionaries() { assertSame(dictionary, module.getHunspellService().getDictionary("foo")); } - @NamedComponent("stableCharFilterFactory") - public static class TestCharFilterFactory implements org.elasticsearch.plugin.analysis.api.CharFilterFactory { - @SuppressForbidden(reason = "need a public constructor") - public TestCharFilterFactory() {} - - @Override - public Reader create(Reader reader) { - return new ReplaceHash(reader); - } - - @Override - public Reader normalize(Reader reader) { - return new ReplaceHash(reader); - } - - } - - static class ReplaceHash extends MappingCharFilter { - - ReplaceHash(Reader in) { - super(charMap(), in); - } - - private static NormalizeCharMap charMap() { - NormalizeCharMap.Builder builder = new NormalizeCharMap.Builder(); - builder.add("#", "3"); - return builder.build(); - } - } - - @NamedComponent("stableTokenFilterFactory") - public static class TestTokenFilterFactory implements org.elasticsearch.plugin.analysis.api.TokenFilterFactory { - - @SuppressForbidden(reason = "need a public constructor") - public TestTokenFilterFactory() {} - - @Override - public TokenStream create(TokenStream tokenStream) { - - return new Skip1TokenFilter(tokenStream); - } - - @Override - public TokenStream normalize(TokenStream tokenStream) { - return new AppendTokenFilter(tokenStream, "1"); - } - - @Override - public AnalysisMode getAnalysisMode() { - return org.elasticsearch.plugin.analysis.api.TokenFilterFactory.super.getAnalysisMode(); - } - - } - - static class Skip1TokenFilter extends FilteringTokenFilter { - - private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class); - - Skip1TokenFilter(TokenStream in) { - super(in); - } - - @Override - protected boolean accept() throws IOException { - return termAtt.buffer()[0] != '1'; - } - } - - @NamedComponent("stableTokenizerFactory") - public static class TestTokenizerFactory implements org.elasticsearch.plugin.analysis.api.TokenizerFactory { - @SuppressForbidden(reason = "need a public constructor") - public TestTokenizerFactory() {} - - @Override - public Tokenizer create() { - return new UnderscoreTokenizer(); - } - - } - - static class UnderscoreTokenizer extends CharTokenizer { - - @Override - protected boolean isTokenChar(int c) { - return c != '_'; - } - } - - @NamedComponent("stableAnalyzerFactory") - public static class TestAnalyzerFactory implements org.elasticsearch.plugin.analysis.api.AnalyzerFactory { - - @Override - public Analyzer create() { - return new CustomAnalyzer(); - } - - static class CustomAnalyzer extends Analyzer { - - @Override - protected TokenStreamComponents createComponents(String fieldName) { - var tokenizer = new UnderscoreTokenizer(); - var tokenFilter = new Skip1TokenFilter(tokenizer); - return new TokenStreamComponents(r -> tokenizer.setReader(new ReplaceHash(r)), tokenFilter); - } - } - } - - public void testStablePlugins() throws IOException { - ClassLoader classLoader = getClass().getClassLoader(); - AnalysisRegistry registry = new AnalysisModule( - TestEnvironment.newEnvironment(emptyNodeSettings), - emptyList(), - new StablePluginsRegistry( - new NamedComponentReader(), - Map.of( - org.elasticsearch.plugin.analysis.api.CharFilterFactory.class.getCanonicalName(), - new NameToPluginInfo( - Map.of( - "stableCharFilterFactory", - new PluginInfo("stableCharFilterFactory", TestCharFilterFactory.class.getName(), classLoader) - ) - ), - org.elasticsearch.plugin.analysis.api.TokenFilterFactory.class.getCanonicalName(), - new NameToPluginInfo( - Map.of( - "stableTokenFilterFactory", - new PluginInfo("stableTokenFilterFactory", TestTokenFilterFactory.class.getName(), classLoader) - ) - ), - org.elasticsearch.plugin.analysis.api.TokenizerFactory.class.getCanonicalName(), - new NameToPluginInfo( - Map.of( - "stableTokenizerFactory", - new PluginInfo("stableTokenizerFactory", TestTokenizerFactory.class.getName(), classLoader) - ) - ), - org.elasticsearch.plugin.analysis.api.AnalyzerFactory.class.getCanonicalName(), - new NameToPluginInfo( - Map.of( - "stableAnalyzerFactory", - new PluginInfo("stableAnalyzerFactory", TestAnalyzerFactory.class.getName(), classLoader) - ) - ) - ) - ) - ).getAnalysisRegistry(); - - Version version = VersionUtils.randomVersion(random()); - IndexAnalyzers analyzers = getIndexAnalyzers( - registry, - Settings.builder() - .put("index.analysis.analyzer.char_filter_test.tokenizer", "standard") - .put("index.analysis.analyzer.char_filter_test.char_filter", "stableCharFilterFactory") - - .put("index.analysis.analyzer.token_filter_test.tokenizer", "standard") - .put("index.analysis.analyzer.token_filter_test.filter", "stableTokenFilterFactory") - - .put("index.analysis.analyzer.tokenizer_test.tokenizer", "stableTokenizerFactory") - - .put("index.analysis.analyzer.analyzer_provider_test.type", "stableAnalyzerFactory") - - .put(IndexMetadata.SETTING_VERSION_CREATED, version) - .build() - ); - assertTokenStreamContents(analyzers.get("char_filter_test").tokenStream("", "t#st"), new String[] { "t3st" }); - assertTokenStreamContents( - analyzers.get("token_filter_test").tokenStream("", "1test 2test 1test 3test "), - new String[] { "2test", "3test" } - ); - assertTokenStreamContents(analyzers.get("tokenizer_test").tokenStream("", "x_y_z"), new String[] { "x", "y", "z" }); - assertTokenStreamContents(analyzers.get("analyzer_provider_test").tokenStream("", "1x_y_#z"), new String[] { "y", "3z" }); - - assertThat(analyzers.get("char_filter_test").normalize("", "t#st").utf8ToString(), equalTo("t3st")); - assertThat( - analyzers.get("token_filter_test").normalize("", "1test 2test 1test 3test ").utf8ToString(), - equalTo("1test 2test 1test 3test 1") - ); - - // TODO does it makes sense to test normalize on tokenizer and analyzer? - } - - // Simple char filter that appends text to the term - public static class AppendCharFilter extends CharFilter { - - static Reader append(Reader input, String appendMe) { - try { - return new StringReader(Streams.copyToString(input) + appendMe); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - public AppendCharFilter(Reader input, String appendMe) { - super(append(input, appendMe)); - } - - @Override - protected int correct(int currentOff) { - return currentOff; - } - - @Override - public int read(char[] cbuf, int off, int len) throws IOException { - return input.read(cbuf, off, len); - } - } - - // Simple token filter that appends text to the term - private static class AppendTokenFilter extends TokenFilter { - public static TokenFilterFactory factoryForSuffix(String suffix) { - return new TokenFilterFactory() { - @Override - public String name() { - return suffix; - } - - @Override - public TokenStream create(TokenStream tokenStream) { - return new AppendTokenFilter(tokenStream, suffix); - } - }; - } - - private final CharTermAttribute term = addAttribute(CharTermAttribute.class); - private final char[] appendMe; - - protected AppendTokenFilter(TokenStream input, String appendMe) { - super(input); - this.appendMe = appendMe.toCharArray(); - } - - @Override - public boolean incrementToken() throws IOException { - if (false == input.incrementToken()) { - return false; - } - term.resizeBuffer(term.length() + appendMe.length); - System.arraycopy(appendMe, 0, term.buffer(), term.length(), appendMe.length); - term.setLength(term.length() + appendMe.length); - return true; - } - } - } diff --git a/server/src/test/java/org/elasticsearch/indices/analysis/IncorrectSetupStablePluginsTests.java b/server/src/test/java/org/elasticsearch/indices/analysis/IncorrectSetupStablePluginsTests.java new file mode 100644 index 000000000000..ee8ef8aa4542 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/indices/analysis/IncorrectSetupStablePluginsTests.java @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.indices.analysis; + +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.analysis.AnalysisRegistry; +import org.elasticsearch.index.analysis.IndexAnalyzers; +import org.elasticsearch.indices.analysis.lucene.ReplaceCharToNumber; +import org.elasticsearch.plugin.api.Inject; +import org.elasticsearch.plugin.api.NamedComponent; +import org.elasticsearch.plugin.api.settings.AnalysisSettings; +import org.elasticsearch.plugins.scanners.NameToPluginInfo; +import org.elasticsearch.plugins.scanners.NamedComponentReader; +import org.elasticsearch.plugins.scanners.PluginInfo; +import org.elasticsearch.plugins.scanners.StablePluginsRegistry; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.IndexSettingsModule; +import org.elasticsearch.test.VersionUtils; + +import java.io.IOException; +import java.io.Reader; +import java.util.Map; + +import static java.util.Collections.emptyList; +import static org.hamcrest.Matchers.equalTo; + +public class IncorrectSetupStablePluginsTests extends ESTestCase { + ClassLoader classLoader = getClass().getClassLoader(); + + private final Settings emptyNodeSettings = Settings.builder() + .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toString()) + .build(); + + public @interface IncorrectAnnotation { + } + + @IncorrectAnnotation + public interface IncorrectlyAnnotatedSettings {} + + @NamedComponent("incorrectlyAnnotatedSettings") + public static class IncorrectlyAnnotatedSettingsCharFilter extends AbstractCharFilterFactory { + @Inject + public IncorrectlyAnnotatedSettingsCharFilter(IncorrectlyAnnotatedSettings settings) {} + } + + public void testIncorrectlyAnnotatedSettingsClass() throws IOException { + var e = expectThrows( + IllegalArgumentException.class, + () -> getIndexAnalyzers( + Settings.builder() + .put("index.analysis.analyzer.char_filter_test.tokenizer", "standard") + .put("index.analysis.analyzer.char_filter_test.char_filter", "incorrectlyAnnotatedSettings") + .put(IndexMetadata.SETTING_VERSION_CREATED, VersionUtils.randomVersion(random())) + .build(), + Map.of( + "incorrectlyAnnotatedSettings", + new PluginInfo("incorrectlyAnnotatedSettings", IncorrectlyAnnotatedSettingsCharFilter.class.getName(), classLoader) + ) + ) + ); + assertThat(e.getMessage(), equalTo("Parameter is not instance of a class annotated with settings annotation.")); + } + + @AnalysisSettings + public interface OkAnalysisSettings {} + + @NamedComponent("noInjectCharFilter") + public static class NoInjectCharFilter extends AbstractCharFilterFactory { + + public NoInjectCharFilter(OkAnalysisSettings settings) {} + } + + public void testIncorrectlyAnnotatedConstructor() throws IOException { + var e = expectThrows( + IllegalStateException.class, + () -> getIndexAnalyzers( + Settings.builder() + .put("index.analysis.analyzer.char_filter_test.tokenizer", "standard") + .put("index.analysis.analyzer.char_filter_test.char_filter", "noInjectCharFilter") + .put(IndexMetadata.SETTING_VERSION_CREATED, VersionUtils.randomVersion(random())) + .build(), + Map.of("noInjectCharFilter", new PluginInfo("noInjectCharFilter", NoInjectCharFilter.class.getName(), classLoader)) + ) + ); + assertThat(e.getMessage(), equalTo("Missing @Inject annotation for constructor with settings.")); + } + + @NamedComponent("multipleConstructors") + public static class MultipleConstructors extends AbstractCharFilterFactory { + public MultipleConstructors() {} + + public MultipleConstructors(OkAnalysisSettings settings) {} + } + + public void testMultiplePublicConstructors() throws IOException { + var e = expectThrows( + IllegalStateException.class, + () -> getIndexAnalyzers( + Settings.builder() + .put("index.analysis.analyzer.char_filter_test.tokenizer", "standard") + .put("index.analysis.analyzer.char_filter_test.char_filter", "multipleConstructors") + .put(IndexMetadata.SETTING_VERSION_CREATED, VersionUtils.randomVersion(random())) + .build(), + Map.of("multipleConstructors", new PluginInfo("multipleConstructors", MultipleConstructors.class.getName(), classLoader)) + ) + ); + assertThat(e.getMessage(), equalTo("Plugin can only have one public constructor.")); + } + + public IndexAnalyzers getIndexAnalyzers(Settings settings, Map mapOfCharFilters) throws IOException { + AnalysisRegistry registry = setupRegistry(mapOfCharFilters); + + IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("test", settings); + return registry.build(idxSettings); + } + + private AnalysisRegistry setupRegistry(Map mapOfCharFilters) throws IOException { + + AnalysisRegistry registry = new AnalysisModule( + TestEnvironment.newEnvironment(emptyNodeSettings), + emptyList(), + new StablePluginsRegistry( + new NamedComponentReader(), + Map.of( + org.elasticsearch.plugin.analysis.api.CharFilterFactory.class.getCanonicalName(), + new NameToPluginInfo(mapOfCharFilters) + ) + ) + ).getAnalysisRegistry(); + return registry; + } + + public abstract static class AbstractCharFilterFactory implements org.elasticsearch.plugin.analysis.api.CharFilterFactory { + + @Override + public Reader create(Reader reader) { + return new ReplaceCharToNumber(reader, "#", 3); + } + + @Override + public Reader normalize(Reader reader) { + return new ReplaceCharToNumber(reader, "#", 3); + } + } +} diff --git a/server/src/test/java/org/elasticsearch/indices/analysis/StableAnalysisPluginsNoSettingsTests.java b/server/src/test/java/org/elasticsearch/indices/analysis/StableAnalysisPluginsNoSettingsTests.java new file mode 100644 index 000000000000..dd3c49b4b8f5 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/indices/analysis/StableAnalysisPluginsNoSettingsTests.java @@ -0,0 +1,232 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.indices.analysis; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.FilteringTokenFilter; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.Tokenizer; +import org.apache.lucene.analysis.charfilter.MappingCharFilter; +import org.apache.lucene.analysis.charfilter.NormalizeCharMap; +import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; +import org.apache.lucene.analysis.util.CharTokenizer; +import org.elasticsearch.Version; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.analysis.AnalysisRegistry; +import org.elasticsearch.index.analysis.IndexAnalyzers; +import org.elasticsearch.indices.analysis.lucene.AppendTokenFilter; +import org.elasticsearch.plugin.analysis.api.AnalysisMode; +import org.elasticsearch.plugin.api.NamedComponent; +import org.elasticsearch.plugins.scanners.NameToPluginInfo; +import org.elasticsearch.plugins.scanners.NamedComponentReader; +import org.elasticsearch.plugins.scanners.PluginInfo; +import org.elasticsearch.plugins.scanners.StablePluginsRegistry; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.IndexSettingsModule; +import org.elasticsearch.test.VersionUtils; + +import java.io.IOException; +import java.io.Reader; +import java.util.Map; + +import static java.util.Collections.emptyList; +import static org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents; +import static org.hamcrest.Matchers.equalTo; + +public class StableAnalysisPluginsNoSettingsTests extends ESTestCase { + private final Settings emptyNodeSettings = Settings.builder() + .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toString()) + .build(); + + public IndexAnalyzers getIndexAnalyzers(Settings settings) throws IOException { + AnalysisRegistry registry = setupRegistry(); + + IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("test", settings); + return registry.build(idxSettings); + } + + public void testStablePlugins() throws IOException { + Version version = VersionUtils.randomVersion(random()); + IndexAnalyzers analyzers = getIndexAnalyzers( + Settings.builder() + .put("index.analysis.analyzer.char_filter_test.tokenizer", "standard") + .put("index.analysis.analyzer.char_filter_test.char_filter", "stableCharFilterFactory") + + .put("index.analysis.analyzer.token_filter_test.tokenizer", "standard") + .put("index.analysis.analyzer.token_filter_test.filter", "stableTokenFilterFactory") + + .put("index.analysis.analyzer.tokenizer_test.tokenizer", "stableTokenizerFactory") + + .put("index.analysis.analyzer.analyzer_provider_test.type", "stableAnalyzerFactory") + + .put(IndexMetadata.SETTING_VERSION_CREATED, version) + .build() + ); + assertTokenStreamContents(analyzers.get("char_filter_test").tokenStream("", "t#st"), new String[] { "t3st" }); + assertTokenStreamContents( + analyzers.get("token_filter_test").tokenStream("", "1test 2test 1test 3test "), + new String[] { "2test", "3test" } + ); + assertTokenStreamContents(analyzers.get("tokenizer_test").tokenStream("", "x_y_z"), new String[] { "x", "y", "z" }); + assertTokenStreamContents(analyzers.get("analyzer_provider_test").tokenStream("", "1x_y_#z"), new String[] { "y", "3z" }); + + assertThat(analyzers.get("char_filter_test").normalize("", "t#st").utf8ToString(), equalTo("t3st")); + assertThat( + analyzers.get("token_filter_test").normalize("", "1test 2test 1test 3test ").utf8ToString(), + equalTo("1test 2test 1test 3test 1") + ); + } + + @NamedComponent("stableCharFilterFactory") + public static class TestCharFilterFactory implements org.elasticsearch.plugin.analysis.api.CharFilterFactory { + + @Override + public Reader create(Reader reader) { + return new ReplaceHash(reader); + } + + @Override + public Reader normalize(Reader reader) { + return new ReplaceHash(reader); + } + + } + + static class ReplaceHash extends MappingCharFilter { + + ReplaceHash(Reader in) { + super(charMap(), in); + } + + private static NormalizeCharMap charMap() { + NormalizeCharMap.Builder builder = new NormalizeCharMap.Builder(); + builder.add("#", "3"); + return builder.build(); + } + } + + @NamedComponent("stableTokenFilterFactory") + public static class TestTokenFilterFactory implements org.elasticsearch.plugin.analysis.api.TokenFilterFactory { + + @Override + public TokenStream create(TokenStream tokenStream) { + + return new Skip1TokenFilter(tokenStream); + } + + @Override + public TokenStream normalize(TokenStream tokenStream) { + return new AppendTokenFilter(tokenStream, "1"); + } + + @Override + public AnalysisMode getAnalysisMode() { + return org.elasticsearch.plugin.analysis.api.TokenFilterFactory.super.getAnalysisMode(); + } + + } + + static class Skip1TokenFilter extends FilteringTokenFilter { + + private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class); + + Skip1TokenFilter(TokenStream in) { + super(in); + } + + @Override + protected boolean accept() throws IOException { + return termAtt.buffer()[0] != '1'; + } + } + + @NamedComponent("stableTokenizerFactory") + public static class TestTokenizerFactory implements org.elasticsearch.plugin.analysis.api.TokenizerFactory { + + @Override + public Tokenizer create() { + return new UnderscoreTokenizer(); + } + + } + + static class UnderscoreTokenizer extends CharTokenizer { + + @Override + protected boolean isTokenChar(int c) { + return c != '_'; + } + } + + @NamedComponent("stableAnalyzerFactory") + public static class TestAnalyzerFactory implements org.elasticsearch.plugin.analysis.api.AnalyzerFactory { + + @Override + public Analyzer create() { + return new CustomAnalyzer(); + } + + static class CustomAnalyzer extends Analyzer { + + @Override + protected TokenStreamComponents createComponents(String fieldName) { + var tokenizer = new UnderscoreTokenizer(); + var tokenFilter = new Skip1TokenFilter(tokenizer); + return new TokenStreamComponents(r -> tokenizer.setReader(new ReplaceHash(r)), tokenFilter); + } + } + } + + private AnalysisRegistry setupRegistry() throws IOException { + ClassLoader classLoader = getClass().getClassLoader(); + + AnalysisRegistry registry = new AnalysisModule( + TestEnvironment.newEnvironment(emptyNodeSettings), + emptyList(), + new StablePluginsRegistry( + new NamedComponentReader(), + Map.of( + org.elasticsearch.plugin.analysis.api.CharFilterFactory.class.getCanonicalName(), + new NameToPluginInfo( + Map.of( + "stableCharFilterFactory", + new PluginInfo("stableCharFilterFactory", TestCharFilterFactory.class.getName(), classLoader) + ) + ), + org.elasticsearch.plugin.analysis.api.TokenFilterFactory.class.getCanonicalName(), + new NameToPluginInfo( + Map.of( + "stableTokenFilterFactory", + new PluginInfo("stableTokenFilterFactory", TestTokenFilterFactory.class.getName(), classLoader) + ) + ), + org.elasticsearch.plugin.analysis.api.TokenizerFactory.class.getCanonicalName(), + new NameToPluginInfo( + Map.of( + "stableTokenizerFactory", + new PluginInfo("stableTokenizerFactory", TestTokenizerFactory.class.getName(), classLoader) + ) + ), + org.elasticsearch.plugin.analysis.api.AnalyzerFactory.class.getCanonicalName(), + new NameToPluginInfo( + Map.of( + "stableAnalyzerFactory", + new PluginInfo("stableAnalyzerFactory", TestAnalyzerFactory.class.getName(), classLoader) + ) + ) + ) + ) + ).getAnalysisRegistry(); + return registry; + } +} diff --git a/server/src/test/java/org/elasticsearch/indices/analysis/StableAnalysisPluginsWithSettingsTests.java b/server/src/test/java/org/elasticsearch/indices/analysis/StableAnalysisPluginsWithSettingsTests.java new file mode 100644 index 000000000000..f6deda90faa1 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/indices/analysis/StableAnalysisPluginsWithSettingsTests.java @@ -0,0 +1,296 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.indices.analysis; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.Tokenizer; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.analysis.AnalysisRegistry; +import org.elasticsearch.index.analysis.IndexAnalyzers; +import org.elasticsearch.indices.analysis.lucene.AppendTokenFilter; +import org.elasticsearch.indices.analysis.lucene.ReplaceCharToNumber; +import org.elasticsearch.indices.analysis.lucene.SkipTokenFilter; +import org.elasticsearch.indices.analysis.lucene.TestTokenizer; +import org.elasticsearch.plugin.analysis.api.AnalysisMode; +import org.elasticsearch.plugin.api.Inject; +import org.elasticsearch.plugin.api.NamedComponent; +import org.elasticsearch.plugin.api.settings.AnalysisSettings; +import org.elasticsearch.plugin.api.settings.BooleanSetting; +import org.elasticsearch.plugin.api.settings.IntSetting; +import org.elasticsearch.plugin.api.settings.ListSetting; +import org.elasticsearch.plugin.api.settings.LongSetting; +import org.elasticsearch.plugin.api.settings.StringSetting; +import org.elasticsearch.plugins.scanners.NameToPluginInfo; +import org.elasticsearch.plugins.scanners.NamedComponentReader; +import org.elasticsearch.plugins.scanners.PluginInfo; +import org.elasticsearch.plugins.scanners.StablePluginsRegistry; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.IndexSettingsModule; +import org.elasticsearch.test.VersionUtils; + +import java.io.IOException; +import java.io.Reader; +import java.util.Map; + +import static java.util.Collections.emptyList; +import static org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents; +import static org.hamcrest.Matchers.equalTo; + +public class StableAnalysisPluginsWithSettingsTests extends ESTestCase { + + protected final Settings emptyNodeSettings = Settings.builder() + .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toString()) + .build(); + + public void testCharFilters() throws IOException { + IndexAnalyzers analyzers = getIndexAnalyzers( + Settings.builder() + .put("index.analysis.char_filter.my_char_filter.type", "stableCharFilterFactory") + .put("index.analysis.char_filter.my_char_filter.old_char", "#") + .put("index.analysis.char_filter.my_char_filter.new_number", 3) + + .put("index.analysis.analyzer.char_filter_test.tokenizer", "standard") + .put("index.analysis.analyzer.char_filter_test.char_filter", "my_char_filter") + + .put("index.analysis.analyzer.char_filter_with_defaults_test.tokenizer", "standard") + .put("index.analysis.analyzer.char_filter_with_defaults_test.char_filter", "stableCharFilterFactory") + + .put(IndexMetadata.SETTING_VERSION_CREATED, VersionUtils.randomVersion(random())) + .build() + ); + assertTokenStreamContents(analyzers.get("char_filter_test").tokenStream("", "t#st"), new String[] { "t3st" }); + assertTokenStreamContents(analyzers.get("char_filter_with_defaults_test").tokenStream("", "t t"), new String[] { "t0t" }); + assertThat(analyzers.get("char_filter_test").normalize("", "t#st").utf8ToString(), equalTo("t3st")); + } + + public void testTokenFilters() throws IOException { + IndexAnalyzers analyzers = getIndexAnalyzers( + Settings.builder() + .put("index.analysis.filter.my_token_filter.type", "stableTokenFilterFactory") + .put("index.analysis.filter.my_token_filter.token_filter_number", 1L) + + .put("index.analysis.analyzer.token_filter_test.tokenizer", "standard") + .put("index.analysis.analyzer.token_filter_test.filter", "my_token_filter") + .put(IndexMetadata.SETTING_VERSION_CREATED, VersionUtils.randomVersion(random())) + .build() + ); + assertTokenStreamContents( + analyzers.get("token_filter_test").tokenStream("", "1test 2test 1test 3test "), + new String[] { "2test", "3test" } + ); + + assertThat( + analyzers.get("token_filter_test").normalize("", "1test 2test 1test 3test ").utf8ToString(), + equalTo("1test 2test 1test 3test 1") + ); + } + + public void testTokenizer() throws IOException { + IndexAnalyzers analyzers = getIndexAnalyzers( + Settings.builder() + .put("index.analysis.tokenizer.my_tokenizer.type", "stableTokenizerFactory") + .putList("index.analysis.tokenizer.my_tokenizer.tokenizer_list_of_chars", "_", " ") + + .put("index.analysis.analyzer.tokenizer_test.tokenizer", "my_tokenizer") + .put(IndexMetadata.SETTING_VERSION_CREATED, VersionUtils.randomVersion(random())) + .build() + ); + assertTokenStreamContents(analyzers.get("tokenizer_test").tokenStream("", "x_y z"), new String[] { "x", "y", "z" }); + } + + public void testAnalyzer() throws IOException { + IndexAnalyzers analyzers = getIndexAnalyzers( + Settings.builder() + .put("index.analysis.analyzer.analyzer_provider_test.type", "stableAnalyzerFactory") + .putList("index.analysis.analyzer.analyzer_provider_test.tokenizer_list_of_chars", "_", " ") + .put("index.analysis.analyzer.analyzer_provider_test.token_filter_number", 1L) + .put("index.analysis.analyzer.analyzer_provider_test.old_char", "#") + .put("index.analysis.analyzer.analyzer_provider_test.new_number", 3) + .put("index.analysis.analyzer.analyzer_provider_test.analyzerUseTokenListOfChars", true) + .put(IndexMetadata.SETTING_VERSION_CREATED, VersionUtils.randomVersion(random())) + .build() + ); + assertTokenStreamContents(analyzers.get("analyzer_provider_test").tokenStream("", "1x_y_#z"), new String[] { "y", "3z" }); + } + + protected IndexAnalyzers getIndexAnalyzers(Settings settings) throws IOException { + AnalysisRegistry registry = setupRegistry(); + + IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("test", settings); + return registry.build(idxSettings); + } + + private AnalysisRegistry setupRegistry() throws IOException { + ClassLoader classLoader = getClass().getClassLoader(); + + AnalysisRegistry registry = new AnalysisModule( + TestEnvironment.newEnvironment(emptyNodeSettings), + emptyList(), + new StablePluginsRegistry( + new NamedComponentReader(), + Map.of( + org.elasticsearch.plugin.analysis.api.CharFilterFactory.class.getCanonicalName(), + new NameToPluginInfo( + Map.of( + "stableCharFilterFactory", + new PluginInfo("stableCharFilterFactory", TestCharFilterFactory.class.getName(), classLoader) + ) + ), + org.elasticsearch.plugin.analysis.api.TokenFilterFactory.class.getCanonicalName(), + new NameToPluginInfo( + Map.of( + "stableTokenFilterFactory", + new PluginInfo("stableTokenFilterFactory", TestTokenFilterFactory.class.getName(), classLoader) + ) + ), + org.elasticsearch.plugin.analysis.api.TokenizerFactory.class.getCanonicalName(), + new NameToPluginInfo( + Map.of( + "stableTokenizerFactory", + new PluginInfo("stableTokenizerFactory", TestTokenizerFactory.class.getName(), classLoader) + ) + ), + org.elasticsearch.plugin.analysis.api.AnalyzerFactory.class.getCanonicalName(), + new NameToPluginInfo( + Map.of( + "stableAnalyzerFactory", + new PluginInfo("stableAnalyzerFactory", TestAnalyzerFactory.class.getName(), classLoader) + ) + ) + ) + ) + ).getAnalysisRegistry(); + return registry; + } + + @AnalysisSettings + public interface TestAnalysisSettings { + @StringSetting(path = "old_char", defaultValue = " ") + String oldChar(); + + @IntSetting(path = "new_number", defaultValue = 0) + int newNumber(); + + @LongSetting(path = "token_filter_number", defaultValue = 0L) + long tokenFilterNumber(); + + @ListSetting(path = "tokenizer_list_of_chars") + java.util.List tokenizerListOfChars(); + + @BooleanSetting(path = "analyzerUseTokenListOfChars", defaultValue = false) + boolean analyzerUseTokenListOfChars(); + } + + @NamedComponent("stableAnalyzerFactory") + public static class TestAnalyzerFactory implements org.elasticsearch.plugin.analysis.api.AnalyzerFactory { + + private final TestAnalysisSettings settings; + + @Inject + public TestAnalyzerFactory(TestAnalysisSettings settings) { + this.settings = settings; + } + + @Override + public Analyzer create() { + return new CustomAnalyzer(settings); + } + + static class CustomAnalyzer extends Analyzer { + + private final TestAnalysisSettings settings; + + CustomAnalyzer(TestAnalysisSettings settings) { + this.settings = settings; + } + + @Override + protected TokenStreamComponents createComponents(String fieldName) { + var tokenizer = new TestTokenizer(settings.tokenizerListOfChars()); + long tokenFilterNumber = settings.analyzerUseTokenListOfChars() ? settings.tokenFilterNumber() : -1; + var tokenFilter = new SkipTokenFilter(tokenizer, tokenFilterNumber); + return new TokenStreamComponents( + r -> tokenizer.setReader(new ReplaceCharToNumber(r, settings.oldChar(), settings.newNumber())), + tokenFilter + ); + } + } + } + + @NamedComponent("stableCharFilterFactory") + public static class TestCharFilterFactory implements org.elasticsearch.plugin.analysis.api.CharFilterFactory { + private final String oldChar; + private final int newNumber; + + @Inject + public TestCharFilterFactory(TestAnalysisSettings settings) { + oldChar = settings.oldChar(); + newNumber = settings.newNumber(); + } + + @Override + public Reader create(Reader reader) { + return new ReplaceCharToNumber(reader, oldChar, newNumber); + } + + @Override + public Reader normalize(Reader reader) { + return new ReplaceCharToNumber(reader, oldChar, newNumber); + } + + } + + @NamedComponent("stableTokenFilterFactory") + public static class TestTokenFilterFactory implements org.elasticsearch.plugin.analysis.api.TokenFilterFactory { + + private final long tokenFilterNumber; + + @Inject + public TestTokenFilterFactory(TestAnalysisSettings settings) { + this.tokenFilterNumber = settings.tokenFilterNumber(); + } + + @Override + public TokenStream create(TokenStream tokenStream) { + return new SkipTokenFilter(tokenStream, tokenFilterNumber); + } + + @Override + public TokenStream normalize(TokenStream tokenStream) { + return new AppendTokenFilter(tokenStream, String.valueOf(tokenFilterNumber)); + } + + @Override + public AnalysisMode getAnalysisMode() { + return org.elasticsearch.plugin.analysis.api.TokenFilterFactory.super.getAnalysisMode(); + } + + } + + @NamedComponent("stableTokenizerFactory") + public static class TestTokenizerFactory implements org.elasticsearch.plugin.analysis.api.TokenizerFactory { + private final java.util.List tokenizerListOfChars; + + @Inject + public TestTokenizerFactory(TestAnalysisSettings settings) { + this.tokenizerListOfChars = settings.tokenizerListOfChars(); + } + + @Override + public Tokenizer create() { + return new TestTokenizer(tokenizerListOfChars); + } + + } +} diff --git a/server/src/test/java/org/elasticsearch/indices/analysis/lucene/AppendCharFilter.java b/server/src/test/java/org/elasticsearch/indices/analysis/lucene/AppendCharFilter.java new file mode 100644 index 000000000000..ce04642a88bf --- /dev/null +++ b/server/src/test/java/org/elasticsearch/indices/analysis/lucene/AppendCharFilter.java @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.indices.analysis.lucene; + +import org.apache.lucene.analysis.CharFilter; +import org.elasticsearch.common.io.Streams; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.UncheckedIOException; + +// Simple char filter that appends text to the term +public class AppendCharFilter extends CharFilter { + + static Reader append(Reader input, String appendMe) { + try { + return new StringReader(Streams.copyToString(input) + appendMe); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public AppendCharFilter(Reader input, String appendMe) { + super(append(input, appendMe)); + } + + @Override + protected int correct(int currentOff) { + return currentOff; + } + + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + return input.read(cbuf, off, len); + } +} diff --git a/server/src/test/java/org/elasticsearch/indices/analysis/lucene/AppendTokenFilter.java b/server/src/test/java/org/elasticsearch/indices/analysis/lucene/AppendTokenFilter.java new file mode 100644 index 000000000000..0a45604deebf --- /dev/null +++ b/server/src/test/java/org/elasticsearch/indices/analysis/lucene/AppendTokenFilter.java @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.indices.analysis.lucene; + +import org.apache.lucene.analysis.TokenFilter; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; +import org.elasticsearch.index.analysis.TokenFilterFactory; + +import java.io.IOException; + +// Simple token filter that appends text to the term +public final class AppendTokenFilter extends TokenFilter { + public static TokenFilterFactory factoryForSuffix(String suffix) { + return new TokenFilterFactory() { + @Override + public String name() { + return suffix; + } + + @Override + public TokenStream create(TokenStream tokenStream) { + return new AppendTokenFilter(tokenStream, suffix); + } + }; + } + + private final CharTermAttribute term = addAttribute(CharTermAttribute.class); + private final char[] appendMe; + + public AppendTokenFilter(TokenStream input, String appendMe) { + super(input); + this.appendMe = appendMe.toCharArray(); + } + + @Override + public boolean incrementToken() throws IOException { + if (false == input.incrementToken()) { + return false; + } + term.resizeBuffer(term.length() + appendMe.length); + System.arraycopy(appendMe, 0, term.buffer(), term.length(), appendMe.length); + term.setLength(term.length() + appendMe.length); + return true; + } +} diff --git a/server/src/test/java/org/elasticsearch/indices/analysis/lucene/ReplaceCharToNumber.java b/server/src/test/java/org/elasticsearch/indices/analysis/lucene/ReplaceCharToNumber.java new file mode 100644 index 000000000000..19d4e6f28ed0 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/indices/analysis/lucene/ReplaceCharToNumber.java @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.indices.analysis.lucene; + +import org.apache.lucene.analysis.charfilter.MappingCharFilter; +import org.apache.lucene.analysis.charfilter.NormalizeCharMap; + +import java.io.Reader; + +public class ReplaceCharToNumber extends MappingCharFilter { + + public ReplaceCharToNumber(Reader in, String oldChar, int newNumber) { + super(charMap(oldChar, newNumber), in); + } + + private static NormalizeCharMap charMap(String oldChar, int newNumber) { + NormalizeCharMap.Builder builder = new NormalizeCharMap.Builder(); + builder.add(oldChar, String.valueOf(newNumber)); + return builder.build(); + } +} diff --git a/server/src/test/java/org/elasticsearch/indices/analysis/lucene/SkipTokenFilter.java b/server/src/test/java/org/elasticsearch/indices/analysis/lucene/SkipTokenFilter.java new file mode 100644 index 000000000000..a2010cb93d5a --- /dev/null +++ b/server/src/test/java/org/elasticsearch/indices/analysis/lucene/SkipTokenFilter.java @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.indices.analysis.lucene; + +import org.apache.lucene.analysis.FilteringTokenFilter; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; + +import java.io.IOException; + +public class SkipTokenFilter extends FilteringTokenFilter { + + private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class); + private final long tokenFilterNumber; + + public SkipTokenFilter(TokenStream in, long tokenFilterNumber) { + super(in); + this.tokenFilterNumber = tokenFilterNumber; + } + + @Override + protected boolean accept() throws IOException { + return termAtt.buffer()[0] != (char) (tokenFilterNumber + '0'); + } +} diff --git a/server/src/test/java/org/elasticsearch/indices/analysis/lucene/TestTokenizer.java b/server/src/test/java/org/elasticsearch/indices/analysis/lucene/TestTokenizer.java new file mode 100644 index 000000000000..70fe2683709b --- /dev/null +++ b/server/src/test/java/org/elasticsearch/indices/analysis/lucene/TestTokenizer.java @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.indices.analysis.lucene; + +import org.apache.lucene.analysis.util.CharTokenizer; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class TestTokenizer extends CharTokenizer { + + private final Set setOfChars; + + public TestTokenizer(List tokenizerListOfChars) { + this.setOfChars = tokenizerListOfChars.stream().map(s -> (int) s.charAt(0)).collect(Collectors.toSet()); + } + + @Override + protected boolean isTokenChar(int c) { + return setOfChars.contains(c) == false; + } +} diff --git a/server/src/test/java/org/elasticsearch/indices/analysis/wrappers/StableApiWrappersTests.java b/server/src/test/java/org/elasticsearch/indices/analysis/wrappers/StableApiWrappersTests.java index 508d0c77f6f8..7414f816e3da 100644 --- a/server/src/test/java/org/elasticsearch/indices/analysis/wrappers/StableApiWrappersTests.java +++ b/server/src/test/java/org/elasticsearch/indices/analysis/wrappers/StableApiWrappersTests.java @@ -74,7 +74,7 @@ public void testStablePluginHasNoArgConstructor() throws IOException { IllegalStateException.class, () -> oldTokenFilter.get(null, mock(Environment.class), null, null) ); - assertThat(illegalStateException.getCause(), instanceOf(NoSuchMethodException.class)); + assertThat(illegalStateException.getMessage(), equalTo("Missing @Inject annotation for constructor with settings.")); } public void testAnalyzerFactoryDelegation() throws IOException { From 143fe5b1c754029f5360c9dbfee6807309a7bd9e Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Tue, 13 Dec 2022 12:42:04 -0500 Subject: [PATCH 255/919] [ML] fix tokenization bug when handling normalization in BERT and MPNet (#92329) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is a small bug in how we handle normalization and accent stripping in BERT and MPNet. The bug may return something like this: ``` 'java.lang.IllegalArgumentException: startOffset must be non-negative, and endOffset must be >= startOffset; got startOffset=553,endOffset=552', 'caused_by': {'type': 'illegal_argument_exception', 'reason': 'startOffset must be non-negative, and endOffset must be >= startOffset; got startOffset=553,endOffset=552' ``` The cause of the bug is when we normalize strings, then attempt to split them on a punctuation mark. Since we don't really handle offset changes on normalization, splitting on punctuation could cause some weirdness. Here is an example token: `Br창n's`. In the old way, we would normalize the string (which decomposes `창` thus increasing the character count), and then split on `'`. Since the decomposition increased the character count, our offsets could break as we calculate them based on character count (which is now increased). To simplify the logic, I changed normalization to be done AFTER our split on punctuation. This way our offsets still refer to the original input offsets and if we add or even remove characters in normalization, it is of no consequence as our punctuation split is already handled. closes: https://github.com/elastic/elasticsearch/issues/92243 --- docs/changelog/92329.yaml | 5 ++++ .../nlp/tokenizers/BasicTokenFilter.java | 28 ++++++++++-------- .../nlp/tokenizers/BertTokenizerTests.java | 29 +++++++++++++++++++ 3 files changed, 50 insertions(+), 12 deletions(-) create mode 100644 docs/changelog/92329.yaml diff --git a/docs/changelog/92329.yaml b/docs/changelog/92329.yaml new file mode 100644 index 000000000000..5eab808b7ca9 --- /dev/null +++ b/docs/changelog/92329.yaml @@ -0,0 +1,5 @@ +pr: 92329 +summary: Fix tokenization bug when handling normalization in BERT and MPNet +area: Machine Learning +type: bug +issues: [] diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/BasicTokenFilter.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/BasicTokenFilter.java index 3be4eded9989..c5eb8f024f0d 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/BasicTokenFilter.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/BasicTokenFilter.java @@ -118,19 +118,19 @@ public boolean incrementToken() throws IOException { } current = null; // not really needed, but for safety if (input.incrementToken()) { - if (isStripAccents) { - stripAccent(); - } if (neverSplitSet.contains(termAtt)) { return true; } // split punctuation and maybe cjk chars!!! LinkedList splits = split(); - // There is nothing to merge, nothing to store, simply return - if (splits.size() == 1) { - return true; + LinkedList delimitedTokens = mergeSplits(splits); + if (isStripAccents) { + for (DelimitedToken token : delimitedTokens) { + tokens.add(stripAccent(token)); + } + } else { + tokens.addAll(delimitedTokens); } - tokens.addAll(mergeSplits(splits)); this.current = captureState(); DelimitedToken token = tokens.removeFirst(); termAtt.setEmpty().append(token.charSequence()); @@ -140,14 +140,14 @@ public boolean incrementToken() throws IOException { return false; } - private void stripAccent() { + private DelimitedToken stripAccent(DelimitedToken token) { accentBuffer.setLength(0); boolean changed = false; - if (normalizer.quickCheck(termAtt) != Normalizer.YES) { - normalizer.normalize(termAtt, accentBuffer); + if (normalizer.quickCheck(token.charSequence()) != Normalizer.YES) { + normalizer.normalize(token.charSequence(), accentBuffer); changed = true; } else { - accentBuffer.append(termAtt); + accentBuffer.append(token.charSequence()); } List badIndices = new ArrayList<>(); List charCount = new ArrayList<>(); @@ -172,8 +172,9 @@ private void stripAccent() { } } if (changed) { - termAtt.setEmpty().append(accentBuffer); + return new DelimitedToken(accentBuffer.toString(), token.startOffset(), token.endOffset()); } + return token; } private LinkedList split() { @@ -210,6 +211,9 @@ private LinkedList split() { } private LinkedList mergeSplits(LinkedList splits) { + if (splits.size() == 1) { + return splits; + } LinkedList mergedTokens = new LinkedList<>(); List matchingTokens = new ArrayList<>(); CharSeqTokenTrieNode current = neverSplit; diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/BertTokenizerTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/BertTokenizerTests.java index 3bb46ec1cedb..4a0ac4f72ca9 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/BertTokenizerTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/BertTokenizerTests.java @@ -67,6 +67,35 @@ public void testTokenize() { } } + public void testTokenizeFailureCaseAccentFilter() { + List testingVocab = List.of( + "[CLS]", + "br", + "##ᄎ", + "##ᅡ", + "##ᆼ", + "##n", + "'", + "s", + "[SEP]", + BertTokenizer.MASK_TOKEN, + BertTokenizer.UNKNOWN_TOKEN, + BertTokenizer.PAD_TOKEN + ); + try ( + BertTokenizer tokenizer = BertTokenizer.builder( + testingVocab, + new BertTokenization(true, true, 512, Tokenization.Truncate.FIRST, -1) + ).build() + ) { + TokenizationResult.Tokens tokenization = tokenizer.tokenize("Br창n's", Tokenization.Truncate.NONE, -1, 0).get(0); + assertThat(tokenization.tokenIds(), equalTo(new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 })); + + tokenization = tokenizer.tokenize("Br창n", Tokenization.Truncate.NONE, -1, 0).get(0); + assertThat(tokenization.tokenIds(), equalTo(new int[] { 0, 1, 2, 3, 4, 5, 8 })); + } + } + public void testTokenizeLargeInputNoTruncation() { try ( BertTokenizer tokenizer = BertTokenizer.builder( From f04c7519972aa057c99f51b088b4d4479927199a Mon Sep 17 00:00:00 2001 From: Lee Hinman Date: Tue, 13 Dec 2022 11:34:32 -0700 Subject: [PATCH 256/919] Mark simulate index API non-experimental in documentation (#92331) This has been stable for a long time, we just missed removing the experimental[] tag in the docs. --- docs/reference/indices/simulate-index.asciidoc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/reference/indices/simulate-index.asciidoc b/docs/reference/indices/simulate-index.asciidoc index 54a9b36f83d1..e86b724332d7 100644 --- a/docs/reference/indices/simulate-index.asciidoc +++ b/docs/reference/indices/simulate-index.asciidoc @@ -4,9 +4,7 @@ Simulate index ++++ -experimental[] - -Returns the index configuration that would be applied to the specified index from an +Returns the index configuration that would be applied to the specified index from an existing <>. //// From a0bdd6582f21048e27bee619721f4e0fecaef929 Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Tue, 13 Dec 2022 14:30:22 -0800 Subject: [PATCH 257/919] Ensure reproducible builds and task avoidance when using x-content jar (#92212) The elasticsearch-x-content jar is special in that it embeds other runtime dependencies under an IMPL-JARS folder in the jar. This has side-effects with Gradle runtime classpath normalization. This commit fixes some isssues her by firstly, sorting the content of the jar listing manifest so it's reproducible across filesystems, and secondly ignoreing nested jar manifests for the purposes of runtime classpath normalization, as they often contain things like timestamps and commit hashes. --- .../gradle/internal/ElasticsearchJavaBasePlugin.java | 1 + libs/x-content/build.gradle | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchJavaBasePlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchJavaBasePlugin.java index 9dfc37d744b3..7a5bead71fb0 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchJavaBasePlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchJavaBasePlugin.java @@ -146,6 +146,7 @@ public static void configureCompile(Project project) { */ public static void configureInputNormalization(Project project) { project.getNormalization().getRuntimeClasspath().ignore("META-INF/MANIFEST.MF"); + project.getNormalization().getRuntimeClasspath().ignore("IMPL-JARS/**/META-INF/MANIFEST.MF"); } private static Provider releaseVersionProviderFromCompileTask(Project project, AbstractCompile compileTask) { diff --git a/libs/x-content/build.gradle b/libs/x-content/build.gradle index bf110b9adb53..958e818e019d 100644 --- a/libs/x-content/build.gradle +++ b/libs/x-content/build.gradle @@ -74,7 +74,7 @@ def generateProviderManifest = tasks.register("generateProviderManifest") { doLast { manifestFile.parentFile.mkdirs() manifestFile.setText(configurations.providerImpl.files.stream() - .map(f -> f.name).collect(Collectors.joining('\n')), 'UTF-8') + .map(f -> f.name).sorted().collect(Collectors.joining('\n')), 'UTF-8') } } From 0e774a3bbd2822f0bba506c64b36fad68ce28613 Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Wed, 14 Dec 2022 08:07:21 +0100 Subject: [PATCH 258/919] Lazy evaluate shouldPeriodicallyFlush() (#92325) --- .../java/org/elasticsearch/index/engine/InternalEngine.java | 3 +-- .../java/org/elasticsearch/index/engine/EngineTestCase.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java index 9deb51cb6188..ad2896683024 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -1952,10 +1952,9 @@ public boolean flush(boolean force, boolean waitIfOngoing) throws EngineExceptio // newly created commit points to a different translog generation (can free translog), // or (4) the local checkpoint information in the last commit is stale, which slows down future recoveries. boolean hasUncommittedChanges = indexWriter.hasUncommittedChanges(); - boolean shouldPeriodicallyFlush = shouldPeriodicallyFlush(); if (hasUncommittedChanges || force - || shouldPeriodicallyFlush + || shouldPeriodicallyFlush() || getProcessedLocalCheckpoint() > Long.parseLong( lastCommittedSegmentInfos.userData.get(SequenceNumbers.LOCAL_CHECKPOINT_KEY) )) { diff --git a/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java index 9156a8c3b242..af469d232eca 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java @@ -1628,7 +1628,7 @@ private static LazySoftDeletesDirectoryReaderWrapper.LazyBits lazyBits(LeafReade throw new IllegalStateException("Can not extract lazy bits from given index reader [" + reader + "]"); } - static CodecService newCodecService() { + protected static CodecService newCodecService() { return new CodecService(null, BigArrays.NON_RECYCLING_INSTANCE); } } From 07377ea20376d68a3e315a614e680cb4bd541333 Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Wed, 14 Dec 2022 08:59:31 +0000 Subject: [PATCH 259/919] Redact another HTTP header in APM traces (#92337) There's another auth-related header that we need to omit when capturing HTTP spans with APM. --- server/src/main/java/org/elasticsearch/rest/RestController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/rest/RestController.java b/server/src/main/java/org/elasticsearch/rest/RestController.java index c314c4dbc82d..f1dbb5489c61 100644 --- a/server/src/main/java/org/elasticsearch/rest/RestController.java +++ b/server/src/main/java/org/elasticsearch/rest/RestController.java @@ -465,7 +465,7 @@ private void startTrace(ThreadContext threadContext, RestChannel channel, String req.getHeaders().forEach((key, values) -> { final String lowerKey = key.toLowerCase(Locale.ROOT).replace('-', '_'); final String value = switch (lowerKey) { - case "authorization", "cookie", "secret", "session", "set_cookie", "token" -> "[REDACTED]"; + case "authorization", "cookie", "secret", "session", "set_cookie", "token", "x_elastic_app_auth" -> "[REDACTED]"; default -> String.join("; ", values); }; attributes.put("http.request.headers." + lowerKey, value); From dfe9d765ec7a0ff668027bf8375eb01cfab098db Mon Sep 17 00:00:00 2001 From: Hendrik Muhs Date: Wed, 14 Dec 2022 11:45:03 +0100 Subject: [PATCH 260/919] [ML] Frequent items sometimes reports sub sets (#92239) depending on order the frequent item set collector reported sets which are sub sets of sets already collected. Those should not be reported again. This PR implements an additional super set check in which case the super set replaces the sub set in the collector queue The PR adds test coverage to test certain data distributions, that's how this issue was found. It contains already testing code for an upcoming optimization. --- docs/changelog/92239.yaml | 5 + .../frequentitemsets/EclatMapReducer.java | 4 + .../FrequentItemSetCollector.java | 90 ++-- .../aggs/frequentitemsets/ItemSetBitSet.java | 43 +- .../EclatMapReducerTests.java | 488 ++++++++++++++++++ .../FrequentItemSetCollectorTests.java | 14 + .../frequentitemsets/ItemSetBitSetTests.java | 17 +- 7 files changed, 614 insertions(+), 47 deletions(-) create mode 100644 docs/changelog/92239.yaml create mode 100644 x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/EclatMapReducerTests.java diff --git a/docs/changelog/92239.yaml b/docs/changelog/92239.yaml new file mode 100644 index 000000000000..7cb621973752 --- /dev/null +++ b/docs/changelog/92239.yaml @@ -0,0 +1,5 @@ +pr: 92239 +summary: Stop the `frequent_items` aggregation reporting a subset when a superset exists +area: Machine Learning +type: bug +issues: [] diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/EclatMapReducer.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/EclatMapReducer.java index 7ba54033c2ed..78fe1caf7f82 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/EclatMapReducer.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/EclatMapReducer.java @@ -90,6 +90,10 @@ FrequentItemSet[] getFrequentItemSets() { return frequentItemSets; } + Map getProfilingInfo() { + return profilingInfo; + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeArray(frequentItemSets); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetCollector.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetCollector.java index 1ceb5935ae2c..0b04240e8970 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetCollector.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetCollector.java @@ -39,7 +39,7 @@ * * Technically this is implemented as priority queue which is implemented as heap with the last item on top. * - * To get to the top-n results pop the collector until it is empty and than reverse the order. + * To get to the top-n results pop the collector until it is empty and then reverse the order. */ public final class FrequentItemSetCollector { @@ -177,6 +177,7 @@ FrequentItemSet toFrequentItemSet(List fields) throws IOException { int pos = items.nextSetBit(0); while (pos > 0) { Tuple item = transactionStore.getItem(topItemIds.getItemIdAt(pos - 1)); + assert item.v1() < fields.size() : "item id exceed number of given items, did you configure eclat correctly?"; final Field field = fields.get(item.v1()); Object formattedValue = field.formatValue(item.v2()); String fieldName = fields.get(item.v1()).getName(); @@ -288,27 +289,42 @@ public long add(ItemSetBitSet itemSet, long docCount) { return queue.top().getDocCount(); } - // closed set criteria: don't add if we already store a superset - if (hasSuperSet(itemSet, docCount)) { - logger.trace("skip itemset with super set"); - return queue.size() < size ? min : queue.top().getDocCount(); + /** + * Criteria for closed sets + * + * An item set is called closed if no superset has the same support. + * e.g. + * + * [cat, dog, crocodile] -> 0.2 + * [cat, dog, crocodile, river] -> 0.2 + * + * [cat, dog, crocodile] gets skipped, because [cat, dog, crocodile, river] has the same support. + * if [cat, dog, crocodile] was added first, [cat, dog, crocodile, river] replaces [cat, dog, crocodile] + */ + List setsThatShareSameDocCountBits = frequentItemsByCount.get(docCount); + + if (setsThatShareSameDocCountBits != null) { + for (FrequentItemSetCandidate otherSet : setsThatShareSameDocCountBits) { + ItemSetBitSet.SetRelation relation = itemSet.setRelation(otherSet.getItems()); + + if (relation.equals(ItemSetBitSet.SetRelation.SUPER_SET)) { + removeFromFrequentItemsByCount(otherSet); + queue.remove(otherSet); + break; + } + + if (relation.equals(ItemSetBitSet.SetRelation.SUB_SET)) { + // closed set criteria: don't add if we already store a superset + return queue.size() < size ? min : queue.top().getDocCount(); + } + } } spareSet.reset(count++, itemSet, docCount); FrequentItemSetCandidate newItemSet = spareSet; FrequentItemSetCandidate removedItemSet = queue.insertWithOverflow(spareSet); if (removedItemSet != null) { - // remove item from frequentItemsByCount - frequentItemsByCount.compute(removedItemSet.getDocCount(), (k, sets) -> { - - // short cut, if there is only 1, it must be the one we are looking for - if (sets.size() == 1) { - return null; - } - - sets.remove(removedItemSet); - return sets; - }); + removeFromFrequentItemsByCount(removedItemSet); spareSet = removedItemSet; } else { spareSet = new FrequentItemSetCandidate(); @@ -319,6 +335,20 @@ public long add(ItemSetBitSet itemSet, long docCount) { return queue.size() < size ? min : queue.top().getDocCount(); } + private void removeFromFrequentItemsByCount(FrequentItemSetCandidate removedItemSet) { + // remove item from frequentItemsByCount + frequentItemsByCount.compute(removedItemSet.getDocCount(), (k, sets) -> { + + // short cut, if there is only 1, it must be the one we are looking for + if (sets.size() == 1) { + return null; + } + + sets.remove(removedItemSet); + return sets; + }); + } + // for unit tests Map> getFrequentItemsByCount() { return frequentItemsByCount; @@ -331,32 +361,4 @@ FrequentItemSetPriorityQueue getQueue() { FrequentItemSetCandidate getLastSet() { return queue.top(); } - - /** - * Criteria for closed sets - * - * A item set is called closed if no superset has the same support. - * - * E.g. - * - * [cat, dog, crocodile] -> 0.2 - * [cat, dog, crocodile, river] -> 0.2 - * - * [cat, dog, crocodile] gets skipped, because [cat, dog, crocodile, river] has the same support. - * - */ - private boolean hasSuperSet(ItemSetBitSet itemSetBitSet, long docCount) { - List setsThatShareSameDocCountBits = frequentItemsByCount.get(docCount); - - if (setsThatShareSameDocCountBits != null) { - for (FrequentItemSetCandidate otherSet : setsThatShareSameDocCountBits) { - if (itemSetBitSet.isSubset(otherSet.getItems())) { - return true; - } - } - } - - return false; - } - } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/ItemSetBitSet.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/ItemSetBitSet.java index a2006550011b..ed1e26a37cbb 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/ItemSetBitSet.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/ItemSetBitSet.java @@ -25,6 +25,13 @@ */ class ItemSetBitSet implements Cloneable { + public enum SetRelation { + DISJOINT_OR_INTERSECT, // we don't care if disjoint or intersect + SUB_SET, + SUPER_SET, + EQUAL + } + // taken from {@code BitSet} private static final int ADDRESS_BITS_PER_WORD = 6; private static final int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD; @@ -110,12 +117,44 @@ boolean isSubset(ItemSetBitSet set) { return false; } - for (int i = wordsInUse - 1; i >= 0; i--) - if ((words[i] & set.words[i]) != words[i]) return false; + for (int i = wordsInUse - 1; i >= 0; i--) { + if ((words[i] & set.words[i]) != words[i]) { + return false; + } + } return true; } + public SetRelation setRelation(ItemSetBitSet set) { + // this method is performance critical, change carefully + + if (wordsInUse > set.wordsInUse || cardinality > set.cardinality) { + for (int i = set.wordsInUse - 1; i >= 0; i--) { + if ((set.words[i] & words[i]) != set.words[i]) { + return SetRelation.DISJOINT_OR_INTERSECT; + } + } + return SetRelation.SUPER_SET; + } else if (wordsInUse < set.wordsInUse || cardinality < set.cardinality) { + for (int i = wordsInUse - 1; i >= 0; i--) { + if ((words[i] & set.words[i]) != words[i]) { + return SetRelation.DISJOINT_OR_INTERSECT; + } + } + return SetRelation.SUB_SET; + } + + // both bitsets have the same wordsInUse and cardinality, so they can be either equal or not + for (int i = wordsInUse - 1; i >= 0; i--) { + if ((words[i] & set.words[i]) != words[i]) { + return SetRelation.DISJOINT_OR_INTERSECT; + } + } + + return SetRelation.EQUAL; + } + int nextSetBit(int fromIndex) { if (fromIndex < 0) throw new IndexOutOfBoundsException("fromIndex < 0: " + fromIndex); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/EclatMapReducerTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/EclatMapReducerTests.java new file mode 100644 index 000000000000..8280f431423d --- /dev/null +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/EclatMapReducerTests.java @@ -0,0 +1,488 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.ml.aggs.frequentitemsets; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.util.MockBigArrays; +import org.elasticsearch.common.util.MockPageCacheRecycler; +import org.elasticsearch.core.Tuple; +import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.ml.aggs.frequentitemsets.mr.ItemSetMapReduceValueSource.Field; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.elasticsearch.core.Tuple.tuple; +import static org.elasticsearch.xpack.ml.aggs.frequentitemsets.mr.ItemSetMapReduceValueSourceTests.createKeywordFieldTestInstance; +import static org.hamcrest.Matchers.equalTo; + +public class EclatMapReducerTests extends ESTestCase { + + private static Supplier doNotCancelSupplier = () -> false; + + public void testSimple() throws IOException { + Field field1 = createKeywordFieldTestInstance("keyword1", 0); + Field field2 = createKeywordFieldTestInstance("keyword2", 1); + Field field3 = createKeywordFieldTestInstance("keyword3", 2); + + EclatMapReducer eclat = new EclatMapReducer(getTestName(), 0.1, 2, 10, true); + + HashBasedTransactionStore transactionStore = eclat.mapInit(mockBigArrays()); + eclat.map(mockOneDocument(List.of(tuple(field1, "f1-a"), tuple(field2, "f2-1"), tuple(field3, "f3-A"))), transactionStore); + eclat.map(mockOneDocument(List.of(tuple(field1, "f1-a"), tuple(field2, "f2-1"), tuple(field3, "f3-B"))), transactionStore); + eclat.map(mockOneDocument(List.of(tuple(field1, "f1-a"), tuple(field2, "f2-1"), tuple(field3, "f3-C"))), transactionStore); + eclat.map(mockOneDocument(List.of(tuple(field1, "f1-a"), tuple(field2, "f2-1"), tuple(field3, "f3-D"))), transactionStore); + eclat.map(mockOneDocument(List.of(tuple(field1, "f1-a"), tuple(field2, "f2-1"), tuple(field3, "f3-E"))), transactionStore); + eclat.map(mockOneDocument(List.of(tuple(field1, "f1-b"), tuple(field2, "f2-1"), tuple(field3, "f3-F"))), transactionStore); + eclat.map(mockOneDocument(List.of(tuple(field1, "f1-b"), tuple(field2, "f2-1"), tuple(field3, "f3-G"))), transactionStore); + eclat.map(mockOneDocument(List.of(tuple(field1, "f1-c"), tuple(field2, "f2-1"), tuple(field3, "f3-H"))), transactionStore); + eclat.map(mockOneDocument(List.of(tuple(field1, "f1-d"), tuple(field2, "f2-1"), tuple(field3, "f3-I"))), transactionStore); + eclat.map(mockOneDocument(List.of(tuple(field1, "f1-b"), tuple(field2, "f2-1"), tuple(field3, "f3-J"))), transactionStore); + eclat.map(mockOneDocument(List.of(tuple(field1, "f1-f"), tuple(field2, "f2-1"), tuple(field3, "f3-K"))), transactionStore); + eclat.map(mockOneDocument(List.of(tuple(field1, "f1-a"), tuple(field2, "f2-1"), tuple(field3, "f3-L"))), transactionStore); + + EclatMapReducer.EclatResult result = runEclat(eclat, List.of(field1, field2, field3), transactionStore); + assertThat(result.getFrequentItemSets().length, equalTo(2)); + assertThat(result.getFrequentItemSets()[0].getSupport(), equalTo(0.5)); + assertThat(result.getFrequentItemSets()[1].getSupport(), equalTo(0.25)); + assertThat(result.getProfilingInfo().get("unique_items_after_reduce"), equalTo(18L)); + assertThat(result.getProfilingInfo().get("total_transactions_after_reduce"), equalTo(12L)); + assertThat(result.getProfilingInfo().get("total_items_after_reduce"), equalTo(36L)); + } + + public void testPruneToNextMainBranch() throws IOException { + Field field1 = createKeywordFieldTestInstance("keyword1", 0); + Field field2 = createKeywordFieldTestInstance("keyword2", 1); + Field field3 = createKeywordFieldTestInstance("keyword3", 2); + + // create 3 fields that "follow" field 2 + Field field2a = createKeywordFieldTestInstance("keyword2a", 3); + Field field2b = createKeywordFieldTestInstance("keyword2b", 4); + Field field2c = createKeywordFieldTestInstance("keyword2c", 5); + + EclatMapReducer eclat = new EclatMapReducer(getTestName(), 0.1, 2, 10, true); + + HashBasedTransactionStore transactionStore = eclat.mapInit(mockBigArrays()); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-a"), + tuple(field2, "f2-1"), + tuple(field3, "f3-A"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-a"), + tuple(field2, "f2-1"), + tuple(field3, "f3-B"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-a"), + tuple(field2, "f2-1"), + tuple(field3, "f3-C"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-a"), + tuple(field2, "f2-1"), + tuple(field3, "f3-D"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-a"), + tuple(field2, "f2-1"), + tuple(field3, "f3-E"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-b"), + tuple(field2, "f2-1"), + tuple(field3, "f3-F"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-b"), + tuple(field2, "f2-1"), + tuple(field3, "f3-G"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-c"), + tuple(field2, "f2-1"), + tuple(field3, "f3-H"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-d"), + tuple(field2, "f2-1"), + tuple(field3, "f3-I"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-b"), + tuple(field2, "f2-1"), + tuple(field3, "f3-J"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-f"), + tuple(field2, "f2-1"), + tuple(field3, "f3-K"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-a"), + tuple(field2, "f2-1"), + tuple(field3, "f3-L"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1") + ) + ), + transactionStore + ); + + EclatMapReducer.EclatResult result = runEclat(eclat, List.of(field1, field2, field3, field2a, field2b, field2c), transactionStore); + assertThat(result.getFrequentItemSets().length, equalTo(3)); + assertThat(result.getFrequentItemSets()[0].getSupport(), equalTo(1.0)); + assertThat(result.getFrequentItemSets()[1].getSupport(), equalTo(0.5)); + assertThat(result.getFrequentItemSets()[2].getSupport(), equalTo(0.25)); + assertThat(result.getProfilingInfo().get("unique_items_after_reduce"), equalTo(21L)); + assertThat(result.getProfilingInfo().get("total_transactions_after_reduce"), equalTo(12L)); + assertThat(result.getProfilingInfo().get("total_items_after_reduce"), equalTo(72L)); + assertThat(result.getProfilingInfo().get("item_sets_checked_eclat"), equalTo(63L)); + } + + public void testPruneToNextMainBranchAfterMinCountPrune() throws IOException { + Field field1 = createKeywordFieldTestInstance("keyword1", 0); + Field field2 = createKeywordFieldTestInstance("keyword2", 1); + Field field3 = createKeywordFieldTestInstance("keyword3", 2); + + // create 3 fields that "follow" field 2 + Field field2a = createKeywordFieldTestInstance("keyword2a", 3); + Field field2b = createKeywordFieldTestInstance("keyword2b", 4); + Field field2c = createKeywordFieldTestInstance("keyword2c", 5); + + // create another field to enforce min count pruning + Field field4 = createKeywordFieldTestInstance("keyword4", 6); + Field field4a = createKeywordFieldTestInstance("keyword4a", 7); + + EclatMapReducer eclat = new EclatMapReducer(getTestName(), 0.1, 2, 10, true); + + HashBasedTransactionStore transactionStore = eclat.mapInit(mockBigArrays()); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-a"), + tuple(field2, "f2-1"), + tuple(field3, "f3-A"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1"), + tuple(field4, "f4-1"), + tuple(field4a, "f4a-1") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-a"), + tuple(field2, "f2-1"), + tuple(field3, "f3-B"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1"), + tuple(field4, "f4-2"), + tuple(field4a, "f4a-2") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-a"), + tuple(field2, "f2-1"), + tuple(field3, "f3-C"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1"), + tuple(field4, "f4-3"), + tuple(field4a, "f4a-3") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-a"), + tuple(field2, "f2-1"), + tuple(field3, "f3-D"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1"), + tuple(field4, "f4-4"), + tuple(field4a, "f4a-4") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-a"), + tuple(field2, "f2-1"), + tuple(field3, "f3-E"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1"), + tuple(field4, "f4-5"), + tuple(field4a, "f4a-5") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-b"), + tuple(field2, "f2-1"), + tuple(field3, "f3-F"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1"), + tuple(field4, "f4-1"), + tuple(field4a, "f4a-1") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-b"), + tuple(field2, "f2-1"), + tuple(field3, "f3-G"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1"), + tuple(field4, "f4-1"), + tuple(field4a, "f4a-1") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-c"), + tuple(field2, "f2-1"), + tuple(field3, "f3-H"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1"), + tuple(field4, "f4-6"), + tuple(field4a, "f4a-6") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-d"), + tuple(field2, "f2-1"), + tuple(field3, "f3-I"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1"), + tuple(field4, "f4-7"), + tuple(field4a, "f4a-7") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-b"), + tuple(field2, "f2-1"), + tuple(field3, "f3-J"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1"), + tuple(field4, "f4-8"), + tuple(field4a, "f4a-8") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-f"), + tuple(field2, "f2-1"), + tuple(field3, "f3-K"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1"), + tuple(field4, "f4-3"), + tuple(field4a, "f4a-3") + ) + ), + transactionStore + ); + eclat.map( + mockOneDocument( + List.of( + tuple(field1, "f1-a"), + tuple(field2, "f2-1"), + tuple(field3, "f3-L"), + tuple(field2a, "f2a-1"), + tuple(field2b, "f2b-1"), + tuple(field2c, "f2c-1"), + tuple(field4, "f4-10"), + tuple(field4a, "f4a-10") + ) + ), + transactionStore + ); + + EclatMapReducer.EclatResult result = runEclat( + eclat, + List.of(field1, field2, field3, field2a, field2b, field2c, field4, field4a), + transactionStore + ); + + assertThat(result.getFrequentItemSets().length, equalTo(6)); + assertThat(result.getFrequentItemSets()[0].getSupport(), equalTo(1.0)); + assertThat(result.getFrequentItemSets()[1].getSupport(), equalTo(0.5)); + assertThat(result.getFrequentItemSets()[2].getSupport(), equalTo(0.25)); + assertThat(result.getProfilingInfo().get("unique_items_after_reduce"), equalTo(39L)); + assertThat(result.getProfilingInfo().get("unique_items_after_prune"), equalTo(10L)); + assertThat(result.getProfilingInfo().get("total_transactions_after_reduce"), equalTo(12L)); + assertThat(result.getProfilingInfo().get("total_items_after_reduce"), equalTo(96L)); + assertThat(result.getProfilingInfo().get("total_items_after_prune"), equalTo(96L)); + assertThat(result.getProfilingInfo().get("item_sets_checked_eclat"), equalTo(495L)); + } + + private static BigArrays mockBigArrays() { + return new MockBigArrays(new MockPageCacheRecycler(Settings.EMPTY), new NoneCircuitBreakerService()); + } + + private static Tuple> mockOneField(Field field, String... fieldValues) { + return tuple(field, Arrays.stream(fieldValues).map(v -> new BytesRef(v)).collect(Collectors.toList())); + } + + private static Stream>> mockOneDocument(List> fieldsAndValues) { + return fieldsAndValues.stream().map(fieldAndValue -> mockOneField(fieldAndValue.v1(), fieldAndValue.v2())); + } + + private static EclatMapReducer.EclatResult runEclat( + EclatMapReducer eclat, + List fields, + HashBasedTransactionStore... transactionStores + ) throws IOException { + HashBasedTransactionStore transactionStoreForReduce = eclat.reduceInit(mockBigArrays()); + + for (HashBasedTransactionStore transactionStore : transactionStores) { + ImmutableTransactionStore transactionStoreAfterFinalizing = eclat.mapFinalize(transactionStore); + List allPartitions = List.of(transactionStoreAfterFinalizing); + eclat.reduce(allPartitions.stream(), transactionStoreForReduce, doNotCancelSupplier); + } + return eclat.reduceFinalize(transactionStoreForReduce, fields, doNotCancelSupplier); + } +} diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetCollectorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetCollectorTests.java index 671f7b3a7c07..847494ecb49d 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetCollectorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetCollectorTests.java @@ -170,6 +170,20 @@ public void testLargerItemSetsPreference() { } } + public void testSuperSetAfterSubSet() { + transactionStore = new HashBasedTransactionStore(mockBigArrays()); + + try (TopItemIds topItemIds = transactionStore.getTopItemIds();) { + FrequentItemSetCollector collector = new FrequentItemSetCollector(transactionStore, topItemIds, 5, Long.MAX_VALUE); + + assertEquals(Long.MAX_VALUE, addToCollector(collector, new long[] { 1L, 2L, 3L, 4L, 6L }, 3L)); + assertEquals(Long.MAX_VALUE, addToCollector(collector, new long[] { 1L, 2L, 3L, 4L, 6L, 8L }, 3L)); + + assertEquals(1, collector.size()); + assertThat(collector.getQueue().pop().getItems(), equalTo(createItemSetBitSet(new long[] { 1L, 2L, 3L, 4L, 6L, 8L }))); + } + } + private static ItemSetBitSet createItemSetBitSet(long[] longs) { ItemSetBitSet itemsAsBitVector = new ItemSetBitSet(); for (int i = 0; i < longs.length; ++i) { diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/ItemSetBitSetTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/ItemSetBitSetTests.java index e40decb0883c..49acb01ddb2a 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/ItemSetBitSetTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/ItemSetBitSetTests.java @@ -44,7 +44,7 @@ public void testBasics() { assertTrue(set.get(200)); } - public void testIsSubSet() { + public void testIsSubSetAndSetRelation() { ItemSetBitSet set1 = new ItemSetBitSet(); set1.set(0); set1.set(3); @@ -60,24 +60,39 @@ public void testIsSubSet() { assertEquals(3, set2.cardinality()); assertTrue(set2.isSubset(set1)); + assertEquals(ItemSetBitSet.SetRelation.SUB_SET, set2.setRelation(set1)); assertFalse(set1.isSubset(set2)); + assertEquals(ItemSetBitSet.SetRelation.SUPER_SET, set1.setRelation(set2)); assertTrue(set1.isSubset(set1)); + assertEquals(ItemSetBitSet.SetRelation.EQUAL, set1.setRelation(set1)); set2.set(0); set2.set(5); assertTrue(set2.isSubset(set1)); assertTrue(set1.isSubset(set2)); + assertEquals(ItemSetBitSet.SetRelation.EQUAL, set1.setRelation(set2)); + assertEquals(ItemSetBitSet.SetRelation.EQUAL, set2.setRelation(set1)); set2.set(99); assertFalse(set2.isSubset(set1)); assertTrue(set1.isSubset(set2)); + assertEquals(ItemSetBitSet.SetRelation.SUPER_SET, set2.setRelation(set1)); + assertEquals(ItemSetBitSet.SetRelation.SUB_SET, set1.setRelation(set2)); set1.set(999); assertFalse(set1.isSubset(set2)); + assertEquals(ItemSetBitSet.SetRelation.DISJOINT_OR_INTERSECT, set2.setRelation(set1)); + assertEquals(ItemSetBitSet.SetRelation.DISJOINT_OR_INTERSECT, set1.setRelation(set2)); + set2.set(999); assertTrue(set1.isSubset(set2)); + assertEquals(ItemSetBitSet.SetRelation.SUPER_SET, set2.setRelation(set1)); + assertEquals(ItemSetBitSet.SetRelation.SUB_SET, set1.setRelation(set2)); + set2.set(2222); assertTrue(set1.isSubset(set2)); + assertEquals(ItemSetBitSet.SetRelation.SUPER_SET, set2.setRelation(set1)); + assertEquals(ItemSetBitSet.SetRelation.SUB_SET, set1.setRelation(set2)); } public void testClone() { From 4e940f275380b906c436ec6f8aaa3f733294d849 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Wed, 14 Dec 2022 12:02:32 +0000 Subject: [PATCH 261/919] Mute the continue scroll yaml test until #91637 is fixed (#92354) --- .../resources/rest-api-spec/test/upgraded_cluster/10_basic.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/10_basic.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/10_basic.yml index d4aec6ac1f0a..cab9f19363d4 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/10_basic.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/10_basic.yml @@ -1,5 +1,8 @@ --- "Continue scroll after upgrade": + - skip: + version: "all" + reason: "AwaitsFix https://github.com/elastic/elasticsearch/issues/91637" - do: get: index: scroll_index From d800d26e5d3f9535a925244c7129778e76489102 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Wed, 14 Dec 2022 14:25:38 +0100 Subject: [PATCH 262/919] Deduplicate RetentionLease.source (#92347) We only ever see the strings "ccr" or "peer recovery" here. A recent experiment with ccr and many follower clusters following one leader, revealed 100s of MB being wasted on duplicate "ccr" strings when fetching index stats -> lets fix this by simple deduplication. --- .../elasticsearch/index/seqno/RetentionLease.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/seqno/RetentionLease.java b/server/src/main/java/org/elasticsearch/index/seqno/RetentionLease.java index c2c33bafc8df..8ac992221aba 100644 --- a/server/src/main/java/org/elasticsearch/index/seqno/RetentionLease.java +++ b/server/src/main/java/org/elasticsearch/index/seqno/RetentionLease.java @@ -99,7 +99,12 @@ public RetentionLease(final String id, final long retainingSequenceNumber, final this.id = id; this.retainingSequenceNumber = retainingSequenceNumber; this.timestamp = timestamp; - this.source = source; + // deduplicate the string instances to save memory for the known possible source values + this.source = switch (source) { + case "ccr" -> "ccr"; + case ReplicationTracker.PEER_RECOVERY_RETENTION_LEASE_SOURCE -> ReplicationTracker.PEER_RECOVERY_RETENTION_LEASE_SOURCE; + default -> source; + }; } /** @@ -109,10 +114,7 @@ public RetentionLease(final String id, final long retainingSequenceNumber, final * @throws IOException if an I/O exception occurs reading from the stream */ public RetentionLease(final StreamInput in) throws IOException { - id = in.readString(); - retainingSequenceNumber = in.readZLong(); - timestamp = in.readVLong(); - source = in.readString(); + this(in.readString(), in.readZLong(), in.readVLong(), in.readString()); } /** From fbb6abd2f41f3af72599500c22f9d83e09484974 Mon Sep 17 00:00:00 2001 From: David Kyle Date: Wed, 14 Dec 2022 13:32:23 +0000 Subject: [PATCH 263/919] [ML] Increase the default timeout for start trained model deployment (#92328) A 30 second timeout is inline with the default value used in most ML APIs. --- docs/changelog/92328.yaml | 5 ++ .../start-trained-model-deployment.asciidoc | 50 +++++++++---------- .../StartTrainedModelDeploymentAction.java | 2 +- ...artTrainedModelDeploymentRequestTests.java | 2 +- 4 files changed, 32 insertions(+), 27 deletions(-) create mode 100644 docs/changelog/92328.yaml diff --git a/docs/changelog/92328.yaml b/docs/changelog/92328.yaml new file mode 100644 index 000000000000..e1cfd9ef955a --- /dev/null +++ b/docs/changelog/92328.yaml @@ -0,0 +1,5 @@ +pr: 92328 +summary: Increase the default timeout for the start trained model deployment API +area: Machine Learning +type: enhancement +issues: [] diff --git a/docs/reference/ml/trained-models/apis/start-trained-model-deployment.asciidoc b/docs/reference/ml/trained-models/apis/start-trained-model-deployment.asciidoc index beef62b88597..b3c1c2ddc488 100644 --- a/docs/reference/ml/trained-models/apis/start-trained-model-deployment.asciidoc +++ b/docs/reference/ml/trained-models/apis/start-trained-model-deployment.asciidoc @@ -28,19 +28,19 @@ in an ingest pipeline or directly in the <> API. Scaling inference performance can be achieved by setting the parameters `number_of_allocations` and `threads_per_allocation`. -Increasing `threads_per_allocation` means more threads are used when an -inference request is processed on a node. This can improve inference speed for +Increasing `threads_per_allocation` means more threads are used when an +inference request is processed on a node. This can improve inference speed for certain models. It may also result in improvement to throughput. -Increasing `number_of_allocations` means more threads are used to process -multiple inference requests in parallel resulting in throughput improvement. -Each model allocation uses a number of threads defined by +Increasing `number_of_allocations` means more threads are used to process +multiple inference requests in parallel resulting in throughput improvement. +Each model allocation uses a number of threads defined by `threads_per_allocation`. -Model allocations are distributed across {ml} nodes. All allocations assigned to -a node share the same copy of the model in memory. To avoid thread -oversubscription which is detrimental to performance, model allocations are -distributed in such a way that the total number of used threads does not surpass +Model allocations are distributed across {ml} nodes. All allocations assigned to +a node share the same copy of the model in memory. To avoid thread +oversubscription which is detrimental to performance, model allocations are +distributed in such a way that the total number of used threads does not surpass the node's allocated processors. [[start-trained-model-deployment-path-params]] @@ -55,9 +55,9 @@ include::{es-repo-dir}/ml/ml-shared.asciidoc[tag=model-id] `cache_size`:: (Optional, <>) -The inference cache size (in memory outside the JVM heap) per node for the -model. The default value is the size of the model as reported by the -`model_size_bytes` field in the <>. To disable the +The inference cache size (in memory outside the JVM heap) per node for the +model. The default value is the size of the model as reported by the +`model_size_bytes` field in the <>. To disable the cache, `0b` can be provided. `number_of_allocations`:: @@ -71,17 +71,17 @@ The priority of the deployment. The default value is `normal`. There are two priority settings: + -- -* `normal`: Use this for deployments in production. The deployment allocations +* `normal`: Use this for deployments in production. The deployment allocations are distributed so that node processors are not oversubscribed. -* `low`: Use this for testing model functionality. The intention is that these -deployments are not sent a high volume of input. The deployment is required to -have a single allocation with just one thread. Low priority deployments may be -assigned on nodes that already utilize all their processors but will be given a -lower CPU priority than normal deployments. Low priority deployments may be +* `low`: Use this for testing model functionality. The intention is that these +deployments are not sent a high volume of input. The deployment is required to +have a single allocation with just one thread. Low priority deployments may be +assigned on nodes that already utilize all their processors but will be given a +lower CPU priority than normal deployments. Low priority deployments may be unassigned in order to satisfy more allocations of normal priority deployments. -- -WARNING: Heavy usage of low priority deployments may impact performance of +WARNING: Heavy usage of low priority deployments may impact performance of normal priority deployments. `queue_capacity`:: @@ -89,20 +89,20 @@ normal priority deployments. Controls how many inference requests are allowed in the queue at a time. Every machine learning node in the cluster where the model can be allocated has a queue of this size; when the number of requests exceeds the total value, -new requests are rejected with a 429 error. Defaults to 1024. Max allowed value +new requests are rejected with a 429 error. Defaults to 1024. Max allowed value is 1000000. `threads_per_allocation`:: (Optional, integer) -Sets the number of threads used by each model allocation during inference. This -generally increases the speed per inference request. The inference process is a -compute-bound process; `threads_per_allocations` must not exceed the number of -available allocated processors per node. Defaults to 1. Must be a power of 2. +Sets the number of threads used by each model allocation during inference. This +generally increases the speed per inference request. The inference process is a +compute-bound process; `threads_per_allocations` must not exceed the number of +available allocated processors per node. Defaults to 1. Must be a power of 2. Max allowed value is 32. `timeout`:: (Optional, time) -Controls the amount of time to wait for the model to deploy. Defaults to 20 +Controls the amount of time to wait for the model to deploy. Defaults to 30 seconds. `wait_for`:: diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StartTrainedModelDeploymentAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StartTrainedModelDeploymentAction.java index ddb01225e666..8a6ccba675a5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StartTrainedModelDeploymentAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StartTrainedModelDeploymentAction.java @@ -47,7 +47,7 @@ public class StartTrainedModelDeploymentAction extends ActionType Date: Wed, 14 Dec 2022 16:08:12 +0200 Subject: [PATCH 264/919] [ML] Relax stratified splitter unfiformity test error bound (#92362) Relaxes slightly the error bound on the uniformity test from 0.13 to 0.14 to avoid sporadic test failures. Closes #92361 --- .../traintestsplit/StratifiedTrainTestSplitterTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/traintestsplit/StratifiedTrainTestSplitterTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/traintestsplit/StratifiedTrainTestSplitterTests.java index 3309e444f178..581270538ce8 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/traintestsplit/StratifiedTrainTestSplitterTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/dataframe/traintestsplit/StratifiedTrainTestSplitterTests.java @@ -214,7 +214,7 @@ public void testIsTraining_SelectsTrainingRowsUniformly() { // should be close to the training percent, which is set to 0.5 for (int rowTrainingCount : trainingCountPerRow) { double meanCount = rowTrainingCount / (double) runCount; - assertThat(meanCount, is(closeTo(0.5, 0.13))); + assertThat(meanCount, is(closeTo(0.5, 0.14))); } } From 92af5fefc645d6f8bc2c11eb7fe0880c8805f72f Mon Sep 17 00:00:00 2001 From: Hendrik Muhs Date: Wed, 14 Dec 2022 15:19:51 +0100 Subject: [PATCH 265/919] fix missing override for matches in ProfileWeight (#92360) profiling wraps a weight object in order to mix-in timings. This PR fixes a missing override of matches. In addition I added test coverage for other overrides. --- docs/changelog/92360.yaml | 5 + .../search/profile/query/ProfileWeight.java | 4 + .../profile/query/ProfileScorerTests.java | 97 +++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 docs/changelog/92360.yaml diff --git a/docs/changelog/92360.yaml b/docs/changelog/92360.yaml new file mode 100644 index 000000000000..7f389646f7e7 --- /dev/null +++ b/docs/changelog/92360.yaml @@ -0,0 +1,5 @@ +pr: 92360 +summary: Fix missing override for matches in `ProfileWeight` +area: Search +type: bug +issues: [] diff --git a/server/src/main/java/org/elasticsearch/search/profile/query/ProfileWeight.java b/server/src/main/java/org/elasticsearch/search/profile/query/ProfileWeight.java index 21f983e0c756..877854f5cff9 100644 --- a/server/src/main/java/org/elasticsearch/search/profile/query/ProfileWeight.java +++ b/server/src/main/java/org/elasticsearch/search/profile/query/ProfileWeight.java @@ -11,6 +11,7 @@ import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.search.BulkScorer; import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.Matches; import org.apache.lucene.search.Query; import org.apache.lucene.search.Scorer; import org.apache.lucene.search.ScorerSupplier; @@ -116,4 +117,7 @@ public boolean isCacheable(LeafReaderContext ctx) { return false; } + public Matches matches(LeafReaderContext context, int doc) throws IOException { + return subQueryWeight.matches(context, doc); + } } diff --git a/server/src/test/java/org/elasticsearch/search/profile/query/ProfileScorerTests.java b/server/src/test/java/org/elasticsearch/search/profile/query/ProfileScorerTests.java index 9e0d4bdeae25..a478e80ca1b4 100644 --- a/server/src/test/java/org/elasticsearch/search/profile/query/ProfileScorerTests.java +++ b/server/src/test/java/org/elasticsearch/search/profile/query/ProfileScorerTests.java @@ -8,10 +8,14 @@ package org.elasticsearch.search.profile.query; +import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.MultiReader; import org.apache.lucene.search.DocIdSetIterator; +import org.apache.lucene.search.Explanation; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.Matches; +import org.apache.lucene.search.MatchesIterator; import org.apache.lucene.search.Query; import org.apache.lucene.search.ScoreMode; import org.apache.lucene.search.Scorer; @@ -19,6 +23,9 @@ import org.elasticsearch.test.ESTestCase; import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; public class ProfileScorerTests extends ESTestCase { @@ -56,6 +63,85 @@ public void setMinCompetitiveScore(float minScore) { } } + private static class FakeWeight extends Weight { + + protected FakeWeight(Query query) { + super(query); + } + + @Override + public Explanation explain(LeafReaderContext context, int doc) throws IOException { + return Explanation.match(1, "fake_description"); + } + + @Override + public Scorer scorer(LeafReaderContext context) throws IOException { + FakeScorer fakeScorer = new FakeScorer(this); + fakeScorer.maxScore = 42f; + return fakeScorer; + } + + @Override + public boolean isCacheable(LeafReaderContext ctx) { + return false; + } + + @Override + public Matches matches(LeafReaderContext context, int doc) throws IOException { + return new Matches() { + @Override + public MatchesIterator getMatches(String field) throws IOException { + return new MatchesIterator() { + @Override + public boolean next() throws IOException { + return false; + } + + @Override + public int startPosition() { + return 42; + } + + @Override + public int endPosition() { + return 43; + } + + @Override + public int startOffset() throws IOException { + return 44; + } + + @Override + public int endOffset() throws IOException { + return 45; + } + + @Override + public MatchesIterator getSubMatches() throws IOException { + return null; + } + + @Override + public Query getQuery() { + return parentQuery; + } + }; + } + + @Override + public Collection getSubMatches() { + return Collections.emptyList(); + } + + @Override + public Iterator iterator() { + return null; + } + }; + } + } + public void testPropagateMinCompetitiveScore() throws IOException { Query query = new MatchAllDocsQuery(); Weight weight = query.createWeight(new IndexSearcher(new MultiReader()), ScoreMode.TOP_SCORES, 1f); @@ -78,4 +164,15 @@ public void testPropagateMaxScore() throws IOException { fakeScorer.maxScore = 42f; assertEquals(42f, profileScorer.getMaxScore(DocIdSetIterator.NO_MORE_DOCS), 0f); } + + // tests that ProfileWeight correctly propagates the wrapped inner weight + public void testPropagateSubWeight() throws IOException { + Query query = new MatchAllDocsQuery(); + Weight fakeWeight = new FakeWeight(query); + QueryProfileBreakdown profile = new QueryProfileBreakdown(); + ProfileWeight profileWeight = new ProfileWeight(query, fakeWeight, profile); + assertEquals(42f, profileWeight.scorer(null).getMaxScore(DocIdSetIterator.NO_MORE_DOCS), 0f); + assertEquals(42, profileWeight.matches(null, 1).getMatches("some_field").startPosition()); + assertEquals("fake_description", profileWeight.explain(null, 1).getDescription()); + } } From 7341cfbe8252e9608a2f9c1c3f0f8f2083d41878 Mon Sep 17 00:00:00 2001 From: Hendrik Muhs Date: Wed, 14 Dec 2022 15:51:09 +0100 Subject: [PATCH 266/919] [ML] Frequent items extension pruning (#92322) Especially machine-generated data contains data that always appears together, e.g. a URI and a path which is the same URI but without the protocol. Such a case is called an "extension". This PR implements so called extension pruning, it avoids iterating over those extensions to build all permutations of subsets from a superset. All with the same item counts. This fixes problems with datasets based on APM and logging. For those a benchmark of the query execution time reduces from 19434198 ms to 103321 ms (factor 188). For more "natural" data sets like the ones used for rally-tracks this PR has no effect as pruning doesn't kick in here. --- docs/changelog/92322.yaml | 5 ++++ .../CountingItemSetTraverser.java | 8 ++++++ .../frequentitemsets/EclatMapReducer.java | 26 +++++++++++++++++++ .../EclatMapReducerTests.java | 10 +++++-- 4 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 docs/changelog/92322.yaml diff --git a/docs/changelog/92322.yaml b/docs/changelog/92322.yaml new file mode 100644 index 000000000000..3b7d93fc6a4e --- /dev/null +++ b/docs/changelog/92322.yaml @@ -0,0 +1,5 @@ +pr: 92322 +summary: implement extension pruning in frequent items to improve runtime +area: Machine Learning +type: enhancement +issues: [] diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/CountingItemSetTraverser.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/CountingItemSetTraverser.java index 3dd56d624645..35a0bd9e4c43 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/CountingItemSetTraverser.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/CountingItemSetTraverser.java @@ -261,6 +261,14 @@ public void prune() { topItemSetTraverser.prune(); } + public void pruneToNextMainBranch() { + long thisCount = getCount(); + + while (getNumberOfItems() > 1 && getCount() == thisCount) { + topItemSetTraverser.prune(); + } + } + /** * Return true if the item set tree is on a leaf, which mean no further items can be added to the candidate set. */ diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/EclatMapReducer.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/EclatMapReducer.java index 78fe1caf7f82..f2fb4fb161f2 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/EclatMapReducer.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/EclatMapReducer.java @@ -424,6 +424,22 @@ private static EclatResult eclat( // no need to set visited, as we are on a leaf } + /** + * Optimization + * + * a - b - c - d + * | | \- h + * | |\- e - f + * | \- h - j + * \- x - y + * + * assume we pruned at d above, if c has the same count as d, we don't need the sub-branches, but go up + * until we find a branch that has a higher count than the pruned one. This allows us to skip over subtrees. + */ + if (setTraverser.atLeaf()) { + setTraverser.pruneToNextMainBranch(); + } + continue; } @@ -473,6 +489,16 @@ private static EclatResult eclat( previousMinCount = minCount; logger.debug("adjusting min count to {}", minCount); } + + /** + * Optimization: + * + * If we reached a leaf, go up the branch until the new branch has a higher count than our current leaf. + */ + if (setTraverser.atLeaf()) { + setTraverser.prune(); + setTraverser.pruneToNextMainBranch(); + } } FrequentItemSet[] topFrequentItems = collector.finalizeAndGetResults(fields); final long eclatRuntimeNanos = System.nanoTime() - relativeStartNanos; diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/EclatMapReducerTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/EclatMapReducerTests.java index 8280f431423d..436bf74d4f28 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/EclatMapReducerTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/EclatMapReducerTests.java @@ -27,6 +27,8 @@ import static org.elasticsearch.core.Tuple.tuple; import static org.elasticsearch.xpack.ml.aggs.frequentitemsets.mr.ItemSetMapReduceValueSourceTests.createKeywordFieldTestInstance; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.lessThan; public class EclatMapReducerTests extends ESTestCase { @@ -240,7 +242,7 @@ public void testPruneToNextMainBranch() throws IOException { assertThat(result.getProfilingInfo().get("unique_items_after_reduce"), equalTo(21L)); assertThat(result.getProfilingInfo().get("total_transactions_after_reduce"), equalTo(12L)); assertThat(result.getProfilingInfo().get("total_items_after_reduce"), equalTo(72L)); - assertThat(result.getProfilingInfo().get("item_sets_checked_eclat"), equalTo(63L)); + assertThat(result.getProfilingInfo().get("item_sets_checked_eclat"), equalTo(47L)); } public void testPruneToNextMainBranchAfterMinCountPrune() throws IOException { @@ -456,7 +458,11 @@ public void testPruneToNextMainBranchAfterMinCountPrune() throws IOException { assertThat(result.getProfilingInfo().get("total_transactions_after_reduce"), equalTo(12L)); assertThat(result.getProfilingInfo().get("total_items_after_reduce"), equalTo(96L)); assertThat(result.getProfilingInfo().get("total_items_after_prune"), equalTo(96L)); - assertThat(result.getProfilingInfo().get("item_sets_checked_eclat"), equalTo(495L)); + + // the number can vary depending on order, so we can only check a range, which is still much lower than without + // that optimization + assertThat((Long) result.getProfilingInfo().get("item_sets_checked_eclat"), greaterThanOrEqualTo(294L)); + assertThat((Long) result.getProfilingInfo().get("item_sets_checked_eclat"), lessThan(310L)); } private static BigArrays mockBigArrays() { From 625879c9a282f24345c9fc3f875dc35c53c96860 Mon Sep 17 00:00:00 2001 From: Joe Gallo Date: Wed, 14 Dec 2022 10:04:45 -0500 Subject: [PATCH 267/919] Upgrade geoip2 dependency (#92364) --- gradle/verification-metadata.xml | 12 ++++++------ modules/ingest-geoip/build.gradle | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index d897088a9c4b..2d9d18c07d4d 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -799,14 +799,14 @@ - - - + + + - - - + + + diff --git a/modules/ingest-geoip/build.gradle b/modules/ingest-geoip/build.gradle index 6163ae0f7776..3211d88444cc 100644 --- a/modules/ingest-geoip/build.gradle +++ b/modules/ingest-geoip/build.gradle @@ -27,12 +27,12 @@ tasks.named('internalClusterTestTestingConventions').configure { } dependencies { - implementation('com.maxmind.geoip2:geoip2:3.0.0') + implementation('com.maxmind.geoip2:geoip2:3.0.2') // geoip2 dependencies: - runtimeOnly("com.fasterxml.jackson.core:jackson-annotations:2.13.1") - runtimeOnly("com.fasterxml.jackson.core:jackson-databind:2.13.1") - runtimeOnly("com.fasterxml.jackson.core:jackson-core:2.13.1") - implementation('com.maxmind.db:maxmind-db:2.0.0') + runtimeOnly("com.fasterxml.jackson.core:jackson-annotations:2.13.4") + runtimeOnly("com.fasterxml.jackson.core:jackson-databind:2.13.4.2") + runtimeOnly("com.fasterxml.jackson.core:jackson-core:2.13.4") + implementation('com.maxmind.db:maxmind-db:2.1.0') testImplementation 'org.elasticsearch:geolite2-databases:20191119' internalClusterTestImplementation project(path: ":modules:reindex") From 9153bd5bbf2111cdbc2aaec0cad651e72822d3e7 Mon Sep 17 00:00:00 2001 From: Artem Prigoda Date: Wed, 14 Dec 2022 16:06:58 +0100 Subject: [PATCH 268/919] Make sure EngineConfig can access whether shard is primary (#92326) Make sure engine factories can access the `RecoveryState#primary` flag, so plugins can create different engines for primary and replica indices. --- .../indices/IndexingMemoryControllerIT.java | 3 ++- .../elasticsearch/index/engine/EngineConfig.java | 14 +++++++++++++- .../org/elasticsearch/index/shard/IndexShard.java | 3 ++- .../index/engine/InternalEngineTests.java | 6 ++++-- .../index/shard/IndexShardTests.java | 13 ++++++++++--- .../index/shard/RefreshListenersTests.java | 3 ++- .../indices/IndexingMemoryControllerTests.java | 3 ++- .../index/engine/EngineTestCase.java | 15 ++++++++++----- .../ccr/index/engine/FollowingEngineTests.java | 3 ++- 9 files changed, 47 insertions(+), 16 deletions(-) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/indices/IndexingMemoryControllerIT.java b/server/src/internalClusterTest/java/org/elasticsearch/indices/IndexingMemoryControllerIT.java index 623cdd6c1070..83078a0d3045 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/indices/IndexingMemoryControllerIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/indices/IndexingMemoryControllerIT.java @@ -79,7 +79,8 @@ EngineConfig engineConfigWithLargerIndexingMemory(EngineConfig config) { config.getSnapshotCommitSupplier(), config.getLeafSorter(), config.getRelativeTimeInNanosSupplier(), - config.getIndexCommitListener() + config.getIndexCommitListener(), + config.isRecoveringAsPrimary() ); } diff --git a/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java b/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java index 139446164a78..b7110a64fa99 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java +++ b/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java @@ -128,6 +128,8 @@ public Supplier retentionLeasesSupplier() { @Nullable private final Engine.IndexCommitListener indexCommitListener; + private final boolean recoveringAsPrimary; + /** * Creates a new {@link org.elasticsearch.index.engine.EngineConfig} */ @@ -156,7 +158,8 @@ public EngineConfig( IndexStorePlugin.SnapshotCommitSupplier snapshotCommitSupplier, Comparator leafSorter, LongSupplier relativeTimeInNanosSupplier, - Engine.IndexCommitListener indexCommitListener + Engine.IndexCommitListener indexCommitListener, + boolean recoveringAsPrimary ) { this.shardId = shardId; this.indexSettings = indexSettings; @@ -198,6 +201,7 @@ public EngineConfig( this.leafSorter = leafSorter; this.relativeTimeInNanosSupplier = relativeTimeInNanosSupplier; this.indexCommitListener = indexCommitListener; + this.recoveringAsPrimary = recoveringAsPrimary; } /** @@ -405,4 +409,12 @@ public LongSupplier getRelativeTimeInNanosSupplier() { public Engine.IndexCommitListener getIndexCommitListener() { return indexCommitListener; } + + /** + * Represents the primary state only in case when a recovery starts. + * IMPORTANT: The flag is a temporary solution and should NOT be used outside of Stateless. + */ + public boolean isRecoveringAsPrimary() { + return recoveringAsPrimary; + } } diff --git a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java index 336aa89c3eea..4d40fa912ba1 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java +++ b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java @@ -3274,7 +3274,8 @@ private EngineConfig newEngineConfig(LongSupplier globalCheckpointSupplier) { snapshotCommitSupplier, isTimeseriesIndex ? TIMESERIES_LEAF_READERS_SORTER : null, relativeTimeInNanosSupplier, - indexCommitListener + indexCommitListener, + recoveryState != null && recoveryState.getPrimary() ); } diff --git a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java index 4f0441ef4f5f..08e945778407 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -3590,7 +3590,8 @@ public void testRecoverFromForeignTranslog() throws IOException { IndexModule.DEFAULT_SNAPSHOT_COMMIT_SUPPLIER, null, config.getRelativeTimeInNanosSupplier(), - null + null, + false ); expectThrows(EngineCreationFailureException.class, () -> new InternalEngine(brokenConfig)); @@ -7260,7 +7261,8 @@ public void testNotWarmUpSearcherInEngineCtor() throws Exception { config.getSnapshotCommitSupplier(), config.getLeafSorter(), config.getRelativeTimeInNanosSupplier(), - config.getIndexCommitListener() + config.getIndexCommitListener(), + config.isRecoveringAsPrimary() ); try (InternalEngine engine = createEngine(configWithWarmer)) { assertThat(warmedUpReaders, empty()); diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java index 9dda27b4521b..6edbf38ae368 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -2900,11 +2900,15 @@ public void testRecoverFromTranslog() throws IOException { } public void testShardActiveDuringInternalRecovery() throws IOException { - IndexShard shard = newStartedShard(true); + boolean isPrimary = randomBoolean(); + IndexShard shard = newStartedShard(isPrimary); indexDoc(shard, "_doc", "0"); shard = reinitShard(shard); DiscoveryNode localNode = new DiscoveryNode("foo", buildNewFakeTransportAddress(), emptyMap(), emptySet(), Version.CURRENT); - shard.markAsRecovering("for testing", new RecoveryState(shard.routingEntry(), localNode, null)); + DiscoveryNode sourceNode = isPrimary + ? null + : new DiscoveryNode("bar", buildNewFakeTransportAddress(), emptyMap(), emptySet(), Version.CURRENT); + shard.markAsRecovering("for testing", new RecoveryState(shard.routingEntry(), localNode, sourceNode)); // Shard is still inactive since we haven't started recovering yet assertFalse(shard.isActive()); shard.prepareForIndexRecovery(); @@ -2914,6 +2918,8 @@ public void testShardActiveDuringInternalRecovery() throws IOException { shard.openEngineAndRecoverFromTranslog(); // Shard should now be active since we did recover: assertTrue(shard.isActive()); + // Recovery state should be propagated to the engine + assertEquals(isPrimary, shard.getEngine().config().isRecoveringAsPrimary()); closeShards(shard); } @@ -4520,7 +4526,8 @@ public void testCloseShardWhileEngineIsWarming() throws Exception { IndexModule.DEFAULT_SNAPSHOT_COMMIT_SUPPLIER, config.getLeafSorter(), config.getRelativeTimeInNanosSupplier(), - config.getIndexCommitListener() + config.getIndexCommitListener(), + config.isRecoveringAsPrimary() ); return new InternalEngine(configWithWarmer); }); diff --git a/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java b/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java index 59e2fe1baa19..d16bc6fc03c5 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java @@ -153,7 +153,8 @@ public void onFailedEngine(String reason, @Nullable Exception e) { IndexModule.DEFAULT_SNAPSHOT_COMMIT_SUPPLIER, null, System::nanoTime, - null + null, + false ); engine = new InternalEngine(config); engine.recoverFromTranslog((e, s) -> 0, Long.MAX_VALUE); diff --git a/server/src/test/java/org/elasticsearch/indices/IndexingMemoryControllerTests.java b/server/src/test/java/org/elasticsearch/indices/IndexingMemoryControllerTests.java index 78606856bf15..f0ae41d0e228 100644 --- a/server/src/test/java/org/elasticsearch/indices/IndexingMemoryControllerTests.java +++ b/server/src/test/java/org/elasticsearch/indices/IndexingMemoryControllerTests.java @@ -402,7 +402,8 @@ EngineConfig configWithRefreshListener(EngineConfig config, ReferenceManager.Ref config.getSnapshotCommitSupplier(), config.getLeafSorter(), config.getRelativeTimeInNanosSupplier(), - config.getIndexCommitListener() + config.getIndexCommitListener(), + config.isRecoveringAsPrimary() ); } diff --git a/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java index af469d232eca..353b8009152f 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java @@ -275,7 +275,8 @@ public EngineConfig copy(EngineConfig config, LongSupplier globalCheckpointSuppl config.getSnapshotCommitSupplier(), config.getLeafSorter(), config.getRelativeTimeInNanosSupplier(), - config.getIndexCommitListener() + config.getIndexCommitListener(), + config.isRecoveringAsPrimary() ); } @@ -305,7 +306,8 @@ public EngineConfig copy(EngineConfig config, Analyzer analyzer) { config.getSnapshotCommitSupplier(), config.getLeafSorter(), config.getRelativeTimeInNanosSupplier(), - config.getIndexCommitListener() + config.getIndexCommitListener(), + config.isRecoveringAsPrimary() ); } @@ -335,7 +337,8 @@ public EngineConfig copy(EngineConfig config, MergePolicy mergePolicy) { config.getSnapshotCommitSupplier(), config.getLeafSorter(), config.getRelativeTimeInNanosSupplier(), - config.getIndexCommitListener() + config.getIndexCommitListener(), + config.isRecoveringAsPrimary() ); } @@ -858,7 +861,8 @@ public EngineConfig config( IndexModule.DEFAULT_SNAPSHOT_COMMIT_SUPPLIER, null, System::nanoTime, - indexCommitListener + indexCommitListener, + false ); } @@ -896,7 +900,8 @@ protected EngineConfig config(EngineConfig config, Store store, Path translogPat config.getSnapshotCommitSupplier(), config.getLeafSorter(), config.getRelativeTimeInNanosSupplier(), - config.getIndexCommitListener() + config.getIndexCommitListener(), + config.isRecoveringAsPrimary() ); } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/index/engine/FollowingEngineTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/index/engine/FollowingEngineTests.java index 9a10269df0ea..c307e1b9cc29 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/index/engine/FollowingEngineTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/index/engine/FollowingEngineTests.java @@ -288,7 +288,8 @@ public void onFailedEngine(String reason, Exception e) { IndexModule.DEFAULT_SNAPSHOT_COMMIT_SUPPLIER, null, System::nanoTime, - null + null, + false ); } From dc7a8e699273f875cda1e68e47d640be6250bf7d Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Wed, 14 Dec 2022 16:08:01 +0100 Subject: [PATCH 269/919] Enable a custom ReplicationTracker override per index (#92353) Necessary to disable replication via a plugin for stateless. --- .../index/shard/IndexShardIT.java | 4 +- .../org/elasticsearch/index/IndexModule.java | 11 ++- .../org/elasticsearch/index/IndexService.java | 10 ++- .../index/seqno/ReplicationTracker.java | 42 +++++++++ .../elasticsearch/index/shard/IndexShard.java | 8 +- .../elasticsearch/index/IndexModuleTests.java | 86 +++++++++++++++++++ .../index/shard/IndexShardTestCase.java | 3 +- 7 files changed, 155 insertions(+), 9 deletions(-) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/index/shard/IndexShardIT.java b/server/src/internalClusterTest/java/org/elasticsearch/index/shard/IndexShardIT.java index cf18f1c3eaa1..2ec1fff5a420 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/index/shard/IndexShardIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/index/shard/IndexShardIT.java @@ -51,6 +51,7 @@ import org.elasticsearch.index.engine.NoOpEngine; import org.elasticsearch.index.flush.FlushStats; import org.elasticsearch.index.mapper.SourceToParse; +import org.elasticsearch.index.seqno.ReplicationTracker; import org.elasticsearch.index.seqno.RetentionLeaseSyncer; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.translog.TestTranslog; @@ -661,7 +662,8 @@ public static final IndexShard newIndexShard( cbs, IndexModule.DEFAULT_SNAPSHOT_COMMIT_SUPPLIER, System::nanoTime, - null + null, + ReplicationTracker.DEFAULT_FACTORY ); } diff --git a/server/src/main/java/org/elasticsearch/index/IndexModule.java b/server/src/main/java/org/elasticsearch/index/IndexModule.java index 3610bb33a098..0ad017511137 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexModule.java +++ b/server/src/main/java/org/elasticsearch/index/IndexModule.java @@ -44,6 +44,7 @@ import org.elasticsearch.index.mapper.IdFieldMapper; import org.elasticsearch.index.mapper.MapperRegistry; import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.seqno.ReplicationTracker; import org.elasticsearch.index.shard.IndexEventListener; import org.elasticsearch.index.shard.IndexingOperationListener; import org.elasticsearch.index.shard.SearchOperationListener; @@ -167,6 +168,8 @@ public interface DirectoryWrapper extends CheckedFunction recoveryStateFactories; private final SetOnce indexCommitListener = new SetOnce<>(); + private final SetOnce replicationTrackerFactory = new SetOnce<>(); + /** * Construct the index module for the index with the specified index settings. The index module contains extension points for plugins * via {@link org.elasticsearch.plugins.Plugin#onIndexModule(IndexModule)}. @@ -376,6 +379,11 @@ public void setIndexCommitListener(Engine.IndexCommitListener listener) { this.indexCommitListener.set(Objects.requireNonNull(listener)); } + public void setReplicationTrackerFactory(ReplicationTracker.Factory factory) { + ensureNotFrozen(); + this.replicationTrackerFactory.set(factory); + } + IndexEventListener freeze() { // pkg private for testing if (this.frozen.compareAndSet(false, true)) { return new CompositeIndexEventListener(indexSettings, indexEventListeners); @@ -524,7 +532,8 @@ public IndexService newIndexService( recoveryStateFactory, indexFoldersDeletionListener, snapshotCommitSupplier, - indexCommitListener.get() + indexCommitListener.get(), + Objects.requireNonNullElse(replicationTrackerFactory.get(), ReplicationTracker.DEFAULT_FACTORY) ); success = true; return indexService; diff --git a/server/src/main/java/org/elasticsearch/index/IndexService.java b/server/src/main/java/org/elasticsearch/index/IndexService.java index ea5ae3a3c27b..0b64a17e75fd 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexService.java +++ b/server/src/main/java/org/elasticsearch/index/IndexService.java @@ -54,6 +54,7 @@ import org.elasticsearch.index.mapper.NodeMappingStats; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.query.SearchIndexNameMatcher; +import org.elasticsearch.index.seqno.ReplicationTracker; import org.elasticsearch.index.seqno.RetentionLeaseSyncer; import org.elasticsearch.index.shard.IndexEventListener; import org.elasticsearch.index.shard.IndexShard; @@ -146,6 +147,8 @@ public class IndexService extends AbstractIndexComponent implements IndicesClust private final Supplier indexSortSupplier; private final ValuesSourceRegistry valuesSourceRegistry; + private final ReplicationTracker.Factory replicationTrackerFactory; + public IndexService( IndexSettings indexSettings, IndexCreationContext indexCreationContext, @@ -177,7 +180,8 @@ public IndexService( IndexStorePlugin.RecoveryStateFactory recoveryStateFactory, IndexStorePlugin.IndexFoldersDeletionListener indexFoldersDeletionListener, IndexStorePlugin.SnapshotCommitSupplier snapshotCommitSupplier, - Engine.IndexCommitListener indexCommitListener + Engine.IndexCommitListener indexCommitListener, + ReplicationTracker.Factory replicationTrackerFactory ) { super(indexSettings); this.allowExpensiveQueries = allowExpensiveQueries; @@ -252,6 +256,7 @@ public IndexService( this.globalCheckpointTask = new AsyncGlobalCheckpointTask(this); this.retentionLeaseSyncTask = new AsyncRetentionLeaseSyncTask(this); } + this.replicationTrackerFactory = replicationTrackerFactory; updateFsyncTaskIfNecessary(); } @@ -520,7 +525,8 @@ public synchronized IndexShard createShard( circuitBreakerService, snapshotCommitSupplier, System::nanoTime, - indexCommitListener + indexCommitListener, + replicationTrackerFactory ); eventListener.indexShardStateChanged(indexShard, null, indexShard.state(), "shard created"); eventListener.afterIndexShardCreated(indexShard); diff --git a/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java b/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java index 5e1f9c4fc604..86290ca79a65 100644 --- a/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java +++ b/server/src/main/java/org/elasticsearch/index/seqno/ReplicationTracker.java @@ -52,6 +52,8 @@ import java.util.stream.LongStream; import java.util.stream.Stream; +import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_SEQ_NO; + /** * This class is responsible for tracking the replication group with its progress and safety markers (local and global checkpoints). * @@ -65,6 +67,28 @@ */ public class ReplicationTracker extends AbstractIndexShardComponent implements LongSupplier { + public static final ReplicationTracker.Factory DEFAULT_FACTORY = ( + shardId, + allocationId, + indexSettings, + operationPrimaryTerm, + onGlobalCheckpointUpdated, + currentTimeMillisSupplier, + onSyncRetentionLeases, + safeCommitInfoSupplier, + onReplicationGroupUpdated) -> new ReplicationTracker( + shardId, + allocationId, + indexSettings, + operationPrimaryTerm, + UNASSIGNED_SEQ_NO, + onGlobalCheckpointUpdated, + currentTimeMillisSupplier, + onSyncRetentionLeases, + safeCommitInfoSupplier, + onReplicationGroupUpdated + ); + /** * The allocation ID for the shard to which this tracker is a component of. */ @@ -1627,4 +1651,22 @@ public int hashCode() { return result; } } + + /** + * Factory interface used by {@link org.elasticsearch.index.IndexModule#setReplicationTrackerFactory(Factory)} to enable custom + * overrides of this class. + */ + public interface Factory { + ReplicationTracker create( + ShardId shardId, + String allocationId, + IndexSettings indexSettings, + long operationPrimaryTerm, + LongConsumer onGlobalCheckpointUpdated, + LongSupplier currentTimeMillisSupplier, + BiConsumer> onSyncRetentionLeases, + Supplier safeCommitInfoSupplier, + Consumer onReplicationGroupUpdated + ); + } } diff --git a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java index 4d40fa912ba1..90c7aa17f156 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java +++ b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java @@ -313,7 +313,8 @@ public IndexShard( final CircuitBreakerService circuitBreakerService, final IndexStorePlugin.SnapshotCommitSupplier snapshotCommitSupplier, final LongSupplier relativeTimeInNanosSupplier, - final Engine.IndexCommitListener indexCommitListener + final Engine.IndexCommitListener indexCommitListener, + final ReplicationTracker.Factory replicationTrackerFactory ) throws IOException { super(shardRouting.shardId(), indexSettings); assert shardRouting.initializing(); @@ -361,12 +362,11 @@ public IndexShard( this.pendingPrimaryTerm = primaryTerm; this.globalCheckpointListeners = new GlobalCheckpointListeners(shardId, threadPool.scheduler(), logger); this.pendingReplicationActions = new PendingReplicationActions(shardId, threadPool); - this.replicationTracker = new ReplicationTracker( + this.replicationTracker = replicationTrackerFactory.create( shardId, aId, indexSettings, primaryTerm, - UNASSIGNED_SEQ_NO, globalCheckpointListeners::globalCheckpointUpdated, threadPool::absoluteTimeInMillis, (retentionLeases, listener) -> retentionLeaseSyncer.sync(shardId, aId, getPendingPrimaryTerm(), retentionLeases, listener), @@ -3774,7 +3774,7 @@ EngineFactory getEngineFactory() { } // for tests - ReplicationTracker getReplicationTracker() { + public ReplicationTracker getReplicationTracker() { return replicationTracker; } diff --git a/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java b/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java index fc644953d1f3..0420d84d0a6f 100644 --- a/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java +++ b/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java @@ -24,8 +24,10 @@ import org.apache.lucene.tests.index.AssertingDirectoryReader; import org.apache.lucene.util.SetOnce.AlreadySetException; import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.indices.flush.FlushRequest; import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.action.support.replication.ReplicationResponse; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.node.DiscoveryNode; @@ -58,14 +60,18 @@ import org.elasticsearch.index.engine.EngineTestCase; import org.elasticsearch.index.engine.InternalEngine; import org.elasticsearch.index.engine.InternalEngineFactory; +import org.elasticsearch.index.engine.SafeCommitInfo; import org.elasticsearch.index.fielddata.IndexFieldDataCache; import org.elasticsearch.index.mapper.MapperRegistry; import org.elasticsearch.index.mapper.ParsedDocument; import org.elasticsearch.index.mapper.Uid; +import org.elasticsearch.index.seqno.ReplicationTracker; import org.elasticsearch.index.seqno.RetentionLeaseSyncer; +import org.elasticsearch.index.seqno.RetentionLeases; import org.elasticsearch.index.shard.IndexEventListener; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.IndexingOperationListener; +import org.elasticsearch.index.shard.ReplicationGroup; import org.elasticsearch.index.shard.SearchOperationListener; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.shard.ShardPath; @@ -106,10 +112,16 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.LongConsumer; +import java.util.function.LongSupplier; +import java.util.function.Supplier; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonMap; import static org.elasticsearch.index.IndexService.IndexCreationContext.CREATE_INDEX; +import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_SEQ_NO; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; @@ -432,6 +444,7 @@ public void testFrozen() { assertEquals(msg, expectThrows(IllegalStateException.class, () -> module.forceQueryCacheProvider(null)).getMessage()); assertEquals(msg, expectThrows(IllegalStateException.class, () -> module.setDirectoryWrapper(null)).getMessage()); assertEquals(msg, expectThrows(IllegalStateException.class, () -> module.setIndexCommitListener(null)).getMessage()); + assertEquals(msg, expectThrows(IllegalStateException.class, () -> module.setReplicationTrackerFactory(null)).getMessage()); } public void testSetupUnknownSimilarity() { @@ -718,6 +731,79 @@ public void onIndexCommitDelete(ShardId shardId, IndexCommit deletedCommit) { } } + public void testCustomReplicationTrackerFactory() throws IOException { + IndexModule module = new IndexModule( + indexSettings, + emptyAnalysisRegistry, + InternalEngine::new, + Collections.emptyMap(), + () -> true, + indexNameExpressionResolver, + Collections.emptyMap() + ); + + class CustomReplicationTracker extends ReplicationTracker { + CustomReplicationTracker( + ShardId shardId, + String allocationId, + IndexSettings indexSettings, + long operationPrimaryTerm, + long globalCheckpoint, + LongConsumer onGlobalCheckpointUpdated, + LongSupplier currentTimeMillisSupplier, + BiConsumer> onSyncRetentionLeases, + Supplier safeCommitInfoSupplier, + Consumer onReplicationGroupUpdated + ) { + super( + shardId, + allocationId, + indexSettings, + operationPrimaryTerm, + globalCheckpoint, + onGlobalCheckpointUpdated, + currentTimeMillisSupplier, + onSyncRetentionLeases, + safeCommitInfoSupplier, + onReplicationGroupUpdated + ); + } + } + module.setReplicationTrackerFactory( + ( + shardId, + allocationId, + indexSettings, + operationPrimaryTerm, + onGlobalCheckpointUpdated, + currentTimeMillisSupplier, + onSyncRetentionLeases, + safeCommitInfoSupplier, + onReplicationGroupUpdated) -> new CustomReplicationTracker( + shardId, + allocationId, + indexSettings, + operationPrimaryTerm, + UNASSIGNED_SEQ_NO, + onGlobalCheckpointUpdated, + currentTimeMillisSupplier, + onSyncRetentionLeases, + safeCommitInfoSupplier, + onReplicationGroupUpdated + ) + ); + final IndexService indexService = newIndexService(module); + ShardId shardId = new ShardId("index", UUIDs.randomBase64UUID(random()), 0); + ShardRouting shardRouting = ShardRouting.newUnassigned( + shardId, + true, + RecoverySource.EmptyStoreRecoverySource.INSTANCE, + new UnassignedInfo(UnassignedInfo.Reason.INDEX_CREATED, null) + ).initialize("_node_id", null, -1); + IndexShard indexShard = indexService.createShard(shardRouting, s -> {}, RetentionLeaseSyncer.EMPTY); + assertThat(indexShard.getReplicationTracker(), instanceOf(CustomReplicationTracker.class)); + } + private ShardRouting createInitializedShardRouting() { ShardRouting shard = ShardRouting.newUnassigned( new ShardId("test", "_na_", 0), diff --git a/test/framework/src/main/java/org/elasticsearch/index/shard/IndexShardTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/shard/IndexShardTestCase.java index 9923340fdd85..852a7a12f6b5 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/shard/IndexShardTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/shard/IndexShardTestCase.java @@ -504,7 +504,8 @@ protected IndexShard newShard( breakerService, IndexModule.DEFAULT_SNAPSHOT_COMMIT_SUPPLIER, relativeTimeSupplier, - null + null, + ReplicationTracker.DEFAULT_FACTORY ); indexShard.addShardFailureCallback(DEFAULT_SHARD_FAILURE_HANDLER); success = true; From 89c396f18bc0bfc4d1e613a83e6ae3f3fd8ee5a6 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Wed, 14 Dec 2022 07:20:04 -0800 Subject: [PATCH 270/919] Increase the number of threads of GET threadpool (#92309) Elasticsearch executes GET requests somewhat similar to SEARCH requests. Hence, they should have similar thread pools. Increasing the number of threads of the GET threadpool should help use-cases with many GET, MGETs requests. --- docs/changelog/92309.yaml | 5 +++++ .../action/search/TransportMultiSearchAction.java | 2 +- .../java/org/elasticsearch/threadpool/ThreadPool.java | 9 ++++++--- 3 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 docs/changelog/92309.yaml diff --git a/docs/changelog/92309.yaml b/docs/changelog/92309.yaml new file mode 100644 index 000000000000..e024ca73f56a --- /dev/null +++ b/docs/changelog/92309.yaml @@ -0,0 +1,5 @@ +pr: 92309 +summary: Increase the number of threads of GET threadpool +area: Search +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/action/search/TransportMultiSearchAction.java b/server/src/main/java/org/elasticsearch/action/search/TransportMultiSearchAction.java index 648f8e6c8ee0..a368cecd2677 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportMultiSearchAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportMultiSearchAction.java @@ -108,7 +108,7 @@ protected void doExecute(Task task, MultiSearchRequest request, ActionListener... customBui new ScalingExecutorBuilder(Names.GENERIC, 4, genericThreadPoolMax, TimeValue.timeValueSeconds(30), false) ); builders.put(Names.WRITE, new FixedExecutorBuilder(settings, Names.WRITE, allocatedProcessors, 10000, false)); - builders.put(Names.GET, new FixedExecutorBuilder(settings, Names.GET, allocatedProcessors, 1000, false)); + builders.put(Names.GET, new FixedExecutorBuilder(settings, Names.GET, searchOrGetThreadPoolSize(allocatedProcessors), 1000, false)); builders.put(Names.ANALYZE, new FixedExecutorBuilder(settings, Names.ANALYZE, 1, 16, false)); - builders.put(Names.SEARCH, new FixedExecutorBuilder(settings, Names.SEARCH, searchThreadPoolSize(allocatedProcessors), 1000, true)); + builders.put( + Names.SEARCH, + new FixedExecutorBuilder(settings, Names.SEARCH, searchOrGetThreadPoolSize(allocatedProcessors), 1000, true) + ); builders.put(Names.SEARCH_COORDINATION, new FixedExecutorBuilder(settings, Names.SEARCH_COORDINATION, halfProcMaxAt5, 1000, true)); builders.put( Names.AUTO_COMPLETE, @@ -557,7 +560,7 @@ static int oneEighthAllocatedProcessors(final int allocatedProcessors) { return boundedBy(allocatedProcessors / 8, 1, Integer.MAX_VALUE); } - public static int searchThreadPoolSize(final int allocatedProcessors) { + public static int searchOrGetThreadPoolSize(final int allocatedProcessors) { return ((allocatedProcessors * 3) / 2) + 1; } From 60870d604694bee27eaccc0a425bf84f3fa78d91 Mon Sep 17 00:00:00 2001 From: Iraklis Psaroudakis Date: Wed, 14 Dec 2022 17:22:12 +0200 Subject: [PATCH 271/919] Move GCS TestUtils to fixture (#92370) --- .../gcs/GoogleCloudStorageBlobStoreRepositoryTests.java | 1 + .../gcs/GoogleCloudStorageBlobContainerRetriesTests.java | 2 +- .../gcs-fixture/src/main/java/fixture}/gcs/TestUtils.java | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) rename {modules/repository-gcs/src/test/java/org/elasticsearch/repositories => test/fixtures/gcs-fixture/src/main/java/fixture}/gcs/TestUtils.java (93%) diff --git a/modules/repository-gcs/src/internalClusterTest/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java b/modules/repository-gcs/src/internalClusterTest/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java index d2cd3dc6a2b2..b658b036ffed 100644 --- a/modules/repository-gcs/src/internalClusterTest/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java +++ b/modules/repository-gcs/src/internalClusterTest/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java @@ -10,6 +10,7 @@ import fixture.gcs.FakeOAuth2HttpHandler; import fixture.gcs.GoogleCloudStorageHttpHandler; +import fixture.gcs.TestUtils; import com.google.api.gax.retrying.RetrySettings; import com.google.cloud.http.HttpTransportOptions; diff --git a/modules/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobContainerRetriesTests.java b/modules/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobContainerRetriesTests.java index 3a9a4fa99c57..5079ef71a218 100644 --- a/modules/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobContainerRetriesTests.java +++ b/modules/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobContainerRetriesTests.java @@ -60,6 +60,7 @@ import static fixture.gcs.GoogleCloudStorageHttpHandler.getContentRangeLimit; import static fixture.gcs.GoogleCloudStorageHttpHandler.getContentRangeStart; import static fixture.gcs.GoogleCloudStorageHttpHandler.parseMultipartRequestBody; +import static fixture.gcs.TestUtils.createServiceAccount; import static java.nio.charset.StandardCharsets.UTF_8; import static org.elasticsearch.repositories.blobstore.ESBlobStoreRepositoryIntegTestCase.randomBytes; import static org.elasticsearch.repositories.gcs.GoogleCloudStorageBlobStore.MAX_DELETES_PER_BATCH; @@ -67,7 +68,6 @@ import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.ENDPOINT_SETTING; import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.READ_TIMEOUT_SETTING; import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.TOKEN_URI_SETTING; -import static org.elasticsearch.repositories.gcs.TestUtils.createServiceAccount; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; diff --git a/modules/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/TestUtils.java b/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/TestUtils.java similarity index 93% rename from modules/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/TestUtils.java rename to test/fixtures/gcs-fixture/src/main/java/fixture/gcs/TestUtils.java index 800846fbbecf..3d831e5212e4 100644 --- a/modules/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/TestUtils.java +++ b/test/fixtures/gcs-fixture/src/main/java/fixture/gcs/TestUtils.java @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -package org.elasticsearch.repositories.gcs; +package fixture.gcs; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentType; @@ -16,14 +16,14 @@ import java.util.Random; import java.util.UUID; -final class TestUtils { +public final class TestUtils { private TestUtils() {} /** * Creates a random Service Account file for testing purpose */ - static byte[] createServiceAccount(final Random random) { + public static byte[] createServiceAccount(final Random random) { try { final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); From a3f8abb953a85a7517d7a220cbd11fc010c0f8b1 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski <6207777+grcevski@users.noreply.github.com> Date: Wed, 14 Dec 2022 10:39:35 -0500 Subject: [PATCH 272/919] Switch usages from KeyStoreWrapper to SecureSettings (#92339) Move away from using the KeyStoreWrapper type directly and switch to SecureSettings, where possible. --- .../server/cli/APMJvmOptions.java | 17 ++++++++-------- .../server/cli/JvmOptionsParser.java | 12 +++++------ .../elasticsearch/server/cli/ServerCli.java | 3 ++- .../server/cli/ServerProcess.java | 20 +++++++++---------- .../server/cli/ServerCliTests.java | 3 ++- .../common/settings/SecureSettings.java | 2 +- .../common/settings/Settings.java | 2 +- .../settings/StatelessSecureSettings.java | 4 ++-- .../NotificationServiceTests.java | 2 +- 9 files changed, 33 insertions(+), 32 deletions(-) diff --git a/distribution/tools/server-cli/src/main/java/org/elasticsearch/server/cli/APMJvmOptions.java b/distribution/tools/server-cli/src/main/java/org/elasticsearch/server/cli/APMJvmOptions.java index b89eb166aad8..c13ab9d10681 100644 --- a/distribution/tools/server-cli/src/main/java/org/elasticsearch/server/cli/APMJvmOptions.java +++ b/distribution/tools/server-cli/src/main/java/org/elasticsearch/server/cli/APMJvmOptions.java @@ -13,7 +13,7 @@ import org.elasticsearch.cli.ExitCodes; import org.elasticsearch.cli.UserException; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.settings.KeyStoreWrapper; +import org.elasticsearch.common.settings.SecureSettings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.Nullable; @@ -133,11 +133,10 @@ class APMJvmOptions { * because it will be deleted once Elasticsearch starts. * * @param settings the Elasticsearch settings to consider - * @param keystore a wrapper to access the keystore, or null if there is no keystore + * @param secrets a wrapper to access the secrets, or null if there is no secrets * @param tmpdir Elasticsearch's temporary directory, where the config file will be written */ - static List apmJvmOptions(Settings settings, @Nullable KeyStoreWrapper keystore, Path tmpdir) throws UserException, - IOException { + static List apmJvmOptions(Settings settings, @Nullable SecureSettings secrets, Path tmpdir) throws UserException, IOException { final Path agentJar = findAgentJar(); if (agentJar == null) { @@ -158,8 +157,8 @@ static List apmJvmOptions(Settings settings, @Nullable KeyStoreWrapper k } } - if (keystore != null) { - extractSecureSettings(keystore, propertiesMap); + if (secrets != null) { + extractSecureSettings(secrets, propertiesMap); } final Map dynamicSettings = extractDynamicSettings(propertiesMap); @@ -180,11 +179,11 @@ static String agentCommandLineOption(Path agentJar, Path tmpPropertiesFile) { return "-javaagent:" + agentJar + "=c=" + tmpPropertiesFile; } - private static void extractSecureSettings(KeyStoreWrapper keystore, Map propertiesMap) { - final Set settingNames = keystore.getSettingNames(); + private static void extractSecureSettings(SecureSettings secrets, Map propertiesMap) { + final Set settingNames = secrets.getSettingNames(); for (String key : List.of("api_key", "secret_token")) { if (settingNames.contains("tracing.apm." + key)) { - try (SecureString token = keystore.getString("tracing.apm." + key)) { + try (SecureString token = secrets.getString("tracing.apm." + key)) { propertiesMap.put(key, token.toString()); } } diff --git a/distribution/tools/server-cli/src/main/java/org/elasticsearch/server/cli/JvmOptionsParser.java b/distribution/tools/server-cli/src/main/java/org/elasticsearch/server/cli/JvmOptionsParser.java index b20aad3a0b84..326ed767e376 100644 --- a/distribution/tools/server-cli/src/main/java/org/elasticsearch/server/cli/JvmOptionsParser.java +++ b/distribution/tools/server-cli/src/main/java/org/elasticsearch/server/cli/JvmOptionsParser.java @@ -11,7 +11,7 @@ import org.elasticsearch.bootstrap.ServerArgs; import org.elasticsearch.cli.ExitCodes; import org.elasticsearch.cli.UserException; -import org.elasticsearch.common.settings.KeyStoreWrapper; +import org.elasticsearch.common.settings.SecureSettings; import java.io.BufferedReader; import java.io.IOException; @@ -70,7 +70,7 @@ SortedMap invalidLines() { * files in the {@code jvm.options.d} directory, and the options given by the {@code ES_JAVA_OPTS} environment * variable. * - * @param keystore the installation's keystore + * @param secrets the installation's secrets * @param configDir the ES config dir * @param tmpDir the directory that should be passed to {@code -Djava.io.tmpdir} * @param envOptions the options passed through the ES_JAVA_OPTS env var @@ -79,7 +79,7 @@ SortedMap invalidLines() { * @throws IOException if there is a problem reading any of the files * @throws UserException if there is a problem parsing the `jvm.options` file or `jvm.options.d` files */ - static List determineJvmOptions(ServerArgs args, KeyStoreWrapper keystore, Path configDir, Path tmpDir, String envOptions) + static List determineJvmOptions(ServerArgs args, SecureSettings secrets, Path configDir, Path tmpDir, String envOptions) throws InterruptedException, IOException, UserException { final JvmOptionsParser parser = new JvmOptionsParser(); @@ -89,7 +89,7 @@ static List determineJvmOptions(ServerArgs args, KeyStoreWrapper keystor substitutions.put("ES_PATH_CONF", configDir.toString()); try { - return parser.jvmOptions(args, keystore, configDir, tmpDir, envOptions, substitutions); + return parser.jvmOptions(args, secrets, configDir, tmpDir, envOptions, substitutions); } catch (final JvmOptionsFileParserException e) { final String errorMessage = String.format( Locale.ROOT, @@ -120,7 +120,7 @@ static List determineJvmOptions(ServerArgs args, KeyStoreWrapper keystor private List jvmOptions( ServerArgs args, - KeyStoreWrapper keystore, + SecureSettings secrets, final Path config, Path tmpDir, final String esJavaOpts, @@ -141,7 +141,7 @@ private List jvmOptions( final List ergonomicJvmOptions = JvmErgonomics.choose(substitutedJvmOptions); final List systemJvmOptions = SystemJvmOptions.systemJvmOptions(); - final List apmOptions = APMJvmOptions.apmJvmOptions(args.nodeSettings(), keystore, tmpDir); + final List apmOptions = APMJvmOptions.apmJvmOptions(args.nodeSettings(), secrets, tmpDir); final List finalJvmOptions = new ArrayList<>( systemJvmOptions.size() + substitutedJvmOptions.size() + ergonomicJvmOptions.size() + apmOptions.size() diff --git a/distribution/tools/server-cli/src/main/java/org/elasticsearch/server/cli/ServerCli.java b/distribution/tools/server-cli/src/main/java/org/elasticsearch/server/cli/ServerCli.java index 73269b8c719f..45fd07dc488d 100644 --- a/distribution/tools/server-cli/src/main/java/org/elasticsearch/server/cli/ServerCli.java +++ b/distribution/tools/server-cli/src/main/java/org/elasticsearch/server/cli/ServerCli.java @@ -23,6 +23,7 @@ import org.elasticsearch.cli.UserException; import org.elasticsearch.common.cli.EnvironmentAwareCommand; import org.elasticsearch.common.settings.KeyStoreWrapper; +import org.elasticsearch.common.settings.SecureSettings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.env.Environment; import org.elasticsearch.monitor.jvm.JvmInfo; @@ -229,7 +230,7 @@ protected Command loadTool(String toolname, String libs) { } // protected to allow tests to override - protected ServerProcess startServer(Terminal terminal, ProcessInfo processInfo, ServerArgs args, KeyStoreWrapper keystore) + protected ServerProcess startServer(Terminal terminal, ProcessInfo processInfo, ServerArgs args, SecureSettings keystore) throws UserException { return ServerProcess.start(terminal, processInfo, args, keystore); } diff --git a/distribution/tools/server-cli/src/main/java/org/elasticsearch/server/cli/ServerProcess.java b/distribution/tools/server-cli/src/main/java/org/elasticsearch/server/cli/ServerProcess.java index 674f9f12c916..6968ec47b038 100644 --- a/distribution/tools/server-cli/src/main/java/org/elasticsearch/server/cli/ServerProcess.java +++ b/distribution/tools/server-cli/src/main/java/org/elasticsearch/server/cli/ServerProcess.java @@ -15,7 +15,7 @@ import org.elasticsearch.cli.Terminal; import org.elasticsearch.cli.UserException; import org.elasticsearch.common.io.stream.OutputStreamStreamOutput; -import org.elasticsearch.common.settings.KeyStoreWrapper; +import org.elasticsearch.common.settings.SecureSettings; import org.elasticsearch.core.IOUtils; import org.elasticsearch.core.PathUtils; import org.elasticsearch.core.SuppressForbidden; @@ -37,7 +37,7 @@ /** * A helper to control a {@link Process} running the main Elasticsearch server. * - *

The process can be started by calling {@link #start(Terminal, ProcessInfo, ServerArgs, KeyStoreWrapper)}. + *

The process can be started by calling {@link #start(Terminal, ProcessInfo, ServerArgs, SecureSettings)}. * The process is controlled by internally sending arguments and control signals on stdin, * and receiving control signals on stderr. The start method does not return until the * server is ready to process requests and has exited the bootstrap thread. @@ -67,7 +67,7 @@ public class ServerProcess { // this allows mocking the process building by tests interface OptionsBuilder { - List getJvmOptions(ServerArgs args, KeyStoreWrapper keyStoreWrapper, Path configDir, Path tmpDir, String envOptions) + List getJvmOptions(ServerArgs args, SecureSettings secrets, Path configDir, Path tmpDir, String envOptions) throws InterruptedException, IOException, UserException; } @@ -82,13 +82,13 @@ interface ProcessStarter { * @param terminal A terminal to connect the standard inputs and outputs to for the new process. * @param processInfo Info about the current process, for passing through to the subprocess. * @param args Arguments to the server process. - * @param keystore A keystore for accessing secrets. + * @param secrets A secrets for accessing secrets. * @return A running server process that is ready for requests * @throws UserException If the process failed during bootstrap */ - public static ServerProcess start(Terminal terminal, ProcessInfo processInfo, ServerArgs args, KeyStoreWrapper keystore) + public static ServerProcess start(Terminal terminal, ProcessInfo processInfo, ServerArgs args, SecureSettings secrets) throws UserException { - return start(terminal, processInfo, args, keystore, JvmOptionsParser::determineJvmOptions, ProcessBuilder::start); + return start(terminal, processInfo, args, secrets, JvmOptionsParser::determineJvmOptions, ProcessBuilder::start); } // package private so tests can mock options building and process starting @@ -96,7 +96,7 @@ static ServerProcess start( Terminal terminal, ProcessInfo processInfo, ServerArgs args, - KeyStoreWrapper keystore, + SecureSettings secrets, OptionsBuilder optionsBuilder, ProcessStarter processStarter ) throws UserException { @@ -105,7 +105,7 @@ static ServerProcess start( boolean success = false; try { - jvmProcess = createProcess(args, keystore, processInfo, args.configDir(), optionsBuilder, processStarter); + jvmProcess = createProcess(args, secrets, processInfo, args.configDir(), optionsBuilder, processStarter); errorPump = new ErrorPumpThread(terminal.getErrorWriter(), jvmProcess.getErrorStream()); errorPump.start(); sendArgs(args, jvmProcess.getOutputStream()); @@ -199,7 +199,7 @@ private void sendShutdownMarker() { private static Process createProcess( ServerArgs args, - KeyStoreWrapper keystore, + SecureSettings secrets, ProcessInfo processInfo, Path configDir, OptionsBuilder optionsBuilder, @@ -211,7 +211,7 @@ private static Process createProcess( envVars.put("LIBFFI_TMPDIR", tempDir.toString()); } - List jvmOptions = optionsBuilder.getJvmOptions(args, keystore, configDir, tempDir, envVars.remove("ES_JAVA_OPTS")); + List jvmOptions = optionsBuilder.getJvmOptions(args, secrets, configDir, tempDir, envVars.remove("ES_JAVA_OPTS")); // also pass through distribution type jvmOptions.add("-Des.distribution.type=" + processInfo.sysprops().get("es.distribution.type")); diff --git a/distribution/tools/server-cli/src/test/java/org/elasticsearch/server/cli/ServerCliTests.java b/distribution/tools/server-cli/src/test/java/org/elasticsearch/server/cli/ServerCliTests.java index 7a189563801e..77134421b5de 100644 --- a/distribution/tools/server-cli/src/test/java/org/elasticsearch/server/cli/ServerCliTests.java +++ b/distribution/tools/server-cli/src/test/java/org/elasticsearch/server/cli/ServerCliTests.java @@ -22,6 +22,7 @@ import org.elasticsearch.cli.UserException; import org.elasticsearch.common.cli.EnvironmentAwareCommand; import org.elasticsearch.common.settings.KeyStoreWrapper; +import org.elasticsearch.common.settings.SecureSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.monitor.jvm.JvmInfo; @@ -436,7 +437,7 @@ protected Command loadTool(String toolname, String libs) { } @Override - protected ServerProcess startServer(Terminal terminal, ProcessInfo processInfo, ServerArgs args, KeyStoreWrapper keystore) { + protected ServerProcess startServer(Terminal terminal, ProcessInfo processInfo, ServerArgs args, SecureSettings secrets) { if (argsValidator != null) { argsValidator.accept(args); } diff --git a/server/src/main/java/org/elasticsearch/common/settings/SecureSettings.java b/server/src/main/java/org/elasticsearch/common/settings/SecureSettings.java index 25edbe6e846b..83c99a8e1aed 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/SecureSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/SecureSettings.java @@ -26,7 +26,7 @@ public interface SecureSettings extends Closeable { Set getSettingNames(); /** Return a string setting. The {@link SecureString} should be closed once it is used. */ - SecureString getString(String setting) throws GeneralSecurityException; + SecureString getString(String setting); /** Return a file setting. The {@link InputStream} should be closed once it is used. */ InputStream getFile(String setting) throws GeneralSecurityException; diff --git a/server/src/main/java/org/elasticsearch/common/settings/Settings.java b/server/src/main/java/org/elasticsearch/common/settings/Settings.java index 1ebb189a358e..ec527b5d5476 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/Settings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/Settings.java @@ -1503,7 +1503,7 @@ public Set getSettingNames() { } @Override - public SecureString getString(String setting) throws GeneralSecurityException { + public SecureString getString(String setting) { return delegate.getString(addPrefix.apply(setting)); } diff --git a/server/src/main/java/org/elasticsearch/common/settings/StatelessSecureSettings.java b/server/src/main/java/org/elasticsearch/common/settings/StatelessSecureSettings.java index 7d221bcb5a15..7417910a15ea 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/StatelessSecureSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/StatelessSecureSettings.java @@ -62,7 +62,7 @@ public Set getSettingNames() { } @Override - public SecureString getString(String setting) throws GeneralSecurityException { + public SecureString getString(String setting) { return new SecureString(STATELESS_SECURE_SETTINGS.getConcreteSetting(PREFIX + setting).get(settings).toCharArray()); } @@ -74,7 +74,7 @@ public InputStream getFile(String setting) throws GeneralSecurityException { } @Override - public byte[] getSHA256Digest(String setting) throws GeneralSecurityException { + public byte[] getSHA256Digest(String setting) { return MessageDigests.sha256() .digest(STATELESS_SECURE_SETTINGS.getConcreteSetting(PREFIX + setting).get(settings).getBytes(StandardCharsets.UTF_8)); } diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/notification/NotificationServiceTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/notification/NotificationServiceTests.java index b8bfdb7e1738..c695a17a1fa7 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/notification/NotificationServiceTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/notification/NotificationServiceTests.java @@ -253,7 +253,7 @@ public boolean isLoaded() { } @Override - public SecureString getString(String setting) throws GeneralSecurityException { + public SecureString getString(String setting) { return new SecureString(secureSettingsMap.get(setting)); } From 8ebf29ba740a5b8118c9e57e56591a2b233fbec4 Mon Sep 17 00:00:00 2001 From: Jack Conradson Date: Wed, 14 Dec 2022 08:17:14 -0800 Subject: [PATCH 273/919] Add vector distance scoring to micro benchmarks (#92340) This change adds the foundation for micro benchmarks for vector distance methods used in scoring vector relevance specifically for scripting (though, some of these are proxies to Lucene methods used directly by HNSW). The parameters included are element (byte, float), type (knn, binary), dims (96), and function (dot, cosine, l1, l2). The design attempts to be easy to extend for local individual tests as well. --- .../vector/DistanceFunctionBenchmark.java | 467 ++++++++++++++++++ docs/changelog/92340.yaml | 5 + 2 files changed, 472 insertions(+) create mode 100644 benchmarks/src/main/java/org/elasticsearch/benchmark/vector/DistanceFunctionBenchmark.java create mode 100644 docs/changelog/92340.yaml diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/vector/DistanceFunctionBenchmark.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/vector/DistanceFunctionBenchmark.java new file mode 100644 index 000000000000..e3b05b8b40f4 --- /dev/null +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/vector/DistanceFunctionBenchmark.java @@ -0,0 +1,467 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.benchmark.vector; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.Version; +import org.elasticsearch.script.field.vectors.BinaryDenseVector; +import org.elasticsearch.script.field.vectors.ByteBinaryDenseVector; +import org.elasticsearch.script.field.vectors.ByteKnnDenseVector; +import org.elasticsearch.script.field.vectors.KnnDenseVector; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OperationsPerInvocation; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * Various benchmarks for the distance functions + * used by indexed and non-indexed vectors. + * Parameters include element, dims, function, and type. + * For individual local tests it may be useful to increase + * fork, measurement, and operations per invocation. (Note + * to also update the benchmark loop if operations per invocation + * is increased.) + */ +@Fork(1) +@Warmup(iterations = 1) +@Measurement(iterations = 2) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@OperationsPerInvocation(25000) +@State(Scope.Benchmark) +public class DistanceFunctionBenchmark { + + @Param({ "float", "byte" }) + private String element; + + @Param({ "96" }) + private int dims; + + @Param({ "dot", "cosine", "l1", "l2" }) + private String function; + + @Param({ "knn", "binary" }) + private String type; + + private abstract static class BenchmarkFunction { + + final int dims; + + private BenchmarkFunction(int dims) { + this.dims = dims; + } + + abstract void execute(Consumer consumer); + } + + private abstract static class KnnFloatBenchmarkFunction extends BenchmarkFunction { + + final float[] docVector; + final float[] queryVector; + + private KnnFloatBenchmarkFunction(int dims, boolean normalize) { + super(dims); + + docVector = new float[dims]; + queryVector = new float[dims]; + + float docMagnitude = 0f; + float queryMagnitude = 0f; + + for (int i = 0; i < dims; ++i) { + docVector[i] = (float) (dims - i); + queryVector[i] = (float) i; + + docMagnitude += (float) (dims - i); + queryMagnitude += (float) i; + } + + docMagnitude /= dims; + queryMagnitude /= dims; + + if (normalize) { + for (int i = 0; i < dims; ++i) { + docVector[i] /= docMagnitude; + queryVector[i] /= queryMagnitude; + } + } + } + } + + private abstract static class BinaryFloatBenchmarkFunction extends BenchmarkFunction { + + final BytesRef docVector; + final float[] queryVector; + + private BinaryFloatBenchmarkFunction(int dims, boolean normalize) { + super(dims); + + float[] docVector = new float[dims]; + queryVector = new float[dims]; + + float docMagnitude = 0f; + float queryMagnitude = 0f; + + for (int i = 0; i < dims; ++i) { + docVector[i] = (float) (dims - i); + queryVector[i] = (float) i; + + docMagnitude += (float) (dims - i); + queryMagnitude += (float) i; + } + + docMagnitude /= dims; + queryMagnitude /= dims; + + ByteBuffer byteBuffer = ByteBuffer.allocate(dims * 4 + 4); + + for (int i = 0; i < dims; ++i) { + if (normalize) { + docVector[i] /= docMagnitude; + queryVector[i] /= queryMagnitude; + } + + byteBuffer.putFloat(docVector[i]); + } + + byteBuffer.putFloat(docMagnitude); + this.docVector = new BytesRef(byteBuffer.array()); + } + } + + private abstract static class KnnByteBenchmarkFunction extends BenchmarkFunction { + + final BytesRef docVector; + final byte[] queryVector; + + final float queryMagnitude; + + private KnnByteBenchmarkFunction(int dims) { + super(dims); + + ByteBuffer docVector = ByteBuffer.allocate(dims); + queryVector = new byte[dims]; + + float queryMagnitude = 0f; + + for (int i = 0; i < dims; ++i) { + docVector.put((byte) (dims - i)); + queryVector[i] = (byte) i; + + queryMagnitude += (float) i; + } + + this.docVector = new BytesRef(docVector.array()); + this.queryMagnitude = queryMagnitude / dims; + } + } + + private abstract static class BinaryByteBenchmarkFunction extends BenchmarkFunction { + + final BytesRef docVector; + final byte[] queryVector; + + final float queryMagnitude; + + private BinaryByteBenchmarkFunction(int dims) { + super(dims); + + ByteBuffer docVector = ByteBuffer.allocate(dims + 4); + queryVector = new byte[dims]; + + float docMagnitude = 0f; + float queryMagnitude = 0f; + + for (int i = 0; i < dims; ++i) { + docVector.put((byte) (dims - i)); + queryVector[i] = (byte) i; + + docMagnitude += (float) (dims - i); + queryMagnitude += (float) i; + } + + docVector.putFloat(docMagnitude / dims); + this.docVector = new BytesRef(docVector.array()); + this.queryMagnitude = queryMagnitude / dims; + + } + } + + private static class DotKnnFloatBenchmarkFunction extends KnnFloatBenchmarkFunction { + + private DotKnnFloatBenchmarkFunction(int dims) { + super(dims, false); + } + + @Override + public void execute(Consumer consumer) { + new KnnDenseVector(docVector).dotProduct(queryVector); + } + } + + private static class DotKnnByteBenchmarkFunction extends KnnByteBenchmarkFunction { + + private DotKnnByteBenchmarkFunction(int dims) { + super(dims); + } + + @Override + public void execute(Consumer consumer) { + new ByteKnnDenseVector(docVector).dotProduct(queryVector); + } + } + + private static class DotBinaryFloatBenchmarkFunction extends BinaryFloatBenchmarkFunction { + + private DotBinaryFloatBenchmarkFunction(int dims) { + super(dims, false); + } + + @Override + public void execute(Consumer consumer) { + new BinaryDenseVector(docVector, dims, Version.CURRENT).dotProduct(queryVector); + } + } + + private static class DotBinaryByteBenchmarkFunction extends BinaryByteBenchmarkFunction { + + private DotBinaryByteBenchmarkFunction(int dims) { + super(dims); + } + + @Override + public void execute(Consumer consumer) { + new ByteBinaryDenseVector(docVector, dims).dotProduct(queryVector); + } + } + + private static class CosineKnnFloatBenchmarkFunction extends KnnFloatBenchmarkFunction { + + private CosineKnnFloatBenchmarkFunction(int dims) { + super(dims, true); + } + + @Override + public void execute(Consumer consumer) { + new KnnDenseVector(docVector).cosineSimilarity(queryVector, false); + } + } + + private static class CosineKnnByteBenchmarkFunction extends KnnByteBenchmarkFunction { + + private CosineKnnByteBenchmarkFunction(int dims) { + super(dims); + } + + @Override + public void execute(Consumer consumer) { + new ByteKnnDenseVector(docVector).cosineSimilarity(queryVector, queryMagnitude); + } + } + + private static class CosineBinaryFloatBenchmarkFunction extends BinaryFloatBenchmarkFunction { + + private CosineBinaryFloatBenchmarkFunction(int dims) { + super(dims, true); + } + + @Override + public void execute(Consumer consumer) { + new BinaryDenseVector(docVector, dims, Version.CURRENT).cosineSimilarity(queryVector, false); + } + } + + private static class CosineBinaryByteBenchmarkFunction extends BinaryByteBenchmarkFunction { + + private CosineBinaryByteBenchmarkFunction(int dims) { + super(dims); + } + + @Override + public void execute(Consumer consumer) { + new ByteBinaryDenseVector(docVector, dims).cosineSimilarity(queryVector, queryMagnitude); + } + } + + private static class L1KnnFloatBenchmarkFunction extends KnnFloatBenchmarkFunction { + + private L1KnnFloatBenchmarkFunction(int dims) { + super(dims, false); + } + + @Override + public void execute(Consumer consumer) { + new KnnDenseVector(docVector).l1Norm(queryVector); + } + } + + private static class L1KnnByteBenchmarkFunction extends KnnByteBenchmarkFunction { + + private L1KnnByteBenchmarkFunction(int dims) { + super(dims); + } + + @Override + public void execute(Consumer consumer) { + new ByteKnnDenseVector(docVector).l1Norm(queryVector); + } + } + + private static class L1BinaryFloatBenchmarkFunction extends BinaryFloatBenchmarkFunction { + + private L1BinaryFloatBenchmarkFunction(int dims) { + super(dims, true); + } + + @Override + public void execute(Consumer consumer) { + new BinaryDenseVector(docVector, dims, Version.CURRENT).l1Norm(queryVector); + } + } + + private static class L1BinaryByteBenchmarkFunction extends BinaryByteBenchmarkFunction { + + private L1BinaryByteBenchmarkFunction(int dims) { + super(dims); + } + + @Override + public void execute(Consumer consumer) { + new ByteBinaryDenseVector(docVector, dims).l1Norm(queryVector); + } + } + + private static class L2KnnFloatBenchmarkFunction extends KnnFloatBenchmarkFunction { + + private L2KnnFloatBenchmarkFunction(int dims) { + super(dims, false); + } + + @Override + public void execute(Consumer consumer) { + new KnnDenseVector(docVector).l2Norm(queryVector); + } + } + + private static class L2KnnByteBenchmarkFunction extends KnnByteBenchmarkFunction { + + private L2KnnByteBenchmarkFunction(int dims) { + super(dims); + } + + @Override + public void execute(Consumer consumer) { + new ByteKnnDenseVector(docVector).l2Norm(queryVector); + } + } + + private static class L2BinaryFloatBenchmarkFunction extends BinaryFloatBenchmarkFunction { + + private L2BinaryFloatBenchmarkFunction(int dims) { + super(dims, true); + } + + @Override + public void execute(Consumer consumer) { + new BinaryDenseVector(docVector, dims, Version.CURRENT).l1Norm(queryVector); + } + } + + private static class L2BinaryByteBenchmarkFunction extends BinaryByteBenchmarkFunction { + + private L2BinaryByteBenchmarkFunction(int dims) { + super(dims); + } + + @Override + public void execute(Consumer consumer) { + consumer.accept(new ByteBinaryDenseVector(docVector, dims).l2Norm(queryVector)); + } + } + + private BenchmarkFunction benchmarkFunction; + + @Setup + public void setBenchmarkFunction() { + switch (element) { + case "float" -> { + switch (function) { + case "dot" -> benchmarkFunction = switch (type) { + case "knn" -> new DotKnnFloatBenchmarkFunction(dims); + case "binary" -> new DotBinaryFloatBenchmarkFunction(dims); + default -> throw new UnsupportedOperationException("unexpected type [" + type + "]"); + }; + case "cosine" -> benchmarkFunction = switch (type) { + case "knn" -> new CosineKnnFloatBenchmarkFunction(dims); + case "binary" -> new CosineBinaryFloatBenchmarkFunction(dims); + default -> throw new UnsupportedOperationException("unexpected type [" + type + "]"); + }; + case "l1" -> benchmarkFunction = switch (type) { + case "knn" -> new L1KnnFloatBenchmarkFunction(dims); + case "binary" -> new L1BinaryFloatBenchmarkFunction(dims); + default -> throw new UnsupportedOperationException("unexpected type [" + type + "]"); + }; + case "l2" -> benchmarkFunction = switch (type) { + case "knn" -> new L2KnnFloatBenchmarkFunction(dims); + case "binary" -> new L2BinaryFloatBenchmarkFunction(dims); + default -> throw new UnsupportedOperationException("unexpected type [" + type + "]"); + }; + default -> throw new UnsupportedOperationException("unexpected function [" + function + "]"); + } + } + case "byte" -> { + switch (function) { + case "dot" -> benchmarkFunction = switch (type) { + case "knn" -> new DotKnnByteBenchmarkFunction(dims); + case "binary" -> new DotBinaryByteBenchmarkFunction(dims); + default -> throw new UnsupportedOperationException("unexpected type [" + type + "]"); + }; + case "cosine" -> benchmarkFunction = switch (type) { + case "knn" -> new CosineKnnByteBenchmarkFunction(dims); + case "binary" -> new CosineBinaryByteBenchmarkFunction(dims); + default -> throw new UnsupportedOperationException("unexpected type [" + type + "]"); + }; + case "l1" -> benchmarkFunction = switch (type) { + case "knn" -> new L1KnnByteBenchmarkFunction(dims); + case "binary" -> new L1BinaryByteBenchmarkFunction(dims); + default -> throw new UnsupportedOperationException("unexpected type [" + type + "]"); + }; + case "l2" -> benchmarkFunction = switch (type) { + case "knn" -> new L2KnnByteBenchmarkFunction(dims); + case "binary" -> new L2BinaryByteBenchmarkFunction(dims); + default -> throw new UnsupportedOperationException("unexpected type [" + type + "]"); + }; + default -> throw new UnsupportedOperationException("unexpected function [" + function + "]"); + } + } + default -> throw new UnsupportedOperationException("unexpected element [" + element + "]"); + } + ; + } + + @Benchmark + public void benchmark() throws IOException { + for (int i = 0; i < 25000; ++i) { + benchmarkFunction.execute(Object::toString); + } + } +} diff --git a/docs/changelog/92340.yaml b/docs/changelog/92340.yaml new file mode 100644 index 000000000000..3ed47c66452b --- /dev/null +++ b/docs/changelog/92340.yaml @@ -0,0 +1,5 @@ +pr: 92340 +summary: Add vector distance scoring to micro benchmarks +area: Performance +type: enhancement +issues: [] From 0ceb062491d893d25942e0ba8b61c309b328599a Mon Sep 17 00:00:00 2001 From: Abdon Pijpelink Date: Wed, 14 Dec 2022 18:44:20 +0100 Subject: [PATCH 274/919] Move 'Explore by use case' block down (#92231) --- docs/reference/index-custom-title-page.html | 75 ++++++++++----------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/docs/reference/index-custom-title-page.html b/docs/reference/index-custom-title-page.html index 0b4e05bf2742..ecd8c6347bb1 100644 --- a/docs/reference/index-custom-title-page.html +++ b/docs/reference/index-custom-title-page.html @@ -68,44 +68,6 @@

Search and analyze your data

-

Explore by use case

- - -

Get to know Elasticsearch

@@ -229,5 +191,42 @@

+

Explore by use case

+ +

View all Elastic docs

From 1c06ed82361c2af18418a371c82febbc0ba5e622 Mon Sep 17 00:00:00 2001 From: Joe Gallo Date: Wed, 14 Dec 2022 15:43:39 -0500 Subject: [PATCH 275/919] Speed up ingest geoip processors (#92372) --- docs/changelog/92372.yaml | 16 +++++++++++ .../geoip/DatabaseReaderLazyLoader.java | 8 ++---- .../ingest/geoip/GeoIpCache.java | 27 +------------------ .../plugin-metadata/plugin-security.policy | 8 ------ 4 files changed, 19 insertions(+), 40 deletions(-) create mode 100644 docs/changelog/92372.yaml diff --git a/docs/changelog/92372.yaml b/docs/changelog/92372.yaml new file mode 100644 index 000000000000..7c882b22236e --- /dev/null +++ b/docs/changelog/92372.yaml @@ -0,0 +1,16 @@ +pr: 92372 +summary: Speed up ingest geoip processors +area: Ingest Node +type: bug +issues: [] +highlight: + title: Speed up ingest geoip processors + body: |- + The `geoip` ingest processor is significantly faster. + + Previous versions of the geoip library needed special permission to execute + databinding code, requiring an expensive permissions check and + `AccessController.doPrivileged` call. The current version of the geoip + library no longer requires that, however, so the expensive code has been + removed, resulting in better performance for the ingest geoip processor. + notable: true diff --git a/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/DatabaseReaderLazyLoader.java b/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/DatabaseReaderLazyLoader.java index 13181e1f96bb..9aecfaeedfd3 100644 --- a/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/DatabaseReaderLazyLoader.java +++ b/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/DatabaseReaderLazyLoader.java @@ -19,7 +19,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.lucene.util.SetOnce; -import org.elasticsearch.SpecialPermission; import org.elasticsearch.common.CheckedBiFunction; import org.elasticsearch.common.CheckedSupplier; import org.elasticsearch.core.Booleans; @@ -34,8 +33,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.Objects; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; @@ -187,14 +184,13 @@ private T getResponse( InetAddress ipAddress, CheckedBiFunction, Exception> responseProvider ) { - SpecialPermission.check(); - return AccessController.doPrivileged((PrivilegedAction) () -> cache.putIfAbsent(ipAddress, databasePath.toString(), ip -> { + return cache.putIfAbsent(ipAddress, databasePath.toString(), ip -> { try { return responseProvider.apply(get(), ipAddress).orElse(null); } catch (Exception e) { throw new RuntimeException(e); } - })); + }); } DatabaseReader get() throws IOException { diff --git a/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpCache.java b/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpCache.java index 01938c617db6..30c0fcb74833 100644 --- a/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpCache.java +++ b/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpCache.java @@ -15,7 +15,6 @@ import java.net.InetAddress; import java.nio.file.Path; -import java.util.Objects; import java.util.function.Function; /** @@ -82,29 +81,5 @@ public int count() { * path is needed to be included in the cache key. For example, if we only used the IP address as the key the City and ASN the same * IP may be in both with different values and we need to cache both. */ - private static class CacheKey { - - private final InetAddress ip; - private final String databasePath; - - private CacheKey(InetAddress ip, String databasePath) { - this.ip = ip; - this.databasePath = databasePath; - } - - // generated - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CacheKey cacheKey = (CacheKey) o; - return Objects.equals(ip, cacheKey.ip) && Objects.equals(databasePath, cacheKey.databasePath); - } - - // generated - @Override - public int hashCode() { - return Objects.hash(ip, databasePath); - } - } + private record CacheKey(InetAddress ip, String databasePath) {} } diff --git a/modules/ingest-geoip/src/main/plugin-metadata/plugin-security.policy b/modules/ingest-geoip/src/main/plugin-metadata/plugin-security.policy index 2f1e80e8e557..7002fba5c0c4 100644 --- a/modules/ingest-geoip/src/main/plugin-metadata/plugin-security.policy +++ b/modules/ingest-geoip/src/main/plugin-metadata/plugin-security.policy @@ -7,13 +7,5 @@ */ grant { - // needed because jackson-databind is using Class#getDeclaredConstructors(), Class#getDeclaredMethods() and - // Class#getDeclaredAnnotations() to find all public, private, protected, package protected and - // private constructors, methods or annotations. Just locating all public constructors, methods and annotations - // should be enough, so this permission wouldn't then be needed. Unfortunately this is not what jackson-databind does - // or can be configured to do. - permission java.lang.RuntimePermission "accessDeclaredMembers"; - // Also needed because of jackson-databind: - permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; permission java.net.SocketPermission "*", "connect"; }; From 9d605b576f47c39ce7d7501a29181849eda6b713 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 15 Dec 2022 10:29:32 +1100 Subject: [PATCH 276/919] JWT realm - add support for required claims (#92314) This PR adds a new required_claims group setting that can be used to specify additional mandatory claim checks for either ID tokens or access tokens. A required claim must have either string or string list value. --- docs/changelog/92314.yaml | 5 + .../security/authc/jwt/JwtRealmSettings.java | 22 +++ .../plugin/security/qa/jwt-realm/build.gradle | 5 +- .../xpack/security/authc/jwt/JwtRestIT.java | 30 +++- .../security/authc/jwt/JwtAuthenticator.java | 23 ++- .../JwtAuthenticatorAccessTokenTypeTests.java | 20 +-- .../jwt/JwtAuthenticatorIdTokenTypeTests.java | 15 +- .../authc/jwt/JwtAuthenticatorTests.java | 169 +++++++++++++++++- .../authc/jwt/JwtRealmSettingsTests.java | 53 ++++++ 9 files changed, 302 insertions(+), 40 deletions(-) create mode 100644 docs/changelog/92314.yaml diff --git a/docs/changelog/92314.yaml b/docs/changelog/92314.yaml new file mode 100644 index 000000000000..1de076290588 --- /dev/null +++ b/docs/changelog/92314.yaml @@ -0,0 +1,5 @@ +pr: 92314 +summary: JWT realm - add support for required claims +area: Authentication +type: enhancement +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/jwt/JwtRealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/jwt/JwtRealmSettings.java index 0b57987b265a..5462c2c42361 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/jwt/JwtRealmSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/jwt/JwtRealmSettings.java @@ -8,6 +8,7 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.Strings; import org.elasticsearch.core.TimeValue; import org.elasticsearch.xpack.core.security.authc.RealmSettings; @@ -160,6 +161,7 @@ private static Set> getNonSecureSettings() { ALLOWED_SUBJECTS, FALLBACK_SUB_CLAIM, FALLBACK_AUD_CLAIM, + REQUIRED_CLAIMS, CLAIMS_PRINCIPAL.getClaim(), CLAIMS_PRINCIPAL.getPattern(), CLAIMS_GROUPS.getClaim(), @@ -302,6 +304,26 @@ public Iterator> settings() { }, Setting.Property.NodeScope) ); + public static final Setting.AffixSetting REQUIRED_CLAIMS = Setting.affixKeySetting( + RealmSettings.realmSettingPrefix(TYPE), + "required_claims", + key -> Setting.groupSetting(key + ".", settings -> { + final List invalidRequiredClaims = List.of("iss", "sub", "aud", "exp", "nbf", "iat"); + for (String name : settings.names()) { + final String fullName = key + "." + name; + if (invalidRequiredClaims.contains(name)) { + throw new IllegalArgumentException( + Strings.format("required claim [%s] cannot be one of [%s]", fullName, String.join(",", invalidRequiredClaims)) + ); + } + final List values = settings.getAsList(name); + if (values.isEmpty()) { + throw new IllegalArgumentException(Strings.format("required claim [%s] cannot be empty", fullName)); + } + } + }, Setting.Property.NodeScope) + ); + // Note: ClaimSetting is a wrapper for two individual settings: getClaim(), getPattern() public static final ClaimSetting CLAIMS_PRINCIPAL = new ClaimSetting(TYPE, "principal"); public static final ClaimSetting CLAIMS_GROUPS = new ClaimSetting(TYPE, "groups"); diff --git a/x-pack/plugin/security/qa/jwt-realm/build.gradle b/x-pack/plugin/security/qa/jwt-realm/build.gradle index 540527d948b5..14fee4918645 100644 --- a/x-pack/plugin/security/qa/jwt-realm/build.gradle +++ b/x-pack/plugin/security/qa/jwt-realm/build.gradle @@ -62,6 +62,8 @@ testClusters.matching { it.name == 'javaRestTest' }.configureEach { setting 'xpack.security.authc.realms.jwt.jwt1.claims.dn', 'dn' setting 'xpack.security.authc.realms.jwt.jwt1.claims.name', 'name' setting 'xpack.security.authc.realms.jwt.jwt1.claims.mail', 'mail' + setting 'xpack.security.authc.realms.jwt.jwt1.required_claims.token_use', 'id' + setting 'xpack.security.authc.realms.jwt.jwt1.required_claims.version', '2.0' setting 'xpack.security.authc.realms.jwt.jwt1.client_authentication.type', 'NONE' // Use default value (RS256) for signature algorithm setting 'xpack.security.authc.realms.jwt.jwt1.pkc_jwkset_path', 'rsa.jwkset' @@ -84,12 +86,13 @@ testClusters.matching { it.name == 'javaRestTest' }.configureEach { setting 'xpack.security.authc.realms.jwt.jwt2.claims.principal', 'sub' } setting 'xpack.security.authc.realms.jwt.jwt2.claim_patterns.principal', '^(.*)@[^.]*[.]example[.]com$' + setting 'xpack.security.authc.realms.jwt.jwt2.required_claims.token_use', 'access' setting 'xpack.security.authc.realms.jwt.jwt2.authorization_realms', 'lookup_native' setting 'xpack.security.authc.realms.jwt.jwt2.client_authentication.type', 'shared_secret' keystore 'xpack.security.authc.realms.jwt.jwt2.client_authentication.shared_secret', 'test-secret' keystore 'xpack.security.authc.realms.jwt.jwt2.hmac_key', 'test-HMAC/secret passphrase-value' - // Place PKI realm after JWT realm to verify realm chain fall-throug + // Place PKI realm after JWT realm to verify realm chain fall-through setting 'xpack.security.authc.realms.pki.pki_realm.order', '4' setting 'xpack.security.authc.realms.jwt.jwt3.order', '5' diff --git a/x-pack/plugin/security/qa/jwt-realm/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/jwt/JwtRestIT.java b/x-pack/plugin/security/qa/jwt-realm/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/jwt/JwtRestIT.java index a62741620a4d..a51557a4e785 100644 --- a/x-pack/plugin/security/qa/jwt-realm/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/jwt/JwtRestIT.java +++ b/x-pack/plugin/security/qa/jwt-realm/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/jwt/JwtRestIT.java @@ -338,6 +338,28 @@ public void testFailureOnInvalidHMACSignature() throws Exception { } + public void testFailureOnRequiredClaims() throws JOSEException, IOException { + final String principal = System.getProperty("jwt2.service_subject"); + final String username = getUsernameFromPrincipal(principal); + final List roles = randomRoles(); + createUser(username, roles, Map.of()); + try { + final String audience = "es0" + randomIntBetween(1, 3); + final Map data = new HashMap<>(Map.of("iss", "my-issuer", "aud", audience, "email", principal)); + // The required claim is either missing or mismatching + if (randomBoolean()) { + data.put("token_use", randomValueOtherThan("access", () -> randomAlphaOfLengthBetween(3, 10))); + } + final JWTClaimsSet claimsSet = buildJwt(data, Instant.now(), false); + final SignedJWT jwt = signHmacJwt(claimsSet, "test-HMAC/secret passphrase-value"); + final TestSecurityClient client = getSecurityClient(jwt, VALID_SHARED_SECRET); + final ResponseException exception = expectThrows(ResponseException.class, client::authenticate); + assertThat(exception.getResponse(), hasStatusCode(RestStatus.UNAUTHORIZED)); + } finally { + deleteUser(username); + } + } + public void testAuthenticationFailureIfDelegatedAuthorizationFails() throws Exception { final String principal = System.getProperty("jwt2.service_subject"); final String username = getUsernameFromPrincipal(principal); @@ -486,7 +508,9 @@ private SignedJWT buildAndSignJwtForRealm1( Map.entry("dn", dn), Map.entry("name", name), Map.entry("mail", mail), - Map.entry("roles", groups) // Realm realm config has `claim.groups: "roles"` + Map.entry("roles", groups), // Realm realm config has `claim.groups: "roles"` + Map.entry("token_use", "id"), + Map.entry("version", "2.0") ), issueTime ); @@ -505,7 +529,9 @@ private SignedJWT buildAndSignJwtForRealm2(String principal, Instant issueTime) private JWTClaimsSet buildJwtForRealm2(String principal, Instant issueTime) { // The "jwt2" realm, supports 3 audiences (es01/02/03) final String audience = "es0" + randomIntBetween(1, 3); - final Map data = new HashMap<>(Map.of("iss", "my-issuer", "aud", audience, "email", principal)); + final Map data = new HashMap<>( + Map.of("iss", "my-issuer", "aud", audience, "email", principal, "token_use", "access") + ); // scope (fallback audience) is ignored since aud exists if (randomBoolean()) { data.put("scope", randomAlphaOfLength(20)); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticator.java index 3f2ff4a39543..d086248d7b53 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticator.java @@ -15,6 +15,7 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.Releasable; import org.elasticsearch.core.TimeValue; import org.elasticsearch.xpack.core.security.authc.RealmConfig; @@ -23,6 +24,7 @@ import java.text.ParseException; import java.time.Clock; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -47,16 +49,19 @@ public JwtAuthenticator( ) { this.realmConfig = realmConfig; this.tokenType = realmConfig.getSetting(JwtRealmSettings.TOKEN_TYPE); + final List jwtFieldValidators = new ArrayList<>(); if (tokenType == JwtRealmSettings.TokenType.ID_TOKEN) { this.fallbackClaimNames = Map.of(); - this.jwtFieldValidators = configureFieldValidatorsForIdToken(realmConfig); + jwtFieldValidators.addAll(configureFieldValidatorsForIdToken(realmConfig)); } else { this.fallbackClaimNames = Map.ofEntries( Map.entry("sub", realmConfig.getSetting(JwtRealmSettings.FALLBACK_SUB_CLAIM)), Map.entry("aud", realmConfig.getSetting(JwtRealmSettings.FALLBACK_AUD_CLAIM)) ); - this.jwtFieldValidators = configureFieldValidatorsForAccessToken(realmConfig, fallbackClaimNames); + jwtFieldValidators.addAll(configureFieldValidatorsForAccessToken(realmConfig, fallbackClaimNames)); } + jwtFieldValidators.addAll(getRequireClaimsValidators()); + this.jwtFieldValidators = List.copyOf(jwtFieldValidators); this.jwtSignatureValidator = new JwtSignatureValidator.DelegatingJwtSignatureValidator(realmConfig, sslService, reloadNotifier); } @@ -104,12 +109,17 @@ public void authenticate(JwtAuthenticationToken jwtAuthenticationToken, ActionLi } try { - jwtSignatureValidator.validate(tokenPrincipal, signedJWT, listener.map(ignored -> jwtClaimsSet)); + validateSignature(tokenPrincipal, signedJWT, listener.map(ignored -> jwtClaimsSet)); } catch (Exception e) { listener.onFailure(e); } } + // Package private for testing + void validateSignature(String tokenPrincipal, SignedJWT signedJWT, ActionListener listener) { + jwtSignatureValidator.validate(tokenPrincipal, signedJWT, listener); + } + @Override public void close() { jwtSignatureValidator.close(); @@ -172,6 +182,13 @@ private static List configureFieldValidatorsForAccessToken( new JwtDateClaimValidator(clock, "iat", allowedClockSkew, JwtDateClaimValidator.Relationship.BEFORE_NOW, false), new JwtDateClaimValidator(clock, "exp", allowedClockSkew, JwtDateClaimValidator.Relationship.AFTER_NOW, false) ); + } + private List getRequireClaimsValidators() { + final Settings requiredClaims = realmConfig.getSetting(JwtRealmSettings.REQUIRED_CLAIMS); + return requiredClaims.names().stream().map(name -> { + final List allowedValues = requiredClaims.getAsList(name); + return new JwtStringClaimValidator(name, allowedValues, false); + }).toList(); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorAccessTokenTypeTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorAccessTokenTypeTests.java index 23dd9d52f933..40bf021a48a9 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorAccessTokenTypeTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorAccessTokenTypeTests.java @@ -9,7 +9,6 @@ import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings; -import org.junit.Before; import java.text.ParseException; @@ -17,23 +16,13 @@ public class JwtAuthenticatorAccessTokenTypeTests extends JwtAuthenticatorTests { - private String fallbackSub; - private String fallbackAud; - - @Before - public void beforeTest() { - doBeforeTest(); - fallbackSub = randomBoolean() ? "_" + randomAlphaOfLength(5) : null; - fallbackAud = randomBoolean() ? "_" + randomAlphaOfLength(8) : null; - } - @Override protected JwtRealmSettings.TokenType getTokenType() { return JwtRealmSettings.TokenType.ACCESS_TOKEN; } public void testSubjectIsRequired() throws ParseException { - final IllegalArgumentException e = doTestSubjectIsRequired(buildJwtAuthenticator(fallbackSub, fallbackAud)); + final IllegalArgumentException e = doTestSubjectIsRequired(buildJwtAuthenticator()); if (fallbackSub != null) { assertThat(e.getMessage(), containsString("missing required string claim [" + fallbackSub + " (fallback of sub)]")); } @@ -41,10 +30,7 @@ public void testSubjectIsRequired() throws ParseException { public void testAccessTokenTypeMandatesAllowedSubjects() { allowedSubject = null; - final IllegalArgumentException e = expectThrows( - IllegalArgumentException.class, - () -> buildJwtAuthenticator(fallbackSub, fallbackAud) - ); + final IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> buildJwtAuthenticator()); assertThat( e.getMessage(), @@ -53,6 +39,6 @@ public void testAccessTokenTypeMandatesAllowedSubjects() { } public void testInvalidIssuerIsCheckedBeforeAlgorithm() throws ParseException { - doTestInvalidIssuerIsCheckedBeforeAlgorithm(buildJwtAuthenticator(fallbackSub, fallbackAud)); + doTestInvalidIssuerIsCheckedBeforeAlgorithm(buildJwtAuthenticator()); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorIdTokenTypeTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorIdTokenTypeTests.java index ad7e50109d56..c4e4cd6e6de6 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorIdTokenTypeTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorIdTokenTypeTests.java @@ -8,7 +8,6 @@ package org.elasticsearch.xpack.security.authc.jwt; import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings; -import org.junit.Before; import java.text.ParseException; @@ -16,27 +15,17 @@ public class JwtAuthenticatorIdTokenTypeTests extends JwtAuthenticatorTests { - private String fallbackSub; - private String fallbackAud; - - @Before - public void beforeTest() { - doBeforeTest(); - fallbackSub = null; - fallbackAud = null; - } - @Override protected JwtRealmSettings.TokenType getTokenType() { return JwtRealmSettings.TokenType.ID_TOKEN; } public void testSubjectIsRequired() throws ParseException { - final IllegalArgumentException e = doTestSubjectIsRequired(buildJwtAuthenticator(fallbackSub, fallbackAud)); + final IllegalArgumentException e = doTestSubjectIsRequired(buildJwtAuthenticator()); assertThat(e.getMessage(), containsString("missing required string claim [sub]")); } public void testInvalidIssuerIsCheckedBeforeAlgorithm() throws ParseException { - doTestInvalidIssuerIsCheckedBeforeAlgorithm(buildJwtAuthenticator(fallbackSub, fallbackAud)); + doTestInvalidIssuerIsCheckedBeforeAlgorithm(buildJwtAuthenticator()); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorTests.java index bd92534c05fc..6960eaa927ed 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorTests.java @@ -12,23 +12,36 @@ import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; +import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.settings.MockSecureSettings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Tuple; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings; +import org.junit.Before; import java.text.ParseException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import static org.elasticsearch.test.ActionListenerUtils.anyActionListener; import static org.elasticsearch.test.TestMatchers.throwableWithMessage; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; public abstract class JwtAuthenticatorTests extends ESTestCase { @@ -39,19 +52,149 @@ public abstract class JwtAuthenticatorTests extends ESTestCase { @Nullable protected String allowedSubject; protected String allowedAudience; + protected String fallbackSub; + protected String fallbackAud; + protected Tuple> requiredClaim; protected abstract JwtRealmSettings.TokenType getTokenType(); - protected void doBeforeTest() { + @Before + public void beforeTest() { realmName = randomAlphaOfLengthBetween(3, 8); allowedIssuer = randomAlphaOfLength(6); allowedAlgorithm = randomFrom(JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS_HMAC); if (getTokenType() == JwtRealmSettings.TokenType.ID_TOKEN) { allowedSubject = randomBoolean() ? randomAlphaOfLength(8) : null; + fallbackSub = null; + fallbackAud = null; } else { allowedSubject = randomAlphaOfLength(8); + fallbackSub = randomBoolean() ? "_" + randomAlphaOfLength(5) : null; + fallbackAud = randomBoolean() ? "_" + randomAlphaOfLength(8) : null; } allowedAudience = randomAlphaOfLength(10); + requiredClaim = Tuple.tuple(randomAlphaOfLength(8), randomList(1, 3, () -> randomAlphaOfLengthBetween(8, 18))); + } + + public void testRequiredClaims() throws ParseException { + final Instant now = Instant.now(); + final String requireClaimValue = randomFrom(requiredClaim.v2()); + final JWTClaimsSet claimsSet = JWTClaimsSet.parse( + Map.of( + "iss", + allowedIssuer, + "sub", + allowedSubject == null ? randomAlphaOfLengthBetween(10, 18) : allowedSubject, + "aud", + allowedAudience, + requiredClaim.v1(), + randomBoolean() ? requireClaimValue : List.of(requireClaimValue, "some-other-value"), + "iat", + now.minus(1, ChronoUnit.DAYS).getEpochSecond(), + "exp", + now.plus(1, ChronoUnit.DAYS).getEpochSecond() + ) + ); + final SignedJWT signedJWT = new SignedJWT( + JWSHeader.parse(Map.of("alg", allowedAlgorithm)).toBase64URL(), + claimsSet.toPayload().toBase64URL(), + Base64URL.encode("signature") + ); + + final JwtAuthenticationToken jwtAuthenticationToken = mock(JwtAuthenticationToken.class); + when(jwtAuthenticationToken.getEndUserSignedJwt()).thenReturn(new SecureString(signedJWT.serialize().toCharArray())); + + final PlainActionFuture future = new PlainActionFuture<>(); + final JwtAuthenticator jwtAuthenticator = buildJwtAuthenticator(); + jwtAuthenticator.authenticate(jwtAuthenticationToken, future); + assertThat(future.actionGet(), equalTo(claimsSet)); + } + + public void testMismatchedRequiredClaims() throws ParseException { + final Instant now = Instant.now(); + final String mismatchRequiredClaimValue = randomValueOtherThanMany( + requiredClaim.v2()::contains, + () -> randomAlphaOfLengthBetween(3, 18) + ); + final JWTClaimsSet claimsSet = JWTClaimsSet.parse( + Map.of( + "iss", + allowedIssuer, + "sub", + allowedSubject == null ? randomAlphaOfLengthBetween(10, 18) : allowedSubject, + "aud", + allowedAudience, + requiredClaim.v1(), + mismatchRequiredClaimValue, + "iat", + now.minus(1, ChronoUnit.DAYS).getEpochSecond(), + "exp", + now.plus(1, ChronoUnit.DAYS).getEpochSecond() + ) + ); + final SignedJWT signedJWT = new SignedJWT( + JWSHeader.parse(Map.of("alg", allowedAlgorithm)).toBase64URL(), + claimsSet.toPayload().toBase64URL(), + Base64URL.encode("signature") + ); + + final JwtAuthenticationToken jwtAuthenticationToken = mock(JwtAuthenticationToken.class); + when(jwtAuthenticationToken.getEndUserSignedJwt()).thenReturn(new SecureString(signedJWT.serialize().toCharArray())); + + final PlainActionFuture future = new PlainActionFuture<>(); + final JwtAuthenticator jwtAuthenticator = buildJwtAuthenticator(); + jwtAuthenticator.authenticate(jwtAuthenticationToken, future); + final IllegalArgumentException e = expectThrows(IllegalArgumentException.class, future::actionGet); + assertThat( + e.getMessage(), + containsString( + "string claim [" + + requiredClaim.v1() + + "] has value [" + + mismatchRequiredClaimValue + + "] which does not match allowed claim values [" + + requiredClaim.v2().stream().collect(Collectors.joining(",")) + + "]" + ) + ); + } + + public void testMissingRequiredClaims() throws ParseException { + final Instant now = Instant.now(); + final JWTClaimsSet claimsSet = JWTClaimsSet.parse( + Map.of( + "iss", + allowedIssuer, + "sub", + allowedSubject == null ? randomAlphaOfLengthBetween(10, 18) : allowedSubject, + "aud", + allowedAudience, + "iat", + now.minus(1, ChronoUnit.DAYS).getEpochSecond(), + "exp", + now.plus(1, ChronoUnit.DAYS).getEpochSecond() + ) + ); + final SignedJWT signedJWT = new SignedJWT( + JWSHeader.parse(Map.of("alg", allowedAlgorithm)).toBase64URL(), + claimsSet.toPayload().toBase64URL(), + Base64URL.encode("signature") + ); + + final JwtAuthenticationToken jwtAuthenticationToken = mock(JwtAuthenticationToken.class); + when(jwtAuthenticationToken.getEndUserSignedJwt()).thenReturn(new SecureString(signedJWT.serialize().toCharArray())); + + // Required claim is mandatory when configured + final PlainActionFuture future1 = new PlainActionFuture<>(); + buildJwtAuthenticator().authenticate(jwtAuthenticationToken, future1); + final IllegalArgumentException e = expectThrows(IllegalArgumentException.class, future1::actionGet); + assertThat(e.getMessage(), containsString("missing required string claim [" + requiredClaim.v1() + "]")); + + // Remove required claim from settings, the JWT now works + requiredClaim = null; + final PlainActionFuture future2 = new PlainActionFuture<>(); + buildJwtAuthenticator().authenticate(jwtAuthenticationToken, future2); + assertThat(future2.actionGet(), equalTo(claimsSet)); } protected IllegalArgumentException doTestSubjectIsRequired(JwtAuthenticator jwtAuthenticator) throws ParseException { @@ -92,14 +235,14 @@ protected void doTestInvalidIssuerIsCheckedBeforeAlgorithm(JwtAuthenticator jwtA ); } - protected JwtAuthenticator buildJwtAuthenticator(String fallbackSub, String fallbackAud) { + protected JwtAuthenticator buildJwtAuthenticator() { final RealmConfig.RealmIdentifier realmIdentifier = new RealmConfig.RealmIdentifier(JwtRealmSettings.TYPE, realmName); final MockSecureSettings secureSettings = new MockSecureSettings(); secureSettings.setString(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.HMAC_KEY), randomAlphaOfLength(40)); final Settings.Builder builder = Settings.builder() .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.ALLOWED_SIGNATURE_ALGORITHMS), allowedAlgorithm) .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.ALLOWED_ISSUER), allowedIssuer) - .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.ALLOWED_AUDIENCES), randomAlphaOfLength(7)) + .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.ALLOWED_AUDIENCES), allowedAudience) .put(RealmSettings.getFullSettingKey(realmIdentifier, RealmSettings.ORDER_SETTING), randomIntBetween(0, 99)) .put("path.home", randomAlphaOfLength(10)) .setSecureSettings(secureSettings); @@ -123,6 +266,16 @@ protected JwtAuthenticator buildJwtAuthenticator(String fallbackSub, String fall builder.put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.FALLBACK_AUD_CLAIM), fallbackAud); } + if (requiredClaim != null) { + final String requiredClaimsKey = RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.REQUIRED_CLAIMS) + requiredClaim + .v1(); + if (requiredClaim.v2().size() == 1 && randomBoolean()) { + builder.put(requiredClaimsKey, requiredClaim.v2().get(0)); + } else { + builder.putList(requiredClaimsKey, requiredClaim.v2()); + } + } + final Settings settings = builder.build(); final RealmConfig realmConfig = new RealmConfig( @@ -132,6 +285,14 @@ protected JwtAuthenticator buildJwtAuthenticator(String fallbackSub, String fall new ThreadContext(settings) ); - return new JwtAuthenticator(realmConfig, null, () -> {}); + final JwtAuthenticator jwtAuthenticator = spy(new JwtAuthenticator(realmConfig, null, () -> {})); + // Short circuit signature validation to be always successful since this test class does not test it + doAnswer(invocation -> { + final ActionListener listener = invocation.getArgument(2); + listener.onResponse(null); + return null; + }).when(jwtAuthenticator).validateSignature(any(), any(), anyActionListener()); + + return jwtAuthenticator; } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmSettingsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmSettingsTests.java index 4a32ba78ef6b..453c1945ddf4 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmSettingsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmSettingsTests.java @@ -23,7 +23,9 @@ import java.util.Locale; import static org.elasticsearch.common.Strings.capitalize; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.emptyIterable; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -535,4 +537,55 @@ public void testRegisteredClaimsCannotBeUsedForFallbackSettings() { ); } + public void testRequiredClaims() { + final String realmName = randomAlphaOfLengthBetween(3, 8); + + // Required claims are optional + final RealmConfig realmConfig1 = buildRealmConfig(JwtRealmSettings.TYPE, realmName, Settings.EMPTY, randomInt()); + assertThat(realmConfig1.getSetting(JwtRealmSettings.REQUIRED_CLAIMS).names(), emptyIterable()); + + // Multiple required claims with different value types + final String prefix = RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.REQUIRED_CLAIMS); + final Settings settings = Settings.builder() + .put(prefix + "extra_1", "foo") + .put(prefix + "extra_2", "hello,world") + .put(prefix + "extra_3", 42) + .build(); + final RealmConfig realmConfig2 = buildRealmConfig(JwtRealmSettings.TYPE, realmName, settings, randomInt()); + final Settings requireClaimsSettings = realmConfig2.getSetting(JwtRealmSettings.REQUIRED_CLAIMS); + assertThat(requireClaimsSettings.names(), containsInAnyOrder("extra_1", "extra_2", "extra_3")); + assertThat(requireClaimsSettings.getAsList("extra_1"), equalTo(List.of("foo"))); + assertThat(requireClaimsSettings.getAsList("extra_2"), equalTo(List.of("hello", "world"))); + assertThat(requireClaimsSettings.getAsList("extra_3"), equalTo(List.of("42"))); + } + + public void testInvalidRequiredClaims() { + final String realmName = randomAlphaOfLengthBetween(3, 8); + final String invalidRequiredClaim = randomFrom("iss", "sub", "aud", "exp", "nbf", "iat"); + final String fullSettingKey = RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.REQUIRED_CLAIMS) + invalidRequiredClaim; + final Settings settings = Settings.builder().put(fullSettingKey, randomAlphaOfLength(8)).build(); + + final RealmConfig realmConfig = buildRealmConfig(JwtRealmSettings.TYPE, realmName, settings, randomInt()); + final IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, + () -> realmConfig.getSetting(JwtRealmSettings.REQUIRED_CLAIMS) + ); + + assertThat(e.getMessage(), containsString("required claim [" + fullSettingKey + "] cannot be one of [iss,sub,aud,exp,nbf,iat]")); + } + + public void testRequiredClaimsCannotBeEmpty() { + final String realmName = randomAlphaOfLengthBetween(3, 8); + final String invalidRequiredClaim = randomAlphaOfLengthBetween(4, 8); + final String fullSettingKey = RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.REQUIRED_CLAIMS) + invalidRequiredClaim; + final Settings settings = Settings.builder().put(fullSettingKey, "").build(); + + final RealmConfig realmConfig = buildRealmConfig(JwtRealmSettings.TYPE, realmName, settings, randomInt()); + final IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, + () -> realmConfig.getSetting(JwtRealmSettings.REQUIRED_CLAIMS) + ); + + assertThat(e.getMessage(), containsString("required claim [" + fullSettingKey + "] cannot be empty")); + } } From 31ac6b0cc85ec54443d8d0f8bff7838939abf9ea Mon Sep 17 00:00:00 2001 From: Rory Hunter Date: Thu, 15 Dec 2022 09:23:19 +0000 Subject: [PATCH 277/919] Make redaction configurable for APM tracing (#92358) Closes #92338. When tracing REST requests with APM, we capture HTTP headers as labels on the trace, but redact sensitive values. However, we can't know ahead of time what are all possible sensitive values. Push this redaction into the tracer, and make the redaction terms configurable. Switch the defaults to the APM Java agent's defaults. --- .../org/elasticsearch/tracing/apm/APM.java | 1 + .../tracing/apm/APMAgentSettings.java | 22 +++++++++++ .../elasticsearch/tracing/apm/APMTracer.java | 28 +++++++++++-- .../tracing/apm/APMTracerTests.java | 39 +++++++++++++++++++ .../elasticsearch/rest/RestController.java | 8 +--- 5 files changed, 89 insertions(+), 9 deletions(-) diff --git a/modules/apm/src/main/java/org/elasticsearch/tracing/apm/APM.java b/modules/apm/src/main/java/org/elasticsearch/tracing/apm/APM.java index 9f10612e116c..ed34bcd0327a 100644 --- a/modules/apm/src/main/java/org/elasticsearch/tracing/apm/APM.java +++ b/modules/apm/src/main/java/org/elasticsearch/tracing/apm/APM.java @@ -101,6 +101,7 @@ public List> getSettings() { APMAgentSettings.APM_ENABLED_SETTING, APMAgentSettings.APM_TRACING_NAMES_INCLUDE_SETTING, APMAgentSettings.APM_TRACING_NAMES_EXCLUDE_SETTING, + APMAgentSettings.APM_TRACING_SANITIZE_FIELD_NAMES, APMAgentSettings.APM_AGENT_SETTINGS, APMAgentSettings.APM_SECRET_TOKEN_SETTING, APMAgentSettings.APM_API_KEY_SETTING diff --git a/modules/apm/src/main/java/org/elasticsearch/tracing/apm/APMAgentSettings.java b/modules/apm/src/main/java/org/elasticsearch/tracing/apm/APMAgentSettings.java index 140d48027a59..1b432c9d3ad3 100644 --- a/modules/apm/src/main/java/org/elasticsearch/tracing/apm/APMAgentSettings.java +++ b/modules/apm/src/main/java/org/elasticsearch/tracing/apm/APMAgentSettings.java @@ -57,6 +57,7 @@ void addClusterSettingsListeners(ClusterService clusterService, APMTracer apmTra }); clusterSettings.addSettingsUpdateConsumer(APM_TRACING_NAMES_INCLUDE_SETTING, apmTracer::setIncludeNames); clusterSettings.addSettingsUpdateConsumer(APM_TRACING_NAMES_EXCLUDE_SETTING, apmTracer::setExcludeNames); + clusterSettings.addSettingsUpdateConsumer(APM_TRACING_SANITIZE_FIELD_NAMES, apmTracer::setLabelFilters); clusterSettings.addAffixMapUpdateConsumer(APM_AGENT_SETTINGS, map -> map.forEach(this::setAgentSetting), (x, y) -> {}); } @@ -143,6 +144,27 @@ void setAgentSetting(String key, String value) { NodeScope ); + static final Setting> APM_TRACING_SANITIZE_FIELD_NAMES = Setting.listSetting( + APM_SETTING_PREFIX + "sanitize_field_names", + List.of( + "password", + "passwd", + "pwd", + "secret", + "*key", + "*token*", + "*session*", + "*credit*", + "*card*", + "*auth*", + "*principal*", + "set-cookie" + ), + Function.identity(), + OperatorDynamic, + NodeScope + ); + static final Setting APM_ENABLED_SETTING = Setting.boolSetting( APM_SETTING_PREFIX + "enabled", false, diff --git a/modules/apm/src/main/java/org/elasticsearch/tracing/apm/APMTracer.java b/modules/apm/src/main/java/org/elasticsearch/tracing/apm/APMTracer.java index fad42b2768c3..5e8375e067fe 100644 --- a/modules/apm/src/main/java/org/elasticsearch/tracing/apm/APMTracer.java +++ b/modules/apm/src/main/java/org/elasticsearch/tracing/apm/APMTracer.java @@ -44,6 +44,7 @@ import static org.elasticsearch.tracing.apm.APMAgentSettings.APM_ENABLED_SETTING; import static org.elasticsearch.tracing.apm.APMAgentSettings.APM_TRACING_NAMES_EXCLUDE_SETTING; import static org.elasticsearch.tracing.apm.APMAgentSettings.APM_TRACING_NAMES_INCLUDE_SETTING; +import static org.elasticsearch.tracing.apm.APMAgentSettings.APM_TRACING_SANITIZE_FIELD_NAMES; /** * This is an implementation of the {@link org.elasticsearch.tracing.Tracer} interface, which uses @@ -65,8 +66,10 @@ public class APMTracer extends AbstractLifecycleComponent implements org.elastic private List includeNames; private List excludeNames; + private List labelFilters; /** Built using {@link #includeNames} and {@link #excludeNames}, and filters out spans based on their name. */ private volatile CharacterRunAutomaton filterAutomaton; + private volatile CharacterRunAutomaton labelFilterAutomaton; private String clusterName; private String nodeName; @@ -86,7 +89,10 @@ record APMServices(Tracer tracer, OpenTelemetry openTelemetry) {} public APMTracer(Settings settings) { this.includeNames = APM_TRACING_NAMES_INCLUDE_SETTING.get(settings); this.excludeNames = APM_TRACING_NAMES_EXCLUDE_SETTING.get(settings); + this.labelFilters = APM_TRACING_SANITIZE_FIELD_NAMES.get(settings); + this.filterAutomaton = buildAutomaton(includeNames, excludeNames); + this.labelFilterAutomaton = buildAutomaton(labelFilters, List.of()); this.enabled = APM_ENABLED_SETTING.get(settings); } @@ -109,6 +115,16 @@ void setExcludeNames(List excludeNames) { this.filterAutomaton = buildAutomaton(includeNames, excludeNames); } + void setLabelFilters(List labelFilters) { + this.labelFilters = labelFilters; + this.labelFilterAutomaton = buildAutomaton(labelFilters, List.of()); + } + + // package-private for testing + CharacterRunAutomaton getLabelFilterAutomaton() { + return labelFilterAutomaton; + } + @Override protected void doStart() { if (enabled) { @@ -271,6 +287,12 @@ private void setSpanAttributes(@Nullable Map spanAttributes, Spa for (Map.Entry entry : spanAttributes.entrySet()) { final String key = entry.getKey(); final Object value = entry.getValue(); + + if (this.labelFilterAutomaton.run(key)) { + spanBuilder.setAttribute(key, "[REDACTED]"); + continue; + } + if (value instanceof String) { spanBuilder.setAttribute(key, (String) value); } else if (value instanceof Long) { @@ -394,9 +416,9 @@ Map getSpans() { return spans; } - private static CharacterRunAutomaton buildAutomaton(List includeNames, List excludeNames) { - Automaton includeAutomaton = patternsToAutomaton(includeNames); - Automaton excludeAutomaton = patternsToAutomaton(excludeNames); + private static CharacterRunAutomaton buildAutomaton(List includePatterns, List excludePatterns) { + Automaton includeAutomaton = patternsToAutomaton(includePatterns); + Automaton excludeAutomaton = patternsToAutomaton(excludePatterns); if (includeAutomaton == null) { includeAutomaton = Automata.makeAnyString(); diff --git a/modules/apm/src/test/java/org/elasticsearch/tracing/apm/APMTracerTests.java b/modules/apm/src/test/java/org/elasticsearch/tracing/apm/APMTracerTests.java index f4ab36d43aa4..9c6582fcc118 100644 --- a/modules/apm/src/test/java/org/elasticsearch/tracing/apm/APMTracerTests.java +++ b/modules/apm/src/test/java/org/elasticsearch/tracing/apm/APMTracerTests.java @@ -8,12 +8,14 @@ package org.elasticsearch.tracing.apm; +import org.apache.lucene.util.automaton.CharacterRunAutomaton; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.tasks.Task; import org.elasticsearch.test.ESTestCase; import java.util.List; +import java.util.stream.Stream; import static org.elasticsearch.tracing.apm.APMAgentSettings.APM_ENABLED_SETTING; import static org.elasticsearch.tracing.apm.APMAgentSettings.APM_TRACING_NAMES_EXCLUDE_SETTING; @@ -166,6 +168,43 @@ public void test_whenTraceStarted_andSpanNameExcluded_thenSpanIsNotStarted() { assertThat(apmTracer.getSpans(), hasKey("id3")); } + /** + * Check that sensitive attributes are not added verbatim to a span, but instead the value is redacted. + */ + public void test_whenAddingAttributes_thenSensitiveValuesAreRedacted() { + Settings settings = Settings.builder().put(APM_ENABLED_SETTING.getKey(), false).build(); + APMTracer apmTracer = buildTracer(settings); + CharacterRunAutomaton labelFilterAutomaton = apmTracer.getLabelFilterAutomaton(); + + Stream.of( + "auth", + "auth-header", + "authValue", + "card", + "card-details", + "card-number", + "credit", + "credit-card", + "key", + "my-credit-number", + "my_session_id", + "passwd", + "password", + "principal", + "principal-value", + "pwd", + "secret", + "secure-key", + "sensitive-token*", + "session", + "session_id", + "set-cookie", + "some-auth", + "some-principal", + "token-for login" + ).forEach(key -> assertTrue("Expected label filter automaton to redact [" + key + "]", labelFilterAutomaton.run(key))); + } + private APMTracer buildTracer(Settings settings) { APMTracer tracer = new APMTracer(settings); tracer.doStart(); diff --git a/server/src/main/java/org/elasticsearch/rest/RestController.java b/server/src/main/java/org/elasticsearch/rest/RestController.java index f1dbb5489c61..c2ab723a15df 100644 --- a/server/src/main/java/org/elasticsearch/rest/RestController.java +++ b/server/src/main/java/org/elasticsearch/rest/RestController.java @@ -461,14 +461,10 @@ private void startTrace(ThreadContext threadContext, RestChannel channel, String name = restPath; } - Map attributes = Maps.newMapWithExpectedSize(req.getHeaders().size() + 3); + final Map attributes = Maps.newMapWithExpectedSize(req.getHeaders().size() + 3); req.getHeaders().forEach((key, values) -> { final String lowerKey = key.toLowerCase(Locale.ROOT).replace('-', '_'); - final String value = switch (lowerKey) { - case "authorization", "cookie", "secret", "session", "set_cookie", "token", "x_elastic_app_auth" -> "[REDACTED]"; - default -> String.join("; ", values); - }; - attributes.put("http.request.headers." + lowerKey, value); + attributes.put("http.request.headers." + lowerKey, values.size() == 1 ? values.get(0) : String.join("; ", values)); }); attributes.put("http.method", method); attributes.put("http.url", req.uri()); From 1e1ba68ed86c00dafda074a74b7aa55f7dbda058 Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Thu, 15 Dec 2022 10:23:58 +0100 Subject: [PATCH 278/919] Adjust azimuth test delta (#92386) --- libs/h3/src/test/java/org/elasticsearch/h3/AzimuthTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/h3/src/test/java/org/elasticsearch/h3/AzimuthTests.java b/libs/h3/src/test/java/org/elasticsearch/h3/AzimuthTests.java index 99b526452c1b..69097e9ad7fe 100644 --- a/libs/h3/src/test/java/org/elasticsearch/h3/AzimuthTests.java +++ b/libs/h3/src/test/java/org/elasticsearch/h3/AzimuthTests.java @@ -30,7 +30,7 @@ public void testLatLonVec3d() { for (int i = 0; i < Vec3d.faceCenterPoint.length; i++) { final double azVec3d = Vec3d.faceCenterPoint[i].geoAzimuthRads(point.x, point.y, point.z); final double azVec2d = Vec2d.faceCenterGeo[i].geoAzimuthRads(point.getLatitude(), point.getLongitude()); - assertEquals("Face " + i, azVec2d, azVec3d, 1e-14); + assertEquals("Face " + i, azVec2d, azVec3d, 1e-12); } } From d7cc19c667ebcb815a6b59197895f3006ef75c7c Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Thu, 15 Dec 2022 11:00:21 +0100 Subject: [PATCH 279/919] Make the visitor of the spatial triangle tree a public interface (#92349) --- .../fielddata/Component2DRelationVisitor.java | 8 +- .../index/fielddata/Component2DVisitor.java | 58 +++---- .../index/fielddata/GeoShapeValues.java | 2 +- .../fielddata/GeometryDocValueReader.java | 2 +- .../index/fielddata/LabelPositionVisitor.java | 24 ++- .../spatial/index/fielddata/ShapeValues.java | 13 +- .../index/fielddata/Tile2DVisitor.java | 2 +- .../index/fielddata/TriangleTreeReader.java | 141 +----------------- .../index/fielddata/TriangleTreeVisitor.java | 132 ++++++++++++++++ .../index/fielddata/TriangleTreeTests.java | 2 +- 10 files changed, 195 insertions(+), 189 deletions(-) create mode 100644 x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/TriangleTreeVisitor.java diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Component2DRelationVisitor.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Component2DRelationVisitor.java index 2402cb8ca644..c40aa582dc3a 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Component2DRelationVisitor.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Component2DRelationVisitor.java @@ -17,7 +17,7 @@ * This class supports checking {@link Component2D} relations against a serialized triangle tree. * It does not support bounding boxes crossing the dateline. */ -class Component2DRelationVisitor extends TriangleTreeReader.DecodedVisitor { +class Component2DRelationVisitor extends TriangleTreeVisitor.TriangleTreeDecodedVisitor { private GeoRelation relation; private Component2D component2D; @@ -39,7 +39,7 @@ public GeoRelation relation() { } @Override - void visitDecodedPoint(double x, double y) { + protected void visitDecodedPoint(double x, double y) { if (component2D.contains(x, y)) { if (canBeContained()) { relation = GeoRelation.QUERY_CONTAINS; @@ -54,7 +54,7 @@ void visitDecodedPoint(double x, double y) { } @Override - void visitDecodedLine(double aX, double aY, double bX, double bY, byte metadata) { + protected void visitDecodedLine(double aX, double aY, double bX, double bY, byte metadata) { if (component2D.intersectsLine(aX, aY, bX, bY)) { final boolean ab = (metadata & 1 << 4) == 1 << 4; if (canBeContained() && component2D.containsLine(aX, aY, bX, bY)) { @@ -70,7 +70,7 @@ void visitDecodedLine(double aX, double aY, double bX, double bY, byte metadata) } @Override - void visitDecodedTriangle(double aX, double aY, double bX, double bY, double cX, double cY, byte metadata) { + protected void visitDecodedTriangle(double aX, double aY, double bX, double bY, double cX, double cY, byte metadata) { if (component2D.intersectsTriangle(aX, aY, bX, bY, cX, cY)) { final boolean ab = (metadata & 1 << 4) == 1 << 4; final boolean bc = (metadata & 1 << 5) == 1 << 5; diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Component2DVisitor.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Component2DVisitor.java index d6881969da82..60faf40bbd93 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Component2DVisitor.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Component2DVisitor.java @@ -12,10 +12,10 @@ import org.apache.lucene.index.PointValues; /** - * A {@link TriangleTreeReader.Visitor} implementation for {@link Component2D} geometries. + * A {@link TriangleTreeVisitor.TriangleTreeDecodedVisitor} implementation for {@link Component2D} geometries. * It can solve spatial relationships against a serialize triangle tree. */ -public abstract class Component2DVisitor extends TriangleTreeReader.DecodedVisitor { +public abstract class Component2DVisitor extends TriangleTreeVisitor.TriangleTreeDecodedVisitor { protected final Component2D component2D; @@ -31,23 +31,23 @@ private Component2DVisitor(Component2D component2D, CoordinateEncoder encoder) { public abstract void reset(); @Override - public boolean pushDecodedX(double minX) { + protected boolean pushDecodedX(double minX) { return component2D.getMaxX() >= minX; } @Override - public boolean pushDecodedY(double minY) { + protected boolean pushDecodedY(double minY) { return component2D.getMaxY() >= minY; } @Override - public boolean pushDecoded(double maxX, double maxY) { + protected boolean pushDecoded(double maxX, double maxY) { return component2D.getMinX() <= maxX && component2D.getMinY() <= maxY; } @Override - public boolean pushDecoded(double minX, double minY, double maxX, double maxY) { + protected boolean pushDecoded(double minX, double minY, double maxX, double maxY) { return pushDecoded(component2D.relate(minX, maxX, minY, maxY)); } @@ -56,7 +56,7 @@ public boolean pushDecoded(double minX, double minY, double maxX, double maxY) { * * @return if true, the visitor keeps traversing the tree, else it stops. * */ - abstract boolean pushDecoded(PointValues.Relation relation); + protected abstract boolean pushDecoded(PointValues.Relation relation); /** * Creates a visitor from the provided Component2D and spatial relationship. Visitors are re-usable by @@ -95,17 +95,17 @@ public void reset() { } @Override - void visitDecodedPoint(double x, double y) { + protected void visitDecodedPoint(double x, double y) { intersects = component2D.contains(x, y); } @Override - void visitDecodedLine(double aX, double aY, double bX, double bY, byte metadata) { + protected void visitDecodedLine(double aX, double aY, double bX, double bY, byte metadata) { intersects = component2D.intersectsLine(aX, aY, bX, bY); } @Override - void visitDecodedTriangle(double aX, double aY, double bX, double bY, double cX, double cY, byte metadata) { + protected void visitDecodedTriangle(double aX, double aY, double bX, double bY, double cX, double cY, byte metadata) { intersects = component2D.intersectsTriangle(aX, aY, bX, bY, cX, cY); } @@ -116,7 +116,7 @@ public boolean push() { } @Override - boolean pushDecoded(PointValues.Relation relation) { + protected boolean pushDecoded(PointValues.Relation relation) { if (relation == PointValues.Relation.CELL_OUTSIDE_QUERY) { // shapes are disjoint, stop traversing the tree. return false; @@ -156,17 +156,17 @@ public void reset() { } @Override - void visitDecodedPoint(double x, double y) { + protected void visitDecodedPoint(double x, double y) { disjoint = component2D.contains(x, y) == false; } @Override - void visitDecodedLine(double aX, double aY, double bX, double bY, byte metadata) { + protected void visitDecodedLine(double aX, double aY, double bX, double bY, byte metadata) { disjoint = component2D.intersectsLine(aX, aY, bX, bY) == false; } @Override - void visitDecodedTriangle(double aX, double aY, double bX, double bY, double cX, double cY, byte metadata) { + protected void visitDecodedTriangle(double aX, double aY, double bX, double bY, double cX, double cY, byte metadata) { disjoint = component2D.intersectsTriangle(aX, aY, bX, bY, cX, cY) == false; } @@ -177,7 +177,7 @@ public boolean push() { } @Override - boolean pushDecoded(PointValues.Relation relation) { + protected boolean pushDecoded(PointValues.Relation relation) { if (relation == PointValues.Relation.CELL_OUTSIDE_QUERY) { // shapes are disjoint, stop traversing the tree. return false; @@ -217,17 +217,17 @@ public void reset() { } @Override - void visitDecodedPoint(double x, double y) { + protected void visitDecodedPoint(double x, double y) { within = component2D.contains(x, y); } @Override - void visitDecodedLine(double aX, double aY, double bX, double bY, byte metadata) { + protected void visitDecodedLine(double aX, double aY, double bX, double bY, byte metadata) { within = component2D.containsLine(aX, aY, bX, bY); } @Override - void visitDecodedTriangle(double aX, double aY, double bX, double bY, double cX, double cY, byte metadata) { + protected void visitDecodedTriangle(double aX, double aY, double bX, double bY, double cX, double cY, byte metadata) { within = component2D.containsTriangle(aX, aY, bX, bY, cX, cY); } @@ -238,31 +238,31 @@ public boolean push() { } @Override - public boolean pushX(int minX) { + protected boolean pushDecodedX(double minX) { // if any part of the tree is skipped, then the doc value is not within the shape, // stop traversing the tree - within = super.pushX(minX); + within = super.pushDecodedX(minX); return within; } @Override - public boolean pushY(int minY) { + protected boolean pushDecodedY(double minY) { // if any part of the tree is skipped, then the doc value is not within the shape, // stop traversing the tree - within = super.pushY(minY); + within = super.pushDecodedY(minY); return within; } @Override - public boolean push(int maxX, int maxY) { + protected boolean pushDecoded(double maxX, double maxY) { // if any part of the tree is skipped, then the doc value is not within the shape, // stop traversing the tree - within = super.push(maxX, maxY); + within = super.pushDecoded(maxX, maxY); return within; } @Override - boolean pushDecoded(PointValues.Relation relation) { + protected boolean pushDecoded(PointValues.Relation relation) { if (relation == PointValues.Relation.CELL_OUTSIDE_QUERY) { // shapes are disjoint, stop traversing the tree. within = false; @@ -297,7 +297,7 @@ public void reset() { } @Override - void visitDecodedPoint(double x, double y) { + protected void visitDecodedPoint(double x, double y) { final Component2D.WithinRelation rel = component2D.withinPoint(x, y); if (rel != Component2D.WithinRelation.DISJOINT) { // Only override relationship if different to DISJOINT @@ -306,7 +306,7 @@ void visitDecodedPoint(double x, double y) { } @Override - void visitDecodedLine(double aX, double aY, double bX, double bY, byte metadata) { + protected void visitDecodedLine(double aX, double aY, double bX, double bY, byte metadata) { final boolean ab = (metadata & 1 << 4) == 1 << 4; final Component2D.WithinRelation rel = component2D.withinLine(aX, aY, ab, bX, bY); if (rel != Component2D.WithinRelation.DISJOINT) { @@ -316,7 +316,7 @@ void visitDecodedLine(double aX, double aY, double bX, double bY, byte metadata) } @Override - void visitDecodedTriangle(double aX, double aY, double bX, double bY, double cX, double cY, byte metadata) { + protected void visitDecodedTriangle(double aX, double aY, double bX, double bY, double cX, double cY, byte metadata) { final boolean ab = (metadata & 1 << 4) == 1 << 4; final boolean bc = (metadata & 1 << 5) == 1 << 5; final boolean ca = (metadata & 1 << 6) == 1 << 6; @@ -334,7 +334,7 @@ public boolean push() { } @Override - boolean pushDecoded(PointValues.Relation relation) { + protected boolean pushDecoded(PointValues.Relation relation) { // Only traverse the tree if the shapes intersects. return relation == PointValues.Relation.CELL_CROSSES_QUERY; } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeoShapeValues.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeoShapeValues.java index fe0ec12e9157..b0bacf7d295f 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeoShapeValues.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeoShapeValues.java @@ -80,7 +80,7 @@ protected Component2D centroidAsComponent2D() throws IOException { */ public GeoRelation relate(int minX, int maxX, int minY, int maxY) throws IOException { tile2DVisitor.reset(minX, minY, maxX, maxY); - reader.visit(tile2DVisitor); + visit(tile2DVisitor); return tile2DVisitor.relation(); } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeometryDocValueReader.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeometryDocValueReader.java index 0b95d5ac2666..51ce9124eac5 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeometryDocValueReader.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeometryDocValueReader.java @@ -98,7 +98,7 @@ protected double getSumCentroidWeight() throws IOException { /** * Visit the triangle tree with the provided visitor */ - public void visit(TriangleTreeReader.Visitor visitor) throws IOException { + public void visit(TriangleTreeVisitor visitor) throws IOException { Extent geometryExtent = getExtent(); int thisMaxX = geometryExtent.maxX(); int thisMinX = geometryExtent.minX(); diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/LabelPositionVisitor.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/LabelPositionVisitor.java index 16939c183cf7..6007a9df569b 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/LabelPositionVisitor.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/LabelPositionVisitor.java @@ -17,41 +17,41 @@ * * TODO: We could instead choose the point closer to the centroid which improves unbalanced trees */ -public class LabelPositionVisitor extends TriangleTreeReader.DecodedVisitor { +class LabelPositionVisitor extends TriangleTreeVisitor.TriangleTreeDecodedVisitor { private SpatialPoint labelPosition; private final BiFunction pointMaker; - public LabelPositionVisitor(CoordinateEncoder encoder, BiFunction pointMaker) { + LabelPositionVisitor(CoordinateEncoder encoder, BiFunction pointMaker) { super(encoder); this.pointMaker = pointMaker; } @Override - void visitDecodedPoint(double x, double y) { + protected void visitDecodedPoint(double x, double y) { assert labelPosition == null; labelPosition = pointMaker.apply(x, y); } @Override - public void visitDecodedLine(double aX, double aY, double bX, double bY, byte metadata) { + protected void visitDecodedLine(double aX, double aY, double bX, double bY, byte metadata) { assert labelPosition == null; labelPosition = pointMaker.apply((aX + bX) / 2.0, (aY + bY) / 2.0); } @Override - public void visitDecodedTriangle(double aX, double aY, double bX, double bY, double cX, double cY, byte metadata) { + protected void visitDecodedTriangle(double aX, double aY, double bX, double bY, double cX, double cY, byte metadata) { assert labelPosition == null; labelPosition = pointMaker.apply((aX + bX + cX) / 3.0, (aY + bY + cY) / 3.0); } @Override - boolean pushDecodedX(double minX) { + protected boolean pushDecodedX(double minX) { return labelPosition == null; } @Override - boolean pushDecodedY(double minX) { + protected boolean pushDecodedY(double minX) { return labelPosition == null; } @@ -62,18 +62,12 @@ public boolean push() { } @Override - boolean pushDecoded(double maxX, double maxY) { + protected boolean pushDecoded(double maxX, double maxY) { return labelPosition == null; } @Override - public boolean push(int minX, int minY, int maxX, int maxY) { - // Always start the traversal - return labelPosition == null; - } - - @Override - boolean pushDecoded(double minX, double minY, double maxX, double maxY) { + protected boolean pushDecoded(double minX, double minY, double maxX, double maxY) { return labelPosition == null; } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/ShapeValues.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/ShapeValues.java index 93cac2971b47..03b9375b19b6 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/ShapeValues.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/ShapeValues.java @@ -91,9 +91,9 @@ public T missing(String missing) { * the provided decoder (could be geo or cartesian) */ protected abstract static class ShapeValue implements ToXContentFragment { - protected final GeometryDocValueReader reader; + private final GeometryDocValueReader reader; private final BoundingBox boundingBox; - protected final CoordinateEncoder encoder; + private final CoordinateEncoder encoder; private final BiFunction pointMaker; private final Component2DRelationVisitor component2DRelationVisitor; @@ -117,6 +117,13 @@ public BoundingBox boundingBox() { return boundingBox; } + /** + * Visit the underlying tree structure using the provided {@link TriangleTreeVisitor} + */ + public void visit(TriangleTreeVisitor visitor) throws IOException { + reader.visit(visitor); + } + protected abstract Component2D centroidAsComponent2D() throws IOException; private boolean centroidWithinShape() throws IOException { @@ -139,7 +146,7 @@ public SpatialPoint labelPosition() throws IOException { } // For all other cases, use the first triangle (or line or point) in the tree which will always intersect the shape LabelPositionVisitor visitor = new LabelPositionVisitor(this.encoder, pointMaker); - reader.visit(visitor); + visit(visitor); return visitor.labelPosition(); } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Tile2DVisitor.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Tile2DVisitor.java index 126f80d1079b..ef780d7e5d06 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Tile2DVisitor.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Tile2DVisitor.java @@ -16,7 +16,7 @@ * This class supports checking bounding box relations against a serialized triangle tree. * */ -class Tile2DVisitor implements TriangleTreeReader.Visitor { +class Tile2DVisitor implements TriangleTreeVisitor { private GeoRelation relation; private int minX; diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/TriangleTreeReader.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/TriangleTreeReader.java index 0bb02f40b505..8fe4299b17a0 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/TriangleTreeReader.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/TriangleTreeReader.java @@ -15,7 +15,7 @@ * A tree reader for a previous serialized {@link org.elasticsearch.geometry.Geometry} using * {@link TriangleTreeWriter}. * - * The tree structure is navigated using a {@link Visitor}. + * The tree structure is navigated using a {@link TriangleTreeVisitor}. * */ class TriangleTreeReader { @@ -23,16 +23,15 @@ class TriangleTreeReader { private TriangleTreeReader() {} /** - * Visit the Triangle tree using the {@link Visitor} provided. + * Visit the Triangle tree using the {@link TriangleTreeVisitor} provided. */ - public static void visit(ByteArrayStreamInput input, TriangleTreeReader.Visitor visitor, int thisMaxX, int thisMaxY) - throws IOException { + public static void visit(ByteArrayStreamInput input, TriangleTreeVisitor visitor, int thisMaxX, int thisMaxY) throws IOException { visit(input, visitor, true, thisMaxX, thisMaxY, true); } private static boolean visit( ByteArrayStreamInput input, - TriangleTreeReader.Visitor visitor, + TriangleTreeVisitor visitor, boolean splitX, int thisMaxX, int thisMaxY, @@ -90,13 +89,8 @@ private static boolean visit( return visitor.push(); } - private static boolean pushLeft( - ByteArrayStreamInput input, - TriangleTreeReader.Visitor visitor, - int thisMaxX, - int thisMaxY, - boolean splitX - ) throws IOException { + private static boolean pushLeft(ByteArrayStreamInput input, TriangleTreeVisitor visitor, int thisMaxX, int thisMaxY, boolean splitX) + throws IOException { int nextMaxX = Math.toIntExact(thisMaxX - input.readVLong()); int nextMaxY = Math.toIntExact(thisMaxY - input.readVLong()); int size = input.readVInt(); @@ -110,7 +104,7 @@ private static boolean pushLeft( private static boolean pushRight( ByteArrayStreamInput input, - TriangleTreeReader.Visitor visitor, + TriangleTreeVisitor visitor, int thisMaxX, int thisMaxY, int thisMinX, @@ -132,125 +126,4 @@ private static boolean pushRight( } return visitor.push(); } - - /** Visitor for triangle interval tree */ - interface Visitor { - - /** visit a node point. */ - void visitPoint(int x, int y); - - /** visit a node line. */ - void visitLine(int aX, int aY, int bX, int bY, byte metadata); - - /** visit a node triangle. */ - void visitTriangle(int aX, int aY, int bX, int bY, int cX, int cY, byte metadata); - - /** Should the visitor keep visiting the tree. Called after visiting a node or skipping - * a tree branch, if the return value is {@code false}, no more nodes will be visited. */ - boolean push(); - - /** Should the visitor visit nodes that have bounds greater or equal - * than the {@code minX} provided. */ - boolean pushX(int minX); - - /** Should the visitor visit nodes that have bounds greater or equal - * than the {@code minY} provided. */ - boolean pushY(int minY); - - /** Should the visitor visit nodes that have bounds lower or equal than the - * {@code maxX} and {@code minX} provided. */ - boolean push(int maxX, int maxY); - - /** Should the visitor visit the tree given the bounding box of the tree. Called before - * visiting the tree. */ - boolean push(int minX, int minY, int maxX, int maxY); - } - - /** Visitor for triangle interval tree which decodes the coordinates */ - public abstract static class DecodedVisitor implements Visitor { - - private final CoordinateEncoder encoder; - - DecodedVisitor(CoordinateEncoder encoder) { - this.encoder = encoder; - } - - @Override - public void visitPoint(int x, int y) { - visitDecodedPoint(encoder.decodeX(x), encoder.decodeY(y)); - } - - /** - * Equivalent to {@link #visitPoint(int, int)} but coordinates are decoded. - */ - abstract void visitDecodedPoint(double x, double y); - - @Override - public void visitLine(int aX, int aY, int bX, int bY, byte metadata) { - visitDecodedLine(encoder.decodeX(aX), encoder.decodeY(aY), encoder.decodeX(bX), encoder.decodeY(bY), metadata); - } - - /** - * Equivalent to {@link #visitLine(int, int, int, int, byte)} but coordinates are decoded. - */ - abstract void visitDecodedLine(double aX, double aY, double bX, double bY, byte metadata); - - @Override - public void visitTriangle(int aX, int aY, int bX, int bY, int cX, int cY, byte metadata) { - visitDecodedTriangle( - encoder.decodeX(aX), - encoder.decodeY(aY), - encoder.decodeX(bX), - encoder.decodeY(bY), - encoder.decodeX(cX), - encoder.decodeY(cY), - metadata - ); - } - - /** - * Equivalent to {@link #visitTriangle(int, int, int, int, int, int, byte)} but coordinates are decoded. - */ - abstract void visitDecodedTriangle(double aX, double aY, double bX, double bY, double cX, double cY, byte metadata); - - @Override - public boolean pushX(int minX) { - return pushDecodedX(encoder.decodeX(minX)); - } - - /** - * Equivalent to {@link #pushX(int)} but coordinates are decoded. - */ - abstract boolean pushDecodedX(double minX); - - @Override - public boolean pushY(int minY) { - return pushDecodedY(encoder.decodeY(minY)); - } - - /** - * Equivalent to {@link #pushY(int)} but coordinates are decoded. - */ - abstract boolean pushDecodedY(double minX); - - @Override - public boolean push(int maxX, int maxY) { - return pushDecoded(encoder.decodeX(maxX), encoder.decodeY(maxY)); - } - - /** - * Equivalent to {@link #push(int, int)} but coordinates are decoded. - */ - abstract boolean pushDecoded(double maxX, double maxY); - - @Override - public boolean push(int minX, int minY, int maxX, int maxY) { - return pushDecoded(encoder.decodeX(minX), encoder.decodeY(minY), encoder.decodeX(maxX), encoder.decodeY(maxY)); - } - - /** - * Equivalent to {@link #push(int, int, int, int)} but coordinates are decoded. - */ - abstract boolean pushDecoded(double minX, double minY, double maxX, double maxY); - } } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/TriangleTreeVisitor.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/TriangleTreeVisitor.java new file mode 100644 index 000000000000..283e8c902a71 --- /dev/null +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/TriangleTreeVisitor.java @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.spatial.index.fielddata; + +/** Visitor for triangle interval tree. + * + * @see TriangleTreeReader + * */ +public interface TriangleTreeVisitor { + + /** visit a node point. */ + void visitPoint(int x, int y); + + /** visit a node line. */ + void visitLine(int aX, int aY, int bX, int bY, byte metadata); + + /** visit a node triangle. */ + void visitTriangle(int aX, int aY, int bX, int bY, int cX, int cY, byte metadata); + + /** Should the visitor keep visiting the tree. Called after visiting a node or skipping + * a tree branch, if the return value is {@code false}, no more nodes will be visited. */ + boolean push(); + + /** Should the visitor visit nodes that have bounds greater or equal + * than the {@code minX} provided. */ + boolean pushX(int minX); + + /** Should the visitor visit nodes that have bounds greater or equal + * than the {@code minY} provided. */ + boolean pushY(int minY); + + /** Should the visitor visit nodes that have bounds lower or equal than the + * {@code maxX} and {@code minX} provided. */ + boolean push(int maxX, int maxY); + + /** Should the visitor visit the tree given the bounding box of the tree. Called before + * visiting the tree. */ + boolean push(int minX, int minY, int maxX, int maxY); + + /** Visitor for triangle interval tree which decodes the coordinates */ + abstract class TriangleTreeDecodedVisitor implements TriangleTreeVisitor { + + private final CoordinateEncoder encoder; + + protected TriangleTreeDecodedVisitor(CoordinateEncoder encoder) { + this.encoder = encoder; + } + + @Override + public final void visitPoint(int x, int y) { + visitDecodedPoint(encoder.decodeX(x), encoder.decodeY(y)); + } + + /** + * Equivalent to {@link #visitPoint(int, int)} but coordinates are decoded. + */ + protected abstract void visitDecodedPoint(double x, double y); + + @Override + public final void visitLine(int aX, int aY, int bX, int bY, byte metadata) { + visitDecodedLine(encoder.decodeX(aX), encoder.decodeY(aY), encoder.decodeX(bX), encoder.decodeY(bY), metadata); + } + + /** + * Equivalent to {@link #visitLine(int, int, int, int, byte)} but coordinates are decoded. + */ + protected abstract void visitDecodedLine(double aX, double aY, double bX, double bY, byte metadata); + + @Override + public final void visitTriangle(int aX, int aY, int bX, int bY, int cX, int cY, byte metadata) { + visitDecodedTriangle( + encoder.decodeX(aX), + encoder.decodeY(aY), + encoder.decodeX(bX), + encoder.decodeY(bY), + encoder.decodeX(cX), + encoder.decodeY(cY), + metadata + ); + } + + /** + * Equivalent to {@link #visitTriangle(int, int, int, int, int, int, byte)} but coordinates are decoded. + */ + protected abstract void visitDecodedTriangle(double aX, double aY, double bX, double bY, double cX, double cY, byte metadata); + + @Override + public final boolean pushX(int minX) { + return pushDecodedX(encoder.decodeX(minX)); + } + + /** + * Equivalent to {@link #pushX(int)} but coordinates are decoded. + */ + protected abstract boolean pushDecodedX(double minX); + + @Override + public final boolean pushY(int minY) { + return pushDecodedY(encoder.decodeY(minY)); + } + + /** + * Equivalent to {@link #pushY(int)} but coordinates are decoded. + */ + protected abstract boolean pushDecodedY(double minX); + + @Override + public final boolean push(int maxX, int maxY) { + return pushDecoded(encoder.decodeX(maxX), encoder.decodeY(maxY)); + } + + /** + * Equivalent to {@link #push(int, int)} but coordinates are decoded. + */ + protected abstract boolean pushDecoded(double maxX, double maxY); + + @Override + public final boolean push(int minX, int minY, int maxX, int maxY) { + return pushDecoded(encoder.decodeX(minX), encoder.decodeY(minY), encoder.decodeX(maxX), encoder.decodeY(maxY)); + } + + /** + * Equivalent to {@link #push(int, int, int, int)} but coordinates are decoded. + */ + protected abstract boolean pushDecoded(double minX, double minY, double maxX, double maxY); + } +} diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/TriangleTreeTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/TriangleTreeTests.java index c9fa0d65cf82..435d0f314412 100644 --- a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/TriangleTreeTests.java +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/TriangleTreeTests.java @@ -42,7 +42,7 @@ public void testVisitAllTriangles() throws IOException { assertThat(fieldList.size(), equalTo(visitor.counter)); } - private static class TriangleCounterVisitor implements TriangleTreeReader.Visitor { + private static class TriangleCounterVisitor implements TriangleTreeVisitor { int counter; From 2061788141386fd6f78c5af54860994d6de19060 Mon Sep 17 00:00:00 2001 From: Przemyslaw Gomulka Date: Thu, 15 Dec 2022 11:23:37 +0100 Subject: [PATCH 280/919] Add jdk.internal.reflect permission to es codebase (#92387) Dynamic settings creation is using dynamic proxy which needs this permission. We use UberModuleClassLoader for stable api without module-info. And this classloader ends up on the stacktrace when the proxy is created. Adding this permission to ES code will allow to pass SecurityManager check. closes #92356 --- docs/changelog/92387.yaml | 6 ++++++ .../resources/org/elasticsearch/bootstrap/security.policy | 3 +++ 2 files changed, 9 insertions(+) create mode 100644 docs/changelog/92387.yaml diff --git a/docs/changelog/92387.yaml b/docs/changelog/92387.yaml new file mode 100644 index 000000000000..558a94169c93 --- /dev/null +++ b/docs/changelog/92387.yaml @@ -0,0 +1,6 @@ +pr: 92387 +summary: Add `jdk.internal.reflect` permission to es codebase +area: Infra/Core +type: bug +issues: + - 92356 diff --git a/server/src/main/resources/org/elasticsearch/bootstrap/security.policy b/server/src/main/resources/org/elasticsearch/bootstrap/security.policy index 3b21c273d3a1..71bcaba7580e 100644 --- a/server/src/main/resources/org/elasticsearch/bootstrap/security.policy +++ b/server/src/main/resources/org/elasticsearch/bootstrap/security.policy @@ -28,6 +28,9 @@ grant codeBase "${codebase.elasticsearch}" { // for module layer permission java.lang.RuntimePermission "createClassLoader"; permission java.lang.RuntimePermission "getClassLoader"; + + // for plugin api dynamic settings instances + permission java.lang.RuntimePermission "accessClassInPackage.jdk.internal.reflect"; }; //// Very special jar permissions: From f185282a88eca05fce68941b00568fda5f1410d6 Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Thu, 15 Dec 2022 13:21:09 +0000 Subject: [PATCH 281/919] Use https in the short URLs for the shards_availability indicator (#92310) --- docs/changelog/92310.yaml | 5 +++++ ...rdsAvailabilityHealthIndicatorService.java | 20 +++++++++---------- 2 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 docs/changelog/92310.yaml diff --git a/docs/changelog/92310.yaml b/docs/changelog/92310.yaml new file mode 100644 index 000000000000..7c22b8219610 --- /dev/null +++ b/docs/changelog/92310.yaml @@ -0,0 +1,5 @@ +pr: 92310 +summary: Use https in the short URLs for the `shards_availability` indicator +area: Health +type: bug +issues: [] diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java index 2efe60c65146..2752c7b6ad93 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java @@ -134,7 +134,7 @@ public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { public static final String PRIMARY_UNASSIGNED_IMPACT_ID = "primary_unassigned"; public static final String REPLICA_UNASSIGNED_IMPACT_ID = "replica_unassigned"; - public static final String RESTORE_FROM_SNAPSHOT_ACTION_GUIDE = "http://ela.st/restore-snapshot"; + public static final String RESTORE_FROM_SNAPSHOT_ACTION_GUIDE = "https://ela.st/restore-snapshot"; public static final Diagnosis.Definition ACTION_RESTORE_FROM_SNAPSHOT = new Diagnosis.Definition( NAME, "restore_from_snapshot", @@ -144,7 +144,7 @@ public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { RESTORE_FROM_SNAPSHOT_ACTION_GUIDE ); - public static final String DIAGNOSE_SHARDS_ACTION_GUIDE = "http://ela.st/diagnose-shards"; + public static final String DIAGNOSE_SHARDS_ACTION_GUIDE = "https://ela.st/diagnose-shards"; public static final Diagnosis.Definition ACTION_CHECK_ALLOCATION_EXPLAIN_API = new Diagnosis.Definition( NAME, "explain_allocations", @@ -155,7 +155,7 @@ public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { DIAGNOSE_SHARDS_ACTION_GUIDE ); - public static final String FIX_DELAYED_SHARDS_GUIDE = "http://ela.st/fix-delayed-shard-allocation"; + public static final String FIX_DELAYED_SHARDS_GUIDE = "https://ela.st/fix-delayed-shard-allocation"; public static final Diagnosis.Definition DIAGNOSIS_WAIT_FOR_OR_FIX_DELAYED_SHARDS = new Diagnosis.Definition( NAME, "delayed_shard_allocations", @@ -166,7 +166,7 @@ public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { FIX_DELAYED_SHARDS_GUIDE ); - public static final String ENABLE_INDEX_ALLOCATION_GUIDE = "http://ela.st/fix-index-allocation"; + public static final String ENABLE_INDEX_ALLOCATION_GUIDE = "https://ela.st/fix-index-allocation"; public static final Diagnosis.Definition ACTION_ENABLE_INDEX_ROUTING_ALLOCATION = new Diagnosis.Definition( NAME, "enable_index_allocations", @@ -179,7 +179,7 @@ public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { + "].", ENABLE_INDEX_ALLOCATION_GUIDE ); - public static final String ENABLE_CLUSTER_ALLOCATION_ACTION_GUIDE = "http://ela.st/fix-cluster-allocation"; + public static final String ENABLE_CLUSTER_ALLOCATION_ACTION_GUIDE = "https://ela.st/fix-cluster-allocation"; public static final Diagnosis.Definition ACTION_ENABLE_CLUSTER_ROUTING_ALLOCATION = new Diagnosis.Definition( NAME, "enable_cluster_allocations", @@ -193,7 +193,7 @@ public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { ENABLE_CLUSTER_ALLOCATION_ACTION_GUIDE ); - public static final String ENABLE_TIER_ACTION_GUIDE = "http://ela.st/enable-tier"; + public static final String ENABLE_TIER_ACTION_GUIDE = "https://ela.st/enable-tier"; public static final Map ACTION_ENABLE_TIERS_LOOKUP = DataTier.ALL_DATA_TIERS.stream() .collect( Collectors.toUnmodifiableMap( @@ -209,7 +209,7 @@ public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { ) ); - public static final String INCREASE_SHARD_LIMIT_ACTION_GUIDE = "http://ela.st/index-total-shards"; + public static final String INCREASE_SHARD_LIMIT_ACTION_GUIDE = "https://ela.st/index-total-shards"; public static final Diagnosis.Definition ACTION_INCREASE_SHARD_LIMIT_INDEX_SETTING = new Diagnosis.Definition( NAME, "increase_shard_limit_index_setting", @@ -240,7 +240,7 @@ public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { ) ); - public static final String INCREASE_CLUSTER_SHARD_LIMIT_ACTION_GUIDE = "http://ela.st/cluster-total-shards"; + public static final String INCREASE_CLUSTER_SHARD_LIMIT_ACTION_GUIDE = "https://ela.st/cluster-total-shards"; public static final Diagnosis.Definition ACTION_INCREASE_SHARD_LIMIT_CLUSTER_SETTING = new Diagnosis.Definition( NAME, "increase_shard_limit_cluster_setting", @@ -271,7 +271,7 @@ public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { ) ); - public static final String MIGRATE_TO_TIERS_ACTION_GUIDE = "http://ela.st/migrate-to-tiers"; + public static final String MIGRATE_TO_TIERS_ACTION_GUIDE = "https://ela.st/migrate-to-tiers"; public static final Diagnosis.Definition ACTION_MIGRATE_TIERS_AWAY_FROM_REQUIRE_DATA = new Diagnosis.Definition( NAME, "migrate_data_tiers_require_data", @@ -340,7 +340,7 @@ public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { ) ); - public static final String TIER_CAPACITY_ACTION_GUIDE = "http://ela.st/tier-capacity"; + public static final String TIER_CAPACITY_ACTION_GUIDE = "https://ela.st/tier-capacity"; public static final Diagnosis.Definition ACTION_INCREASE_NODE_CAPACITY = new Diagnosis.Definition( NAME, "increase_node_capacity_for_allocations", From ff6f1bc227cc05aad190d91faef0eeb2d76b6417 Mon Sep 17 00:00:00 2001 From: apurvsibal <30145118+apurvsibal@users.noreply.github.com> Date: Thu, 15 Dec 2022 08:44:58 -0500 Subject: [PATCH 282/919] Wrapper query docs refer to transport client and HLRC #89263 (#91149) --- docs/reference/query-dsl/wrapper-query.asciidoc | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/reference/query-dsl/wrapper-query.asciidoc b/docs/reference/query-dsl/wrapper-query.asciidoc index b8b9626202e7..fe0d72684943 100644 --- a/docs/reference/query-dsl/wrapper-query.asciidoc +++ b/docs/reference/query-dsl/wrapper-query.asciidoc @@ -20,7 +20,4 @@ GET /_search <1> Base64 encoded string: `{"term" : { "user.id" : "kimchy" }}` -This query is more useful in the context of the Java high-level REST client or -transport client to also accept queries as json formatted string. -In these cases queries can be specified as a json or yaml formatted string or -as a query builder (which is a available in the Java high-level REST client). \ No newline at end of file +This query is more useful in the context of Spring Data Elasticsearch. It's the way a user can add custom queries when using Spring Data repositories. The user can add a @Query() annotation to a repository method. When such a method is called we do a parameter replacement in the query argument of the annotation and then send this as the query part of a search request. \ No newline at end of file From 53dc50caef81acfb3fa190fd381a93bae2bc56bb Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Thu, 15 Dec 2022 18:13:12 +0400 Subject: [PATCH 283/919] Bump versions after 7.17.8 release --- .ci/bwcVersions | 1 + .ci/snapshotBwcVersions | 2 +- server/src/main/java/org/elasticsearch/Version.java | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.ci/bwcVersions b/.ci/bwcVersions index 3ff318966aba..2f1531477afc 100644 --- a/.ci/bwcVersions +++ b/.ci/bwcVersions @@ -57,6 +57,7 @@ BWC_VERSION: - "7.17.6" - "7.17.7" - "7.17.8" + - "7.17.9" - "8.0.0" - "8.0.1" - "8.1.0" diff --git a/.ci/snapshotBwcVersions b/.ci/snapshotBwcVersions index 549f4b3959a8..c9e1a9e1000a 100644 --- a/.ci/snapshotBwcVersions +++ b/.ci/snapshotBwcVersions @@ -1,5 +1,5 @@ BWC_VERSION: - - "7.17.8" + - "7.17.9" - "8.5.4" - "8.6.0" - "8.7.0" diff --git a/server/src/main/java/org/elasticsearch/Version.java b/server/src/main/java/org/elasticsearch/Version.java index 5c2bd4d325bc..8f4820b9eba1 100644 --- a/server/src/main/java/org/elasticsearch/Version.java +++ b/server/src/main/java/org/elasticsearch/Version.java @@ -105,6 +105,7 @@ public class Version implements Comparable, ToXContentFragment { public static final Version V_7_17_6 = new Version(7_17_06_99, org.apache.lucene.util.Version.LUCENE_8_11_1); public static final Version V_7_17_7 = new Version(7_17_07_99, org.apache.lucene.util.Version.LUCENE_8_11_1); public static final Version V_7_17_8 = new Version(7_17_08_99, org.apache.lucene.util.Version.LUCENE_8_11_1); + public static final Version V_7_17_9 = new Version(7_17_09_99, org.apache.lucene.util.Version.LUCENE_8_11_1); public static final Version V_8_0_0 = new Version(8_00_00_99, org.apache.lucene.util.Version.LUCENE_9_0_0); public static final Version V_8_0_1 = new Version(8_00_01_99, org.apache.lucene.util.Version.LUCENE_9_0_0); public static final Version V_8_1_0 = new Version(8_01_00_99, org.apache.lucene.util.Version.LUCENE_9_0_0); From 2d6ff2c7a0d610c85e3841b3881a497e8e9e4902 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Thu, 15 Dec 2022 07:46:52 -0800 Subject: [PATCH 284/919] Force init of Unbox in log4j (#92377) * Force init of Unbox in log4j Some libaries have static initializers that require security manager permissions. When this initialization occurs during normal operation, it can cause security manager exceptions depending on where a logging call is made. This commit adds a place for static initialization of log4j utility classes to be run, and specifically adds the Unbox class which wants to read env vars. closes #91964 --- docs/changelog/92377.yaml | 6 ++++++ .../common/logging/LogConfigurator.java | 14 ++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 docs/changelog/92377.yaml diff --git a/docs/changelog/92377.yaml b/docs/changelog/92377.yaml new file mode 100644 index 000000000000..0eb622f71714 --- /dev/null +++ b/docs/changelog/92377.yaml @@ -0,0 +1,6 @@ +pr: 92377 +summary: Force init of Unbox in log4j +area: Infra/Core +type: bug +issues: + - 91964 diff --git a/server/src/main/java/org/elasticsearch/common/logging/LogConfigurator.java b/server/src/main/java/org/elasticsearch/common/logging/LogConfigurator.java index 2147b7a5531b..149877524b77 100644 --- a/server/src/main/java/org/elasticsearch/common/logging/LogConfigurator.java +++ b/server/src/main/java/org/elasticsearch/common/logging/LogConfigurator.java @@ -30,6 +30,7 @@ import org.apache.logging.log4j.status.StatusData; import org.apache.logging.log4j.status.StatusListener; import org.apache.logging.log4j.status.StatusLogger; +import org.apache.logging.log4j.util.Unbox; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.common.logging.internal.LoggerFactoryImpl; import org.elasticsearch.common.settings.Settings; @@ -41,6 +42,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; +import java.lang.invoke.MethodHandles; import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; @@ -122,6 +124,7 @@ public static void configure(final Environment environment, boolean useConsole) } configureESLogging(); configure(environment.settings(), environment.configFile(), environment.logsFile(), useConsole); + initializeStatics(); } public static void configureESLogging() { @@ -144,6 +147,17 @@ public static void setNodeName(String nodeName) { NodeNamePatternConverter.setNodeName(nodeName); } + // Some classes within log4j have static initializers that require security manager permissions. + // Here we aggressively initialize those classes during logging configuration so that + // actual logging calls at runtime do not trigger that initialization. + private static void initializeStatics() { + try { + MethodHandles.publicLookup().ensureInitialized(Unbox.class); + } catch (IllegalAccessException impossible) { + throw new AssertionError(impossible); + } + } + private static void checkErrorListener() { assert errorListenerIsRegistered() : "expected error listener to be registered"; if (error.get()) { From e5ed2942e06651becb185a70e98ec325ec4317f2 Mon Sep 17 00:00:00 2001 From: Craig Taverner Date: Thu, 15 Dec 2022 18:34:43 +0100 Subject: [PATCH 285/919] Fix flaky tests for points (#92151) (#92401) This fixes https://github.com/elastic/elasticsearch/issues/92151 It also provides some additional testing around points, lines and polygons. And it provides an extra test in preparation for fixing flaky testLine https://github.com/elastic/elasticsearch/issues/92142 The testLine flaky test is not fixed however, as it requires a fix in lucene, which will hopefully come out soon. See https://github.com/apache/lucene/pull/12022 After that a small PR to remove the remaining two `@AwaitsFix` lines can be made. --- .../fielddata/Component2DRelationVisitor.java | 25 +- .../LatLonGeometryRelationVisitorTests.java | 283 +++++++++++++++--- 2 files changed, 248 insertions(+), 60 deletions(-) diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Component2DRelationVisitor.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Component2DRelationVisitor.java index c40aa582dc3a..bdbf52c311df 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Component2DRelationVisitor.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/Component2DRelationVisitor.java @@ -41,10 +41,10 @@ public GeoRelation relation() { @Override protected void visitDecodedPoint(double x, double y) { if (component2D.contains(x, y)) { - if (canBeContained()) { - relation = GeoRelation.QUERY_CONTAINS; - } else if (component2D.withinPoint(x, y) == Component2D.WithinRelation.CANDIDATE) { + if (canBeInside() && component2D.withinPoint(x, y) == Component2D.WithinRelation.CANDIDATE) { relation = GeoRelation.QUERY_INSIDE; + } else if (canBeContained()) { + relation = GeoRelation.QUERY_CONTAINS; } else { relation = GeoRelation.QUERY_CROSSES; } @@ -57,10 +57,10 @@ protected void visitDecodedPoint(double x, double y) { protected void visitDecodedLine(double aX, double aY, double bX, double bY, byte metadata) { if (component2D.intersectsLine(aX, aY, bX, bY)) { final boolean ab = (metadata & 1 << 4) == 1 << 4; - if (canBeContained() && component2D.containsLine(aX, aY, bX, bY)) { - relation = GeoRelation.QUERY_CONTAINS; - } else if (canBeInside() && component2D.withinLine(aX, aY, ab, bX, bY) == Component2D.WithinRelation.CANDIDATE) { + if (canBeInside() && component2D.withinLine(aX, aY, ab, bX, bY) == Component2D.WithinRelation.CANDIDATE) { relation = GeoRelation.QUERY_INSIDE; + } else if (canBeContained() && component2D.containsLine(aX, aY, bX, bY)) { + relation = GeoRelation.QUERY_CONTAINS; } else { relation = GeoRelation.QUERY_CROSSES; } @@ -75,14 +75,13 @@ protected void visitDecodedTriangle(double aX, double aY, double bX, double bY, final boolean ab = (metadata & 1 << 4) == 1 << 4; final boolean bc = (metadata & 1 << 5) == 1 << 5; final boolean ca = (metadata & 1 << 6) == 1 << 6; - if (canBeContained() && component2D.containsTriangle(aX, aY, bX, bY, cX, cY)) { + if (canBeInside() && component2D.withinTriangle(aX, aY, ab, bX, bY, bc, cX, cY, ca) == Component2D.WithinRelation.CANDIDATE) { + relation = GeoRelation.QUERY_INSIDE; + } else if (canBeContained() && component2D.containsTriangle(aX, aY, bX, bY, cX, cY)) { relation = GeoRelation.QUERY_CONTAINS; - } else if (canBeInside() - && component2D.withinTriangle(aX, aY, ab, bX, bY, bc, cX, cY, ca) == Component2D.WithinRelation.CANDIDATE) { - relation = GeoRelation.QUERY_INSIDE; - } else { - relation = GeoRelation.QUERY_CROSSES; - } + } else { + relation = GeoRelation.QUERY_CROSSES; + } } else { adjustRelationForNotIntersectingComponent(); } diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/LatLonGeometryRelationVisitorTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/LatLonGeometryRelationVisitorTests.java index 25b7a381e862..3ba3b513ebfa 100644 --- a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/LatLonGeometryRelationVisitorTests.java +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/LatLonGeometryRelationVisitorTests.java @@ -7,27 +7,42 @@ package org.elasticsearch.xpack.spatial.index.fielddata; -import org.apache.lucene.document.ShapeField; +import org.apache.lucene.document.ShapeField.QueryRelation; import org.apache.lucene.geo.Component2D; import org.apache.lucene.geo.LatLonGeometry; +import org.apache.lucene.geo.Line; +import org.apache.lucene.geo.Point; +import org.apache.lucene.geo.Polygon; import org.apache.lucene.tests.geo.GeoTestUtil; +import org.elasticsearch.common.geo.GeometryNormalizer; +import org.elasticsearch.common.geo.Orientation; import org.elasticsearch.geo.GeometryTestUtils; import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.LinearRing; +import org.elasticsearch.geometry.MultiPoint; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.spatial.util.GeoTestUtils; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.function.Supplier; +import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude; +import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude; +import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude; +import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude; import static org.hamcrest.Matchers.equalTo; public class LatLonGeometryRelationVisitorTests extends ESTestCase { - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/92151") public void testPoint() throws Exception { doTestShapes(GeoTestUtil::nextPoint); } @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/92142") + // This is waiting for a Lucene release that includes https://github.com/apache/lucene/pull/12022 public void testLine() throws Exception { doTestShapes(GeoTestUtil::nextLine); } @@ -36,55 +51,229 @@ public void testPolygon() throws Exception { doTestShapes(GeoTestUtil::nextPolygon); } - private void doTestShapes(Supplier supplier) throws Exception { - Geometry geometry = GeometryTestUtils.randomGeometryWithoutCircle(0, false); + // Specific tests for known troublesome points from https://github.com/elastic/elasticsearch/issues/92151 + public void testTroublesomePoints() throws Exception { + ArrayList points = new ArrayList<>(); + points.add(new org.elasticsearch.geometry.Point(-6.6957112520498185, 5.337277253715181E-129)); + points.add(new org.elasticsearch.geometry.Point(0.0, 1.6938947866910307E-202)); + points.add(new org.elasticsearch.geometry.Point(-114.40977624485328, -37.484381576244864)); + points.add(new org.elasticsearch.geometry.Point(49.1828546738179, 23.813793855174865)); + points.add(new org.elasticsearch.geometry.Point(60.5683489131913, 90.0)); + points.add(new org.elasticsearch.geometry.Point(-79.65717776327665, -39.5)); + points.add(new org.elasticsearch.geometry.Point(1.401298464324817E-45, 0.0)); + MultiPoint geometry = new MultiPoint(points); GeoShapeValues.GeoShapeValue geoShapeValue = GeoTestUtils.geoShapeValue(geometry); GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(geometry, CoordinateEncoder.GEO); - for (int i = 0; i < 1000; i++) { - LatLonGeometry latLonGeometry = supplier.get(); + Point[] troublesome = new Point[] { + new Point(-6.404126347681029E-213, 0.0), // This point will match the last point in the multipoint + new Point(0.0, 0.0) // this point will match the second point in the multipoint + }; + for (Point point : troublesome) { + doTestShape(geometry, geoShapeValue, reader, quantize(point)); + } + } + + public void testIdenticalPoint() throws Exception { + double x = quantizeLon(1); + double y = quantizeLat(1); + org.elasticsearch.geometry.Point shape = new org.elasticsearch.geometry.Point(x, y); + Point latLonGeometry = new Point(shape.getLat(), shape.getLon()); + + GeoShapeValues.GeoShapeValue geoShapeValue = GeoTestUtils.geoShapeValue(shape); + GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(shape, CoordinateEncoder.GEO); + GeoRelation relation = geoShapeValue.relate(latLonGeometry); + assertThat("Identical points", relation, equalTo(GeoRelation.QUERY_INSIDE)); + doTestShape(shape, reader, latLonGeometry, relation, true); + } + + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/92142") + // This is waiting for a Lucene release that includes https://github.com/apache/lucene/pull/12022 + public void testVeryFlatPolygonDoesNotContainIntersectingLine() throws Exception { + double[] x = new double[] { -0.001, -0.001, 0.001, 0.001, -0.001 }; + double[] y = new double[] { 1e-10, 0, -1e-10, 0, 1e-10 }; + Geometry geometry = new org.elasticsearch.geometry.Polygon(new LinearRing(x, y)); + GeoShapeValues.GeoShapeValue geoShapeValue = GeoTestUtils.geoShapeValue(geometry); + GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(geometry, CoordinateEncoder.GEO); + double[] lons = new double[] { 0.0, 0.0 }; + double[] lats = new double[] { 0.0, 0.001 }; + Line line = new Line(lats, lons); + doTestShape(geometry, geoShapeValue, reader, line); + } + + public void testContainedPolygons() throws Exception { + // Create simple rectangular polygon + double[] x = new double[] { -1, 1, 1, -1, -1 }; + double[] y = new double[] { -1, -1, 1, 1, -1 }; + quantize(y, x); + org.elasticsearch.geometry.Polygon shape = new org.elasticsearch.geometry.Polygon(new LinearRing(x, y)); + + // Setup tests for contains, identical and within + LinkedHashMap tests = new LinkedHashMap<>(); + tests.put(0.5, GeoRelation.QUERY_INSIDE); + tests.put(1.0, GeoRelation.QUERY_CONTAINS); + tests.put(2.0, GeoRelation.QUERY_CONTAINS); + for (Map.Entry entry : tests.entrySet()) { + double factor = entry.getKey(); + GeoRelation expected = entry.getValue(); + double[] lats = new double[y.length]; + double[] lons = new double[x.length]; + for (int i = 0; i < x.length; i++) { + lats[i] = quantizeLat(y[i] * factor); + lons[i] = quantizeLon(x[i] * factor); + } + Polygon latLonGeometry = new Polygon(lats, lons); + boolean identical = factor == 1.0; + // Assert that polygons are identical + if (identical) { + for (int i = 0; i < latLonGeometry.numPoints(); i++) { + assertThat("Latitude[" + i + "]", latLonGeometry.getPolyLat(i), equalTo(shape.getPolygon().getLat(i))); + assertThat("Longitude[" + i + "]", latLonGeometry.getPolyLon(i), equalTo(shape.getPolygon().getLon(i))); + } + } + + GeoShapeValues.GeoShapeValue geoShapeValue = GeoTestUtils.geoShapeValue(shape); + GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(shape, CoordinateEncoder.GEO); GeoRelation relation = geoShapeValue.relate(latLonGeometry); - Component2D component2D = LatLonGeometry.create(latLonGeometry); - Component2DVisitor contains = Component2DVisitor.getVisitor( - component2D, - ShapeField.QueryRelation.CONTAINS, - CoordinateEncoder.GEO - ); - reader.visit(contains); - Component2DVisitor intersects = Component2DVisitor.getVisitor( - component2D, - ShapeField.QueryRelation.INTERSECTS, - CoordinateEncoder.GEO - ); - reader.visit(intersects); - Component2DVisitor disjoint = Component2DVisitor.getVisitor( - component2D, - ShapeField.QueryRelation.DISJOINT, - CoordinateEncoder.GEO - ); - reader.visit(disjoint); - Component2DVisitor within = Component2DVisitor.getVisitor(component2D, ShapeField.QueryRelation.WITHIN, CoordinateEncoder.GEO); - reader.visit(within); - if (relation == GeoRelation.QUERY_INSIDE) { - assertThat(contains.matches(), equalTo(true)); - assertThat(intersects.matches(), equalTo(true)); - assertThat(disjoint.matches(), equalTo(false)); - assertThat(within.matches(), equalTo(false)); - } else if (relation == GeoRelation.QUERY_CROSSES) { - assertThat(contains.matches(), equalTo(false)); - assertThat(intersects.matches(), equalTo(true)); - assertThat(disjoint.matches(), equalTo(false)); - assertThat(within.matches(), equalTo(false)); - } else if (relation == GeoRelation.QUERY_CONTAINS) { - assertThat(contains.matches(), equalTo(false)); - assertThat(intersects.matches(), equalTo(true)); - assertThat(disjoint.matches(), equalTo(false)); - assertThat(within.matches(), equalTo(true)); - } else { - assertThat(contains.matches(), equalTo(false)); - assertThat(intersects.matches(), equalTo(false)); - assertThat(disjoint.matches(), equalTo(true)); - assertThat(within.matches(), equalTo(false)); + assertThat("Polygon scaled by " + factor, relation, equalTo(expected)); + doTestShape(shape, reader, latLonGeometry, relation, false); + } + } + + private void doTestShapes(Supplier supplier) throws Exception { + Geometry geometry = GeometryNormalizer.apply(Orientation.CCW, GeometryTestUtils.randomGeometryWithoutCircle(0, false)); + GeoShapeValues.GeoShapeValue geoShapeValue = GeoTestUtils.geoShapeValue(geometry); + GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(geometry, CoordinateEncoder.GEO); + for (int i = 0; i < 1000; i++) { + LatLonGeometry latLonGeometry = quantize(supplier.get()); + doTestShape(geometry, geoShapeValue, reader, latLonGeometry); + } + } + + private void doTestShape( + Geometry geometry, + GeoShapeValues.GeoShapeValue geoShapeValue, + GeometryDocValueReader reader, + LatLonGeometry latLonGeometry + ) throws Exception { + doTestShape(geometry, reader, latLonGeometry, geoShapeValue.relate(latLonGeometry)); + } + + private void doTestShape(Geometry geometry, GeometryDocValueReader reader, LatLonGeometry latLonGeometry, GeoRelation relation) + throws Exception { + doTestShape(geometry, reader, latLonGeometry, relation, isIdenticalPoint(geometry, latLonGeometry)); + } + + private boolean isIdenticalPoint(Geometry geometry, LatLonGeometry latLonGeometry) { + if (geometry instanceof org.elasticsearch.geometry.Point point) { + if (latLonGeometry instanceof Point latLonPoint) { + return encodeLatitude(point.getLat()) == encodeLatitude(latLonPoint.getLat()) + && encodeLongitude(point.getLon()) == encodeLongitude(latLonPoint.getLon()); + } + } + return false; + } + + private boolean pointsOnly(Geometry geometry) { + return geometry instanceof org.elasticsearch.geometry.Point || geometry instanceof org.elasticsearch.geometry.MultiPoint; + } + + private boolean pointsOnly(LatLonGeometry geometry) { + return geometry instanceof Point; + } + + private void doTestShape( + Geometry geometry, + GeometryDocValueReader reader, + LatLonGeometry latLonGeometry, + GeoRelation relation, + boolean identicalPoint // When both geometries are points and identical, then CONTAINS==WITHIN + ) throws Exception { + boolean pointsOnly = pointsOnly(geometry) && pointsOnly(latLonGeometry); + String description = "Geometry " + latLonGeometry + " relates to shape " + geometry.getClass().getSimpleName() + ": " + relation; + Component2D component2D = LatLonGeometry.create(latLonGeometry); + Component2DVisitor contains = visitQueryRelation(component2D, QueryRelation.CONTAINS, reader); + Component2DVisitor intersects = visitQueryRelation(component2D, QueryRelation.INTERSECTS, reader); + Component2DVisitor disjoint = visitQueryRelation(component2D, QueryRelation.DISJOINT, reader); + Component2DVisitor within = visitQueryRelation(component2D, QueryRelation.WITHIN, reader); + if (relation == GeoRelation.QUERY_INSIDE) { + assertThat("CONTAINS/" + relation + ": " + description, contains.matches(), equalTo(true)); + assertThat("INTERSECTS/" + relation + ": " + description, intersects.matches(), equalTo(true)); + assertThat("DISJOINT/" + relation + ": " + description, disjoint.matches(), equalTo(false)); + assertThat("WITHIN/" + relation + ": " + description, within.matches(), equalTo(identicalPoint)); + } else if (relation == GeoRelation.QUERY_CROSSES) { + if (pointsOnly == false) { + // When we have point comparisons, CROSSES can also allow CONTAINS + assertThat("CONTAINS/" + relation + ": " + description, contains.matches(), equalTo(false)); } + assertThat("INTERSECTS/" + relation + ": " + description, intersects.matches(), equalTo(true)); + assertThat("DISJOINT/" + relation + ": " + description, disjoint.matches(), equalTo(false)); + assertThat("WITHIN/" + relation + ": " + description, within.matches(), equalTo(false)); + } else if (relation == GeoRelation.QUERY_CONTAINS) { + assertThat("CONTAINS/" + relation + ": " + description, contains.matches(), equalTo(identicalPoint)); + assertThat("INTERSECTS/" + relation + ": " + description, intersects.matches(), equalTo(true)); + assertThat("DISJOINT/" + relation + ": " + description, disjoint.matches(), equalTo(false)); + assertThat("WITHIN/" + relation + ": " + description, within.matches(), equalTo(true)); + } else { + assertThat("CONTAINS/" + relation + ": " + description, contains.matches(), equalTo(false)); + assertThat("INTERSECTS/" + relation + ": " + description, intersects.matches(), equalTo(false)); + assertThat("DISJOINT/" + relation + ": " + description, disjoint.matches(), equalTo(true)); + assertThat("WITHIN/" + relation + ": " + description, within.matches(), equalTo(false)); + } + } + + private Component2DVisitor visitQueryRelation(Component2D component2D, QueryRelation queryRelation, GeometryDocValueReader reader) + throws IOException { + Component2DVisitor contains = Component2DVisitor.getVisitor(component2D, queryRelation, CoordinateEncoder.GEO); + reader.visit(contains); + return contains; + } + + private LatLonGeometry quantize(LatLonGeometry geometry) { + if (geometry instanceof Point point) { + return quantize(point); + } else if (geometry instanceof Line line) { + return quantize(line); + } else if (geometry instanceof Polygon polygon) { + return quantize(polygon); + } else { + throw new IllegalArgumentException("Unimplemented: quantize(" + geometry.getClass().getSimpleName() + ")"); } } + + private Point quantize(Point point) { + return new Point(quantizeLat(point.getLat()), quantizeLon(point.getLon())); + } + + private Line quantize(Line line) { + double[] lons = line.getLons(); + double[] lats = line.getLats(); + quantize(lats, lons); + return new Line(lats, lons); + } + + private Polygon quantize(Polygon polygon) { + Polygon[] holes = polygon.getHoles(); + for (int i = 0; i < holes.length; i++) { + holes[i] = quantize(holes[i]); + } + double[] lats = polygon.getPolyLats(); + double[] lons = polygon.getPolyLons(); + quantize(lats, lons); + return new Polygon(lats, lons, holes); + } + + private void quantize(double[] lats, double[] lons) { + for (int i = 0; i < lons.length; i++) { + lats[i] = quantizeLat(lats[i]); + lons[i] = quantizeLon(lons[i]); + } + } + + private double quantizeLat(double lat) { + return decodeLatitude(encodeLatitude(lat)); + } + + private double quantizeLon(double lon) { + return decodeLongitude(encodeLongitude(lon)); + } } From eea68d74e1e7ac7f9dd5d7157c72641b470c2499 Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Thu, 15 Dec 2022 10:22:35 -0800 Subject: [PATCH 286/919] Revert back to S3 repository for ML snapshot artifacts (#92381) We have been having issues with reliability of downloads of artifacts from our GCS-backed artifact repository. The root cause has been identified and reported to GCP and a production fix is incoming. The current ETA is January so let's just revert for now to reduce some of the CI failure noise. This change only affects development builds. Both snapshot and staging builds will continue to use DRA artifacts downloaded by CI. --- x-pack/plugin/ml/build.gradle | 32 +++++++------------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/x-pack/plugin/ml/build.gradle b/x-pack/plugin/ml/build.gradle index a7ff0a68b58e..06186540bcab 100644 --- a/x-pack/plugin/ml/build.gradle +++ b/x-pack/plugin/ml/build.gradle @@ -1,6 +1,3 @@ -import org.elasticsearch.gradle.VersionProperties -import org.elasticsearch.gradle.internal.dra.DraResolvePlugin - apply plugin: 'elasticsearch.internal-es-plugin' apply plugin: 'elasticsearch.internal-cluster-test' apply plugin: 'elasticsearch.internal-test-artifact' @@ -14,42 +11,27 @@ esplugin { extendedPlugins = ['x-pack-autoscaling', 'lang-painless'] } -def localRepo = providers.systemProperty('build.ml_cpp.repo').orNull if (useDra == false) { repositories { exclusiveContent { - filter { - includeGroup 'org.elasticsearch.ml' - } forRepository { ivy { name "ml-cpp" + url providers.systemProperty('build.ml_cpp.repo').orElse('https://prelert-artifacts.s3.amazonaws.com').get() metadataSources { // no repository metadata, look directly for the artifact artifact() } - if (localRepo) { - url localRepo - patternLayout { - artifact "maven/[orgPath]/[module]/[revision]/[module]-[revision](-[classifier]).[ext]" - } - } else { - url "https://artifacts-snapshot.elastic.co/" - patternLayout { - if (VersionProperties.isElasticsearchSnapshot()) { - artifact '/ml-cpp/[revision]/downloads/ml-cpp/[module]-[revision]-[classifier].[ext]' - } else { - // When building locally we always use snapshot artifacts even if passing `-Dbuild.snapshot=false`. - // Release builds are always done with a local repo. - artifact '/ml-cpp/[revision]-SNAPSHOT/downloads/ml-cpp/[module]-[revision]-SNAPSHOT-[classifier].[ext]' - } - } + patternLayout { + artifact "maven/org/elasticsearch/ml/ml-cpp/[revision]/[module]-[revision](-[classifier]).[ext]" } } } + filter { + includeGroup 'org.elasticsearch.ml' + } } } - } configurations { @@ -130,4 +112,4 @@ tasks.named("dependencyLicenses").configure { mapping from: /lucene-.*/, to: 'lucene' } -addQaCheckDependencies(project) \ No newline at end of file +addQaCheckDependencies(project) From 1d585cd76807406ccd5fa670e0bbe15ab793e6bd Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 16 Dec 2022 08:40:10 +1100 Subject: [PATCH 287/919] Configurable retention period for inactive API keys (#92219) Inactive API keys, either expired or actively invalidated, are removed from the security index eventually. However expired and invalidated keys have different retention periods. It is hard-coded to be 7 days for expired keys and Zero for invalidated keys. This PR makes the retention period consistent for both expired and invalidated keys. It also exposes a new setting so that the retention period is dyanmically configurable (defaults to 7 days). Relates: #91738 --- docs/changelog/92219.yaml | 5 + .../security/authc/ApiKeyIntegTests.java | 206 ++++++++++++++---- .../xpack/security/Security.java | 1 + .../xpack/security/authc/ApiKeyService.java | 14 +- ...mover.java => InactiveApiKeysRemover.java} | 29 ++- .../logfile/LoggingAuditTrailFilterTests.java | 1 + .../audit/logfile/LoggingAuditTrailTests.java | 3 +- .../security/authc/ApiKeyServiceTests.java | 8 +- .../authc/AuthenticationServiceTests.java | 8 +- .../support/SecondaryAuthenticatorTests.java | 3 + .../authz/store/CompositeRolesStoreTests.java | 13 +- 11 files changed, 237 insertions(+), 54 deletions(-) create mode 100644 docs/changelog/92219.yaml rename x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/{ExpiredApiKeysRemover.java => InactiveApiKeysRemover.java} (77%) diff --git a/docs/changelog/92219.yaml b/docs/changelog/92219.yaml new file mode 100644 index 000000000000..2b889a9c3210 --- /dev/null +++ b/docs/changelog/92219.yaml @@ -0,0 +1,5 @@ +pr: 92219 +summary: Configurable retention period for invalidated or expired API keys +area: Security +type: enhancement +issues: [] diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java index ad3e2c691429..79f674014f68 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java @@ -96,6 +96,7 @@ import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.junit.After; import org.junit.Before; +import org.junit.BeforeClass; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -149,6 +150,7 @@ import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; public class ApiKeyIntegTests extends SecurityIntegTestCase { @@ -162,6 +164,13 @@ public class ApiKeyIntegTests extends SecurityIntegTestCase { null ); + private static long deleteRetentionPeriodDays; + + @BeforeClass + public static void randomDeleteRetentionPeriod() { + deleteRetentionPeriodDays = randomLongBetween(0, 7); + } + @Override public Settings nodeSettings(int nodeOrdinal, Settings otherSettings) { return Settings.builder() @@ -170,6 +179,7 @@ public Settings nodeSettings(int nodeOrdinal, Settings otherSettings) { .put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), true) .put(ApiKeyService.DELETE_INTERVAL.getKey(), TimeValue.timeValueMillis(DELETE_INTERVAL_MILLIS)) .put(ApiKeyService.DELETE_TIMEOUT.getKey(), TimeValue.timeValueSeconds(5L)) + .put(ApiKeyService.DELETE_RETENTION_PERIOD.getKey(), TimeValue.timeValueDays(deleteRetentionPeriodDays)) .put("xpack.security.crypto.thread_pool.queue_size", CRYPTO_THREAD_POOL_QUEUE_SIZE) .build(); } @@ -459,12 +469,48 @@ private void verifyInvalidateResponse( assertThat(invalidateResponse.getErrors().size(), equalTo(0)); } - public void testInvalidatedApiKeysDeletedByRemover() throws Exception { + public void testApiKeyRemover() throws Exception { + final String namePrefix = randomAlphaOfLength(10); + try { + if (deleteRetentionPeriodDays == 0) { + doTestInvalidKeysImmediatelyDeletedByRemover(namePrefix); + // Change the setting dynamically and test the other behaviour + deleteRetentionPeriodDays = randomIntBetween(1, 7); + setRetentionPeriod(false); + doTestDeletionBehaviorWhenKeysBecomeInvalidBeforeAndAfterRetentionPeriod("not-" + namePrefix); + } else { + doTestDeletionBehaviorWhenKeysBecomeInvalidBeforeAndAfterRetentionPeriod(namePrefix); + // Change the setting dynamically and test the other behaviour + deleteRetentionPeriodDays = 0; + setRetentionPeriod(false); + doTestInvalidKeysImmediatelyDeletedByRemover("not-" + namePrefix); + } + } finally { + setRetentionPeriod(true); + } + } + + private void setRetentionPeriod(boolean clear) { + final Settings.Builder builder = Settings.builder(); + if (clear) { + builder.putNull(ApiKeyService.DELETE_RETENTION_PERIOD.getKey()); + } else { + builder.put(ApiKeyService.DELETE_RETENTION_PERIOD.getKey(), TimeValue.timeValueDays(deleteRetentionPeriodDays)); + } + client().admin().cluster().prepareUpdateSettings().setPersistentSettings(builder).get(); + } + + private void doTestInvalidKeysImmediatelyDeletedByRemover(String namePrefix) throws Exception { + assertThat(deleteRetentionPeriodDays, equalTo(0L)); Client client = waitForExpiredApiKeysRemoverTriggerReadyAndGetClient().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING)) ); - List createdApiKeys = createApiKeys(2, null).v1(); + // Create a very short-lived key (1ms expiration) + createApiKeys(1, TimeValue.timeValueMillis(1)); + // Create keys that will not expire during this test + final CreateApiKeyResponse nonExpiringKey = createApiKeys(1, namePrefix, TimeValue.timeValueDays(1)).v1().get(0); + List createdApiKeys = createApiKeys(2, namePrefix, randomBoolean() ? TimeValue.timeValueDays(1) : null).v1(); PlainActionFuture listener = new PlainActionFuture<>(); client.execute( @@ -481,13 +527,21 @@ public void testInvalidatedApiKeysDeletedByRemover() throws Exception { refreshSecurityIndex(); PlainActionFuture getApiKeyResponseListener = new PlainActionFuture<>(); - client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.builder().realmName("file").build(), getApiKeyResponseListener); - Set expectedKeyIds = Sets.newHashSet(createdApiKeys.get(0).getId(), createdApiKeys.get(1).getId()); + client.execute( + GetApiKeyAction.INSTANCE, + GetApiKeyRequest.builder().apiKeyName(namePrefix + "*").build(), + getApiKeyResponseListener + ); + // The first API key with 1ms expiration should already be deleted + Set expectedKeyIds = Sets.newHashSet(nonExpiringKey.getId(), createdApiKeys.get(0).getId(), createdApiKeys.get(1).getId()); boolean apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover = false; for (ApiKey apiKey : getApiKeyResponseListener.get().getApiKeyInfos()) { assertThat(apiKey.getId(), is(in(expectedKeyIds))); - if (apiKey.getId().equals(createdApiKeys.get(0).getId())) { - // has been invalidated but not yet deleted by ExpiredApiKeysRemover + if (apiKey.getId().equals(nonExpiringKey.getId())) { + assertThat(apiKey.isInvalidated(), is(false)); + assertThat(apiKey.getExpiration(), notNullValue()); + } else if (apiKey.getId().equals(createdApiKeys.get(0).getId())) { + // has been invalidated but not yet deleted by InactiveApiKeysRemover assertThat(apiKey.isInvalidated(), is(true)); apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover = true; } else if (apiKey.getId().equals(createdApiKeys.get(1).getId())) { @@ -497,7 +551,7 @@ public void testInvalidatedApiKeysDeletedByRemover() throws Exception { } assertThat( getApiKeyResponseListener.get().getApiKeyInfos().length, - is((apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover) ? 2 : 1) + is((apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover) ? 3 : 2) ); client = waitForExpiredApiKeysRemoverTriggerReadyAndGetClient().filterWithHeader( @@ -517,22 +571,29 @@ public void testInvalidatedApiKeysDeletedByRemover() throws Exception { refreshSecurityIndex(); // Verify that 1st invalidated API key is deleted whereas the next one may be or may not be as it depends on whether update was - // indexed before ExpiredApiKeysRemover ran + // indexed before InactiveApiKeysRemover ran getApiKeyResponseListener = new PlainActionFuture<>(); - client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.builder().realmName("file").build(), getApiKeyResponseListener); - expectedKeyIds = Sets.newHashSet(createdApiKeys.get(1).getId()); + client.execute( + GetApiKeyAction.INSTANCE, + GetApiKeyRequest.builder().apiKeyName(namePrefix + "*").build(), + getApiKeyResponseListener + ); + expectedKeyIds = Sets.newHashSet(nonExpiringKey.getId(), createdApiKeys.get(1).getId()); apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover = false; for (ApiKey apiKey : getApiKeyResponseListener.get().getApiKeyInfos()) { assertThat(apiKey.getId(), is(in(expectedKeyIds))); - if (apiKey.getId().equals(createdApiKeys.get(1).getId())) { - // has been invalidated but not yet deleted by ExpiredApiKeysRemover + if (apiKey.getId().equals(nonExpiringKey.getId())) { + assertThat(apiKey.isInvalidated(), is(false)); + assertThat(apiKey.getExpiration(), notNullValue()); + } else if (apiKey.getId().equals(createdApiKeys.get(1).getId())) { + // has been invalidated but not yet deleted by InactiveApiKeysRemover assertThat(apiKey.isInvalidated(), is(true)); apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover = true; } } assertThat( getApiKeyResponseListener.get().getApiKeyInfos().length, - is((apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover) ? 1 : 0) + is((apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover) ? 2 : 1) ); } @@ -554,44 +615,99 @@ private Client waitForExpiredApiKeysRemoverTriggerReadyAndGetClient() throws Exc return internalCluster().client(nodeWithMostRecentRun); } - public void testExpiredApiKeysBehaviorWhenKeysExpired1WeekBeforeAnd1DayBefore() throws Exception { + private void doTestDeletionBehaviorWhenKeysBecomeInvalidBeforeAndAfterRetentionPeriod(String namePrefix) throws Exception { + assertThat(deleteRetentionPeriodDays, greaterThan(0L)); Client client = waitForExpiredApiKeysRemoverTriggerReadyAndGetClient().filterWithHeader( Collections.singletonMap("Authorization", basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING)) ); - int noOfKeys = 4; - List createdApiKeys = createApiKeys(noOfKeys, null).v1(); + int noOfKeys = 9; + List createdApiKeys = createApiKeys(noOfKeys, namePrefix, null).v1(); Instant created = Instant.now(); PlainActionFuture getApiKeyResponseListener = new PlainActionFuture<>(); - client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.builder().realmName("file").build(), getApiKeyResponseListener); + client.execute( + GetApiKeyAction.INSTANCE, + GetApiKeyRequest.builder().apiKeyName(namePrefix + "*").build(), + getApiKeyResponseListener + ); assertThat(getApiKeyResponseListener.get().getApiKeyInfos().length, is(noOfKeys)); // Expire the 1st key such that it cannot be deleted by the remover - // hack doc to modify the expiration time to a day before - Instant dayBefore = created.minus(1L, ChronoUnit.DAYS); - assertTrue(Instant.now().isAfter(dayBefore)); + // hack doc to modify the expiration time + Instant withinRetention = created.minus(deleteRetentionPeriodDays - 1, ChronoUnit.DAYS); + assertFalse(created.isBefore(withinRetention)); UpdateResponse expirationDateUpdatedResponse = client.prepareUpdate(SECURITY_MAIN_ALIAS, createdApiKeys.get(0).getId()) - .setDoc("expiration_time", dayBefore.toEpochMilli()) + .setDoc("expiration_time", withinRetention.toEpochMilli()) .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) .get(); assertThat(expirationDateUpdatedResponse.getResult(), is(DocWriteResponse.Result.UPDATED)); // Expire the 2nd key such that it can be deleted by the remover - // hack doc to modify the expiration time to the week before - Instant weekBefore = created.minus(8L, ChronoUnit.DAYS); - assertTrue(Instant.now().isAfter(weekBefore)); + // hack doc to modify the expiration time + Instant outsideRetention = created.minus(deleteRetentionPeriodDays + 1, ChronoUnit.DAYS); + assertTrue(Instant.now().isAfter(outsideRetention)); expirationDateUpdatedResponse = client.prepareUpdate(SECURITY_MAIN_ALIAS, createdApiKeys.get(1).getId()) - .setDoc("expiration_time", weekBefore.toEpochMilli()) + .setDoc("expiration_time", outsideRetention.toEpochMilli()) .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) .get(); assertThat(expirationDateUpdatedResponse.getResult(), is(DocWriteResponse.Result.UPDATED)); + // Invalidate the 3rd key such that it cannot be deleted by the remover + UpdateResponse invalidateUpdateResponse = client.prepareUpdate(SECURITY_MAIN_ALIAS, createdApiKeys.get(2).getId()) + .setDoc("invalidation_time", withinRetention.toEpochMilli(), "api_key_invalidated", true) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .get(); + assertThat(invalidateUpdateResponse.getResult(), is(DocWriteResponse.Result.UPDATED)); + + // Invalidate the 4th key such that it will be deleted by the remover + invalidateUpdateResponse = client.prepareUpdate(SECURITY_MAIN_ALIAS, createdApiKeys.get(3).getId()) + .setDoc("invalidation_time", outsideRetention.toEpochMilli(), "api_key_invalidated", true) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .get(); + assertThat(invalidateUpdateResponse.getResult(), is(DocWriteResponse.Result.UPDATED)); + + // 5th key will be deleted because its expiration is outside of retention even though its invalidation time is not + UpdateResponse updateResponse = client.prepareUpdate(SECURITY_MAIN_ALIAS, createdApiKeys.get(4).getId()) + .setDoc( + "expiration_time", + outsideRetention.toEpochMilli(), + "invalidation_time", + withinRetention.toEpochMilli(), + "api_key_invalidated", + true + ) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .get(); + assertThat(updateResponse.getResult(), is(DocWriteResponse.Result.UPDATED)); + + // 6th key will be deleted because its invalidation time is outside of retention even though its expiration is not + updateResponse = client.prepareUpdate(SECURITY_MAIN_ALIAS, createdApiKeys.get(5).getId()) + .setDoc( + "expiration_time", + withinRetention.toEpochMilli(), + "invalidation_time", + outsideRetention.toEpochMilli(), + "api_key_invalidated", + true + ) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .get(); + assertThat(updateResponse.getResult(), is(DocWriteResponse.Result.UPDATED)); + + // 7th key will be deleted because it has old style invalidation (no invalidation time) + // It does not matter whether it has an expiration time or whether the expiration time is still within retention period + updateResponse = client.prepareUpdate(SECURITY_MAIN_ALIAS, createdApiKeys.get(6).getId()) + .setDoc("api_key_invalidated", true, "expiration_time", randomBoolean() ? withinRetention.toEpochMilli() : null) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .get(); + assertThat(updateResponse.getResult(), is(DocWriteResponse.Result.UPDATED)); + // Invalidate to trigger the remover PlainActionFuture listener = new PlainActionFuture<>(); client.execute( InvalidateApiKeyAction.INSTANCE, - InvalidateApiKeyRequest.usingApiKeyId(createdApiKeys.get(2).getId(), false), + InvalidateApiKeyRequest.usingApiKeyId(createdApiKeys.get(7).getId(), false), listener ); assertThat(listener.get().getInvalidatedApiKeys().size(), is(1)); @@ -600,16 +716,20 @@ public void testExpiredApiKeysBehaviorWhenKeysExpired1WeekBeforeAnd1DayBefore() refreshSecurityIndex(); - // Verify get API keys does not return api keys deleted by ExpiredApiKeysRemover + // Verify get API keys does not return api keys deleted by InactiveApiKeysRemover getApiKeyResponseListener = new PlainActionFuture<>(); - client.execute(GetApiKeyAction.INSTANCE, GetApiKeyRequest.builder().realmName("file").build(), getApiKeyResponseListener); + client.execute( + GetApiKeyAction.INSTANCE, + GetApiKeyRequest.builder().apiKeyName(namePrefix + "*").build(), + getApiKeyResponseListener + ); Set expectedKeyIds = Sets.newHashSet( createdApiKeys.get(0).getId(), createdApiKeys.get(2).getId(), - createdApiKeys.get(3).getId() + createdApiKeys.get(7).getId(), + createdApiKeys.get(8).getId() ); - boolean apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover = false; for (ApiKey apiKey : getApiKeyResponseListener.get().getApiKeyInfos()) { assertThat(apiKey.getId(), is(in(expectedKeyIds))); if (apiKey.getId().equals(createdApiKeys.get(0).getId())) { @@ -617,11 +737,14 @@ public void testExpiredApiKeysBehaviorWhenKeysExpired1WeekBeforeAnd1DayBefore() assertTrue(apiKey.getExpiration().isBefore(Instant.now())); assertThat(apiKey.isInvalidated(), is(false)); } else if (apiKey.getId().equals(createdApiKeys.get(2).getId())) { - // has not been expired as no expiration, is invalidated but not yet deleted by ExpiredApiKeysRemover + // has been invalidated, not expired + assertThat(apiKey.getExpiration(), nullValue()); + assertThat(apiKey.isInvalidated(), is(true)); + } else if (apiKey.getId().equals(createdApiKeys.get(7).getId())) { + // has not been expired as no expiration, is invalidated but not yet deleted by InactiveApiKeysRemover assertThat(apiKey.getExpiration(), is(nullValue())); assertThat(apiKey.isInvalidated(), is(true)); - apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover = true; - } else if (apiKey.getId().equals(createdApiKeys.get(3).getId())) { + } else if (apiKey.getId().equals(createdApiKeys.get(8).getId())) { // has not been expired as no expiration, not invalidated assertThat(apiKey.getExpiration(), is(nullValue())); assertThat(apiKey.isInvalidated(), is(false)); @@ -629,10 +752,7 @@ public void testExpiredApiKeysBehaviorWhenKeysExpired1WeekBeforeAnd1DayBefore() fail("unexpected API key " + apiKey); } } - assertThat( - getApiKeyResponseListener.get().getApiKeyInfos().length, - is((apiKeyInvalidatedButNotYetDeletedByExpiredApiKeysRemover) ? 3 : 2) - ); + assertThat(getApiKeyResponseListener.get().getApiKeyInfos().length, is(4)); } private void refreshSecurityIndex() throws Exception { @@ -2894,6 +3014,18 @@ private Tuple, List>> createApiKe return createApiKeys(ES_TEST_ROOT_USER, noOfApiKeys, expiration, DEFAULT_API_KEY_ROLE_DESCRIPTOR.getClusterPrivileges()); } + private Tuple, List>> createApiKeys( + int noOfApiKeys, + String namePrefix, + TimeValue expiration + ) { + final Map headers = Collections.singletonMap( + "Authorization", + basicAuthHeaderValue(ES_TEST_ROOT_USER, TEST_PASSWORD_SECURE_STRING) + ); + return createApiKeys(headers, noOfApiKeys, namePrefix, expiration, DEFAULT_API_KEY_ROLE_DESCRIPTOR.getClusterPrivileges()); + } + private Tuple, List>> createApiKeys( String user, int noOfApiKeys, diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index d8d3cf61c041..986c877dcafe 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -1108,6 +1108,7 @@ public static List> getSettings(List securityExten settingsList.add(ApiKeyService.PASSWORD_HASHING_ALGORITHM); settingsList.add(ApiKeyService.DELETE_TIMEOUT); settingsList.add(ApiKeyService.DELETE_INTERVAL); + settingsList.add(ApiKeyService.DELETE_RETENTION_PERIOD); settingsList.add(ApiKeyService.CACHE_HASH_ALGO_SETTING); settingsList.add(ApiKeyService.CACHE_MAX_KEYS_SETTING); settingsList.add(ApiKeyService.CACHE_TTL_SETTING); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 4fda6d853229..805922e48138 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -163,6 +163,12 @@ public class ApiKeyService { TimeValue.timeValueHours(24L), Property.NodeScope ); + public static final Setting DELETE_RETENTION_PERIOD = Setting.positiveTimeSetting( + "xpack.security.authc.api_key.delete.retention_period", + TimeValue.timeValueDays(7), + Property.NodeScope, + Property.Dynamic + ); public static final Setting CACHE_HASH_ALGO_SETTING = Setting.simpleString( "xpack.security.authc.api_key.cache.hash_algo", "ssha256", @@ -193,7 +199,7 @@ public class ApiKeyService { private final Hasher hasher; private final boolean enabled; private final Settings settings; - private final ExpiredApiKeysRemover expiredApiKeysRemover; + private final InactiveApiKeysRemover inactiveApiKeysRemover; private final TimeValue deleteInterval; private final Cache> apiKeyAuthCache; private final Hasher cacheHasher; @@ -225,7 +231,7 @@ public ApiKeyService( this.hasher = Hasher.resolve(PASSWORD_HASHING_ALGORITHM.get(settings)); this.settings = settings; this.deleteInterval = DELETE_INTERVAL.get(settings); - this.expiredApiKeysRemover = new ExpiredApiKeysRemover(settings, client); + this.inactiveApiKeysRemover = new InactiveApiKeysRemover(settings, client, clusterService); this.threadPool = threadPool; this.cacheHasher = Hasher.resolve(CACHE_HASH_ALGO_SETTING.get(settings)); final TimeValue ttl = CACHE_TTL_SETTING.get(settings); @@ -1554,7 +1560,7 @@ private static E traceLog(String action, E exception) { // pkg scoped for testing boolean isExpirationInProgress() { - return expiredApiKeysRemover.isExpirationInProgress(); + return inactiveApiKeysRemover.isExpirationInProgress(); } // pkg scoped for testing @@ -1565,7 +1571,7 @@ long lastTimeWhenApiKeysRemoverWasTriggered() { private void maybeStartApiKeyRemover() { if (securityIndex.isAvailable()) { if (client.threadPool().relativeTimeInMillis() - lastExpirationRunMs > deleteInterval.getMillis()) { - expiredApiKeysRemover.submit(client.threadPool()); + inactiveApiKeysRemover.submit(client.threadPool()); lastExpirationRunMs = client.threadPool().relativeTimeInMillis(); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ExpiredApiKeysRemover.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InactiveApiKeysRemover.java similarity index 77% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ExpiredApiKeysRemover.java rename to x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InactiveApiKeysRemover.java index bec3e94a3c73..1d34371f6186 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ExpiredApiKeysRemover.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InactiveApiKeysRemover.java @@ -12,6 +12,7 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.bulk.BulkItemResponse; import org.elasticsearch.client.internal.Client; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.AbstractRunnable; import org.elasticsearch.core.TimeValue; @@ -24,9 +25,9 @@ import org.elasticsearch.threadpool.ThreadPool.Names; import org.elasticsearch.xpack.security.support.SecuritySystemIndices; -import java.time.Duration; import java.time.Instant; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; import static org.elasticsearch.action.support.TransportActions.isShardNotAvailableException; import static org.elasticsearch.core.Strings.format; @@ -36,18 +37,23 @@ /** * Responsible for cleaning the invalidated and expired API keys from the security index. */ -public final class ExpiredApiKeysRemover extends AbstractRunnable { - public static final Duration EXPIRED_API_KEYS_RETENTION_PERIOD = Duration.ofDays(7L); - - private static final Logger logger = LogManager.getLogger(ExpiredApiKeysRemover.class); +public final class InactiveApiKeysRemover extends AbstractRunnable { + private static final Logger logger = LogManager.getLogger(InactiveApiKeysRemover.class); private final Client client; private final AtomicBoolean inProgress = new AtomicBoolean(false); private final TimeValue timeout; + private final AtomicLong retentionPeriodInMs; - ExpiredApiKeysRemover(Settings settings, Client client) { + InactiveApiKeysRemover(Settings settings, Client client, ClusterService clusterService) { this.client = client; this.timeout = ApiKeyService.DELETE_TIMEOUT.get(settings); + this.retentionPeriodInMs = new AtomicLong(ApiKeyService.DELETE_RETENTION_PERIOD.get(settings).getMillis()); + clusterService.getClusterSettings() + .addSettingsUpdateConsumer( + ApiKeyService.DELETE_RETENTION_PERIOD, + newRetentionPeriod -> this.retentionPeriodInMs.set(newRetentionPeriod.getMillis()) + ); } @Override @@ -58,11 +64,18 @@ public void doRun() { expiredDbq.getSearchRequest().source().timeout(timeout); } final Instant now = Instant.now(); + final long cutoffTimestamp = now.minusMillis(retentionPeriodInMs.get()).toEpochMilli(); expiredDbq.setQuery( QueryBuilders.boolQuery() .filter(QueryBuilders.termsQuery("doc_type", "api_key")) - .should(QueryBuilders.termsQuery("api_key_invalidated", true)) - .should(QueryBuilders.rangeQuery("expiration_time").lte(now.minus(EXPIRED_API_KEYS_RETENTION_PERIOD).toEpochMilli())) + .should(QueryBuilders.rangeQuery("expiration_time").lte(cutoffTimestamp)) + .should( + QueryBuilders.boolQuery() + .filter(QueryBuilders.termsQuery("api_key_invalidated", true)) + .should(QueryBuilders.rangeQuery("invalidation_time").lte(cutoffTimestamp)) + .should(QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery("invalidation_time"))) + .minimumShouldMatch(1) + ) .minimumShouldMatch(1) ); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java index c4cb5e615c0c..881432bced14 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java @@ -2869,6 +2869,7 @@ private ClusterSettings mockClusterSettings() { final List> settingsList = new ArrayList<>(); LoggingAuditTrail.registerSettings(settingsList); settingsList.addAll(ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); + settingsList.add(ApiKeyService.DELETE_RETENTION_PERIOD); return new ClusterSettings(settings, new HashSet<>(settingsList)); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java index fcd6601360a9..b0ff033f4ce7 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java @@ -320,7 +320,8 @@ public void init() throws Exception { LoggingAuditTrail.FILTER_POLICY_IGNORE_ROLES, LoggingAuditTrail.FILTER_POLICY_IGNORE_INDICES, LoggingAuditTrail.FILTER_POLICY_IGNORE_ACTIONS, - Loggers.LOG_LEVEL_SETTING + Loggers.LOG_LEVEL_SETTING, + ApiKeyService.DELETE_RETENTION_PERIOD ) ); when(clusterService.getClusterSettings()).thenReturn(clusterSettings); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index b7a1cfd05d5a..b4aea7984fe3 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -40,12 +40,14 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.cache.Cache; import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.AbstractRunnable; import org.elasticsearch.common.util.concurrent.EsRejectedExecutionException; import org.elasticsearch.common.util.concurrent.ListenableFuture; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.core.Nullable; @@ -2119,12 +2121,16 @@ private ApiKeyService createApiKeyService(Settings baseSettings) { .put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), true) .put(baseSettings) .build(); + final ClusterSettings clusterSettings = new ClusterSettings( + settings, + Sets.union(ClusterSettings.BUILT_IN_CLUSTER_SETTINGS, Set.of(ApiKeyService.DELETE_RETENTION_PERIOD)) + ); final ApiKeyService service = new ApiKeyService( settings, clock, client, securityIndex, - ClusterServiceUtils.createClusterService(threadPool), + ClusterServiceUtils.createClusterService(threadPool, clusterSettings), cacheInvalidatorRegistry, threadPool ); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index ac604fb7a5ea..6d5eaf3ef2df 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -37,9 +37,11 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.Tuple; import org.elasticsearch.env.Environment; @@ -304,7 +306,11 @@ public void init() throws Exception { runnable.run(); return null; }).when(securityIndex).checkIndexVersionThenExecute(anyConsumer(), any(Runnable.class)); - ClusterService clusterService = ClusterServiceUtils.createClusterService(threadPool); + final ClusterSettings clusterSettings = new ClusterSettings( + settings, + Sets.union(ClusterSettings.BUILT_IN_CLUSTER_SETTINGS, Set.of(ApiKeyService.DELETE_RETENTION_PERIOD)) + ); + ClusterService clusterService = ClusterServiceUtils.createClusterService(threadPool, clusterSettings); final SecurityContext securityContext = new SecurityContext(settings, threadContext); apiKeyService = new ApiKeyService( settings, diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java index 8b80112d9caf..7fbcef61af07 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/SecondaryAuthenticatorTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; @@ -64,6 +65,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; @@ -122,6 +124,7 @@ public void setupMocks() throws Exception { final ClusterService clusterService = mock(ClusterService.class); final ClusterState clusterState = ClusterState.EMPTY_STATE; when(clusterService.state()).thenReturn(clusterState); + when(clusterService.getClusterSettings()).thenReturn(new ClusterSettings(settings, Set.of(ApiKeyService.DELETE_RETENTION_PERIOD))); securityContext = new SecurityContext(settings, threadContext); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index 0df955e901ca..c56e018cafb1 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -33,6 +33,7 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.set.Sets; @@ -1806,13 +1807,17 @@ public void testApiKeyAuthUsesApiKeyService() throws Exception { }).when(nativeRolesStore).getRoleDescriptors(isASet(), anyActionListener()); final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore()); ThreadContext threadContext = new ThreadContext(SECURITY_ENABLED_SETTINGS); + final ClusterService clusterService = mock(ClusterService.class); + when(clusterService.getClusterSettings()).thenReturn( + new ClusterSettings(SECURITY_ENABLED_SETTINGS, Set.of(ApiKeyService.DELETE_RETENTION_PERIOD)) + ); ApiKeyService apiKeyService = spy( new ApiKeyService( SECURITY_ENABLED_SETTINGS, Clock.systemUTC(), mock(Client.class), mock(SecurityIndexManager.class), - mock(ClusterService.class), + clusterService, mock(CacheInvalidatorRegistry.class), mock(ThreadPool.class) ) @@ -1882,13 +1887,17 @@ public void testApiKeyAuthUsesApiKeyServiceWithScopedRole() throws Exception { final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore()); ThreadContext threadContext = new ThreadContext(SECURITY_ENABLED_SETTINGS); + final ClusterService clusterService = mock(ClusterService.class); + when(clusterService.getClusterSettings()).thenReturn( + new ClusterSettings(SECURITY_ENABLED_SETTINGS, Set.of(ApiKeyService.DELETE_RETENTION_PERIOD)) + ); ApiKeyService apiKeyService = spy( new ApiKeyService( SECURITY_ENABLED_SETTINGS, Clock.systemUTC(), mock(Client.class), mock(SecurityIndexManager.class), - mock(ClusterService.class), + clusterService, mock(CacheInvalidatorRegistry.class), mock(ThreadPool.class) ) From e023213b073365bcbd9a53284fef30f9957cb40f Mon Sep 17 00:00:00 2001 From: Pooya Salehi Date: Fri, 16 Dec 2022 09:33:48 +0100 Subject: [PATCH 288/919] Use smaller snapshot threadpool size on small nodes (#92392) While on larger data nodes (>= 750MB max heap), larger snapshot threadpool size improves snapshotting on high latency blob stores (#90282), smaller instances can run into OOM issues and need a smaller snapshot threadpool size. Here, for these small nodes we revert back to the pool size pre-8.6. Closes #92410 --- .../elasticsearch/threadpool/ThreadPool.java | 15 +++++++++++++- .../threadpool/ThreadPoolTests.java | 20 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/threadpool/ThreadPool.java b/server/src/main/java/org/elasticsearch/threadpool/ThreadPool.java index fa54cd01c593..017ba1bae822 100644 --- a/server/src/main/java/org/elasticsearch/threadpool/ThreadPool.java +++ b/server/src/main/java/org/elasticsearch/threadpool/ThreadPool.java @@ -15,6 +15,8 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.ByteSizeUnit; +import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.SizeValue; import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.common.util.concurrent.EsRejectedExecutionException; @@ -206,7 +208,9 @@ public ThreadPool(final Settings settings, final ExecutorBuilder... customBui builders.put(Names.FLUSH, new ScalingExecutorBuilder(Names.FLUSH, 1, halfProcMaxAt5, TimeValue.timeValueMinutes(5), false)); builders.put(Names.REFRESH, new ScalingExecutorBuilder(Names.REFRESH, 1, halfProcMaxAt10, TimeValue.timeValueMinutes(5), false)); builders.put(Names.WARMER, new ScalingExecutorBuilder(Names.WARMER, 1, halfProcMaxAt5, TimeValue.timeValueMinutes(5), false)); - builders.put(Names.SNAPSHOT, new ScalingExecutorBuilder(Names.SNAPSHOT, 1, 10, TimeValue.timeValueMinutes(5), false)); + ByteSizeValue maxHeapSize = ByteSizeValue.ofBytes(Runtime.getRuntime().maxMemory()); + final int maxSnapshotCores = getMaxSnapshotCores(allocatedProcessors, maxHeapSize); + builders.put(Names.SNAPSHOT, new ScalingExecutorBuilder(Names.SNAPSHOT, 1, maxSnapshotCores, TimeValue.timeValueMinutes(5), false)); builders.put( Names.SNAPSHOT_META, new ScalingExecutorBuilder( @@ -564,6 +568,15 @@ public static int searchOrGetThreadPoolSize(final int allocatedProcessors) { return ((allocatedProcessors * 3) / 2) + 1; } + static int getMaxSnapshotCores(int allocatedProcessors, final ByteSizeValue maxHeapSize) { + // While on larger data nodes, larger snapshot threadpool size improves snapshotting on high latency blob stores, + // smaller instances can run into OOM issues and need a smaller snapshot threadpool size. + if (maxHeapSize.compareTo(new ByteSizeValue(750, ByteSizeUnit.MB)) < 0) { + return halfAllocatedProcessorsMaxFive(allocatedProcessors); + } + return 10; + } + static class ThreadedRunnable implements Runnable { private final Runnable runnable; diff --git a/server/src/test/java/org/elasticsearch/threadpool/ThreadPoolTests.java b/server/src/test/java/org/elasticsearch/threadpool/ThreadPoolTests.java index d22538e986cc..2c01cfd8daa8 100644 --- a/server/src/test/java/org/elasticsearch/threadpool/ThreadPoolTests.java +++ b/server/src/test/java/org/elasticsearch/threadpool/ThreadPoolTests.java @@ -13,6 +13,7 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.common.util.concurrent.FutureUtils; import org.elasticsearch.core.TimeValue; @@ -25,6 +26,8 @@ import static org.elasticsearch.threadpool.ThreadPool.ESTIMATED_TIME_INTERVAL_SETTING; import static org.elasticsearch.threadpool.ThreadPool.LATE_TIME_INTERVAL_WARN_THRESHOLD_SETTING; import static org.elasticsearch.threadpool.ThreadPool.assertCurrentMethodIsNotCalledRecursively; +import static org.elasticsearch.threadpool.ThreadPool.getMaxSnapshotCores; +import static org.elasticsearch.threadpool.ThreadPool.halfAllocatedProcessorsMaxFive; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.greaterThan; @@ -328,4 +331,21 @@ public void testForceMergeThreadPoolSize() { assertTrue(terminate(threadPool)); } } + + public void testGetMaxSnapshotCores() { + int allocatedProcessors = randomIntBetween(1, 16); + assertThat( + getMaxSnapshotCores(allocatedProcessors, ByteSizeValue.ofMb(400)), + equalTo(halfAllocatedProcessorsMaxFive(allocatedProcessors)) + ); + allocatedProcessors = randomIntBetween(1, 16); + assertThat( + getMaxSnapshotCores(allocatedProcessors, ByteSizeValue.ofMb(749)), + equalTo(halfAllocatedProcessorsMaxFive(allocatedProcessors)) + ); + allocatedProcessors = randomIntBetween(1, 16); + assertThat(getMaxSnapshotCores(allocatedProcessors, ByteSizeValue.ofMb(750)), equalTo(10)); + allocatedProcessors = randomIntBetween(1, 16); + assertThat(getMaxSnapshotCores(allocatedProcessors, ByteSizeValue.ofGb(4)), equalTo(10)); + } } From fd4b2c9491a2952085473708d4ea4a6872f457a5 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Fri, 16 Dec 2022 10:16:17 +0100 Subject: [PATCH 289/919] Fix stackoverflow in AggregationProfileShardResult.toString (#92397) This would obviously recurse until SO -> since it's a toXContent we can use the string utility to get a reasonable string should this ever be used. --- .../aggregation/AggregationProfileShardResult.java | 4 ++-- .../aggregation/AggregationProfileShardResultTests.java | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/profile/aggregation/AggregationProfileShardResult.java b/server/src/main/java/org/elasticsearch/search/profile/aggregation/AggregationProfileShardResult.java index 685ebdcb6d66..a49231780f54 100644 --- a/server/src/main/java/org/elasticsearch/search/profile/aggregation/AggregationProfileShardResult.java +++ b/server/src/main/java/org/elasticsearch/search/profile/aggregation/AggregationProfileShardResult.java @@ -8,6 +8,7 @@ package org.elasticsearch.search.profile.aggregation; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; @@ -20,7 +21,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Objects; import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; @@ -83,7 +83,7 @@ public int hashCode() { @Override public String toString() { - return Objects.toString(this); + return Strings.toString(this); } public static AggregationProfileShardResult fromXContent(XContentParser parser) throws IOException { diff --git a/server/src/test/java/org/elasticsearch/search/profile/aggregation/AggregationProfileShardResultTests.java b/server/src/test/java/org/elasticsearch/search/profile/aggregation/AggregationProfileShardResultTests.java index 531405344abb..967ec7231f43 100644 --- a/server/src/test/java/org/elasticsearch/search/profile/aggregation/AggregationProfileShardResultTests.java +++ b/server/src/test/java/org/elasticsearch/search/profile/aggregation/AggregationProfileShardResultTests.java @@ -27,6 +27,8 @@ import java.util.function.Predicate; import static org.elasticsearch.common.xcontent.XContentHelper.toXContent; +import static org.hamcrest.Matchers.emptyString; +import static org.hamcrest.Matchers.not; public class AggregationProfileShardResultTests extends AbstractXContentSerializingTestCase { @@ -118,4 +120,10 @@ public void testToXContent() throws IOException { }"""), xContent.utf8ToString()); } + public void testToString() { + final String toString = createTestInstance().toString(); + assertNotNull(toString); + assertThat(toString, not(emptyString())); + } + } From 93cf40b19e3cc832baad40e6ee4e52221b2cc5b6 Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Fri, 16 Dec 2022 10:41:49 +0100 Subject: [PATCH 290/919] Expose EngineConfig.getCodecService (#92413) It helps to carry over the CodeService when an EngineConfig is copied. --- .../java/org/elasticsearch/index/engine/EngineConfig.java | 7 +++++++ .../org/elasticsearch/index/engine/EngineTestCase.java | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java b/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java index b7110a64fa99..6963192f267e 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java +++ b/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java @@ -245,6 +245,13 @@ public Codec getCodec() { return codecService.codec(codecName); } + /** + * @return the {@link CodecService} + */ + public CodecService getCodecService() { + return codecService; + } + /** * Returns a thread-pool mainly used to get estimated time stamps from * {@link org.elasticsearch.threadpool.ThreadPool#relativeTimeInMillis()} and to schedule diff --git a/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java index 353b8009152f..1333073a63e4 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java @@ -259,7 +259,7 @@ public EngineConfig copy(EngineConfig config, LongSupplier globalCheckpointSuppl config.getMergePolicy(), config.getAnalyzer(), config.getSimilarity(), - newCodecService(), + config.getCodecService(), config.getEventListener(), config.getQueryCache(), config.getQueryCachingPolicy(), @@ -290,7 +290,7 @@ public EngineConfig copy(EngineConfig config, Analyzer analyzer) { config.getMergePolicy(), analyzer, config.getSimilarity(), - newCodecService(), + config.getCodecService(), config.getEventListener(), config.getQueryCache(), config.getQueryCachingPolicy(), @@ -321,7 +321,7 @@ public EngineConfig copy(EngineConfig config, MergePolicy mergePolicy) { mergePolicy, config.getAnalyzer(), config.getSimilarity(), - newCodecService(), + config.getCodecService(), config.getEventListener(), config.getQueryCache(), config.getQueryCachingPolicy(), From 311d8cfc74fa9ef0fb1178aba7124cc1176f39b6 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Fri, 16 Dec 2022 12:20:50 +0200 Subject: [PATCH 291/919] Always handle unavailable indices inside `IndexNameExpressionResolver#resolveExpressions` (#91908) When evaluating an expression with `ignoreUnavailable=false`, it is possible to get an "index not found exception" in the following cases: * the resource does not exist * the resource is an alias, but the request options tell to ignore aliases * the resource is a datastream, but the request type doesn't work on datastreams These are already MOSTLY covered by `IndexNameExpressionResolver#resolve` method. The only case when they're not handled is when wildcards are not expanded. This PR corrects that. At a high-level, I'm working to move the handling of the ignore unavailable checks mentioned above to a single place in the code. This PR is step 1. The checks are inside the `resolveExpressions` method, but there're still in two places. A follow-up will coalesce those. --- .../action/bulk/TransportBulkAction.java | 17 +- .../metadata/IndexNameExpressionResolver.java | 11 +- .../IndexNameExpressionResolverTests.java | 332 ++++++++++++++++-- .../WildcardExpressionResolverTests.java | 64 +++- 4 files changed, 383 insertions(+), 41 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java b/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java index 94df5bda48c4..7bdeb5e4b199 100644 --- a/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java +++ b/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java @@ -80,6 +80,7 @@ import java.util.function.LongSupplier; import java.util.stream.Collectors; +import static org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.EXCLUDED_DATA_STREAMS_KEY; import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_PRIMARY_TERM; import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_SEQ_NO; @@ -754,10 +755,18 @@ private static class ConcreteIndices { } IndexAbstraction resolveIfAbsent(DocWriteRequest request) { - return indexAbstractions.computeIfAbsent( - request.index(), - key -> indexNameExpressionResolver.resolveWriteIndexAbstraction(state, request) - ); + try { + return indexAbstractions.computeIfAbsent( + request.index(), + key -> indexNameExpressionResolver.resolveWriteIndexAbstraction(state, request) + ); + } catch (IndexNotFoundException e) { + if (e.getMetadataKeys().contains(EXCLUDED_DATA_STREAMS_KEY)) { + throw new IllegalArgumentException("only write ops with an op_type of create are allowed in data streams", e); + } else { + throw e; + } + } } IndexRouting routing(Index index) { diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java index 16a5c2419d54..b4e54f8cf152 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java @@ -219,9 +219,6 @@ public IndexAbstraction resolveWriteIndexAbstraction(ClusterState state, DocWrit if (expressions.size() == 1) { IndexAbstraction ia = state.metadata().getIndicesLookup().get(expressions.iterator().next()); - if (ia == null) { - throw new IndexNotFoundException(expressions.iterator().next()); - } if (ia.getType() == Type.ALIAS) { Index writeIndex = ia.getWriteIndex(); if (writeIndex == null) { @@ -325,9 +322,6 @@ Index[] concreteIndices(Context context, String... indexExpressions) { final Set concreteIndicesResult = Sets.newLinkedHashSetWithExpectedSize(expressions.size()); final Map indicesLookup = context.getState().metadata().getIndicesLookup(); for (String expression : expressions) { - if (context.getOptions().ignoreUnavailable() == false) { - ensureAliasOrIndexExists(context, expression); - } final IndexAbstraction indexAbstraction = indicesLookup.get(expression); if (indexAbstraction == null) { continue; @@ -1122,6 +1116,11 @@ public static Collection resolve(Context context, List expressio if (expressions.size() == 1 && expressions.get(0).equals(Metadata.ALL)) { return List.of(); } else { + if (context.getOptions().ignoreUnavailable() == false) { + for (String expression : expressions) { + ensureAliasOrIndexExists(context, expression); + } + } return expressions; } } else if (isEmptyOrTrivialWildcard(expressions)) { diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolverTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolverTests.java index d08166eaf411..0e7164c4ad49 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolverTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolverTests.java @@ -753,11 +753,43 @@ public void testIndexOptionsEmptyCluster() { IndicesOptions.fromOptions(true, false, true, false), SystemIndexAccessLevel.NONE ); - IndexNotFoundException infe = expectThrows( - IndexNotFoundException.class, - () -> indexNameExpressionResolver.concreteIndexNames(context3, Strings.EMPTY_ARRAY) + { + IndexNotFoundException infe = expectThrows( + IndexNotFoundException.class, + () -> indexNameExpressionResolver.concreteIndexNames(context3, Strings.EMPTY_ARRAY) + ); + assertThat(infe.getResourceId().toString(), equalTo("[_all]")); + } + + // no wildcard expand + final IndexNameExpressionResolver.Context context4 = new IndexNameExpressionResolver.Context( + state, + IndicesOptions.fromOptions(false, true, false, false), + randomFrom(SystemIndexAccessLevel.values()) ); - assertThat(infe.getResourceId().toString(), equalTo("[_all]")); + results = indexNameExpressionResolver.concreteIndexNames(context4, Strings.EMPTY_ARRAY); + assertThat(results, emptyArray()); + { + IndexNotFoundException infe = expectThrows( + IndexNotFoundException.class, + () -> indexNameExpressionResolver.concreteIndexNames(context4, "foo") + ); + assertThat(infe.getIndex().getName(), equalTo("foo")); + } + { + IndexNotFoundException infe = expectThrows( + IndexNotFoundException.class, + () -> indexNameExpressionResolver.concreteIndexNames(context4, "foo*") + ); + assertThat(infe.getIndex().getName(), equalTo("foo*")); + } + { + IndexNotFoundException infe = expectThrows( + IndexNotFoundException.class, + () -> indexNameExpressionResolver.concreteIndexNames(context4, "bar", "foo*") + ); + assertThat(infe.getIndex().getName(), equalTo("bar")); + } } public void testConcreteIndicesIgnoreIndicesOneMissingIndex() { @@ -768,12 +800,25 @@ public void testConcreteIndicesIgnoreIndicesOneMissingIndex() { IndicesOptions.strictExpandOpen(), SystemIndexAccessLevel.NONE ); - IndexNotFoundException infe = expectThrows( IndexNotFoundException.class, () -> indexNameExpressionResolver.concreteIndexNames(context, "testZZZ") ); assertThat(infe.getMessage(), is("no such index [testZZZ]")); + // same as above, but DO NOT expand wildcards + IndexNameExpressionResolver.Context context_no_expand = new IndexNameExpressionResolver.Context( + state, + new IndicesOptions( + EnumSet.of(IndicesOptions.Option.ALLOW_NO_INDICES), + randomFrom(EnumSet.noneOf(IndicesOptions.WildcardStates.class), EnumSet.of(IndicesOptions.WildcardStates.HIDDEN)) + ), + randomFrom(SystemIndexAccessLevel.values()) + ); + IndexNotFoundException infe_no_expand = expectThrows( + IndexNotFoundException.class, + () -> indexNameExpressionResolver.concreteIndexNames(context_no_expand, "testZZZ") + ); + assertThat(infe_no_expand.getMessage(), is("no such index [testZZZ]")); } public void testConcreteIndicesIgnoreIndicesOneMissingIndexOtherFound() { @@ -805,6 +850,20 @@ public void testConcreteIndicesIgnoreIndicesAllMissing() { () -> indexNameExpressionResolver.concreteIndexNames(context, "testMo", "testMahdy") ); assertThat(infe.getMessage(), is("no such index [testMo]")); + // same as above, but DO NOT expand wildcards + IndexNameExpressionResolver.Context context_no_expand = new IndexNameExpressionResolver.Context( + state, + new IndicesOptions( + EnumSet.of(IndicesOptions.Option.ALLOW_NO_INDICES), + randomFrom(EnumSet.noneOf(IndicesOptions.WildcardStates.class), EnumSet.of(IndicesOptions.WildcardStates.HIDDEN)) + ), + randomFrom(SystemIndexAccessLevel.values()) + ); + IndexNotFoundException infe_no_expand = expectThrows( + IndexNotFoundException.class, + () -> indexNameExpressionResolver.concreteIndexNames(context_no_expand, "testMo", "testMahdy") + ); + assertThat(infe_no_expand.getMessage(), is("no such index [testMo]")); } public void testConcreteIndicesIgnoreIndicesEmptyRequest() { @@ -993,7 +1052,7 @@ public void testConcreteIndicesWildcardAndAliases() { // when ignoreAliases option is set, concreteIndexNames resolves the provided expressions // only against the defined indices - IndicesOptions ignoreAliasesOptions = IndicesOptions.fromOptions(false, false, true, false, true, false, true, false); + IndicesOptions ignoreAliasesOptions = IndicesOptions.fromOptions(false, randomBoolean(), true, false, true, false, true, false); String[] indexNamesIndexWildcard = indexNameExpressionResolver.concreteIndexNames(state, ignoreAliasesOptions, "foo*"); @@ -1019,9 +1078,32 @@ public void testConcreteIndicesWildcardAndAliases() { iae.getMessage() ); + // same as above, but DO NOT expand wildcards + iae = expectThrows( + IllegalArgumentException.class, + () -> indexNameExpressionResolver.concreteIndexNames( + state, + IndicesOptions.fromOptions(false, randomBoolean(), false, false, true, false, true, false), + "foo" + ) + ); + assertEquals( + "The provided expression [foo] matches an alias, specify the corresponding concrete indices instead.", + iae.getMessage() + ); + // when ignoreAliases option is not set, concreteIndexNames resolves the provided // expressions against the defined indices and aliases - IndicesOptions indicesAndAliasesOptions = IndicesOptions.fromOptions(false, false, true, false, true, false, false, false); + IndicesOptions indicesAndAliasesOptions = IndicesOptions.fromOptions( + false, + randomBoolean(), + true, + false, + true, + false, + false, + false + ); List indexNames = Arrays.asList(indexNameExpressionResolver.concreteIndexNames(state, indicesAndAliasesOptions, "foo*")); assertEquals(2, indexNames.size()); @@ -1042,6 +1124,13 @@ public void testConcreteIndicesWildcardAndAliases() { assertEquals(2, indexNames.size()); assertTrue(indexNames.contains("foo_foo")); assertTrue(indexNames.contains("bar_bar")); + + // same as above, but DO NOT expand wildcards + indicesAndAliasesOptions = IndicesOptions.fromOptions(false, randomBoolean(), false, false, true, false, false, false); + indexNames = Arrays.asList(indexNameExpressionResolver.concreteIndexNames(state, indicesAndAliasesOptions, "foo")); + assertEquals(2, indexNames.size()); + assertTrue(indexNames.contains("foo_foo")); + assertTrue(indexNames.contains("bar_bar")); } public void testHiddenAliasAndHiddenIndexResolution() { @@ -1858,13 +1947,42 @@ public void testDeleteIndexIgnoresAliases() { assertEquals("does_not_exist", infe.getIndex().getName()); assertEquals("no such index [does_not_exist]", infe.getMessage()); } + { + // same delete request but with request options that DO NOT expand wildcards + DeleteIndexRequest request = new DeleteIndexRequest("does_not_exist").indicesOptions( + new IndicesOptions( + EnumSet.of(IndicesOptions.Option.ALLOW_NO_INDICES), + randomFrom(EnumSet.noneOf(IndicesOptions.WildcardStates.class), EnumSet.of(IndicesOptions.WildcardStates.HIDDEN)) + ) + ); + IndexNotFoundException infe = expectThrows( + IndexNotFoundException.class, + () -> indexNameExpressionResolver.concreteIndexNames(state, request) + ); + assertEquals("does_not_exist", infe.getIndex().getName()); + assertEquals("no such index [does_not_exist]", infe.getMessage()); + } { IllegalArgumentException iae = expectThrows( IllegalArgumentException.class, () -> indexNameExpressionResolver.concreteIndexNames(state, new DeleteIndexRequest("test-alias")) ); assertEquals( - "The provided expression [test-alias] matches an alias, " + "specify the corresponding concrete indices instead.", + "The provided expression [test-alias] matches an alias, specify the corresponding concrete indices instead.", + iae.getMessage() + ); + } + { + // same delete request but with request options that DO NOT expand wildcards + DeleteIndexRequest request = new DeleteIndexRequest("test-alias").indicesOptions( + IndicesOptions.fromOptions(false, true, false, false, false, false, true, false) + ); + IllegalArgumentException iae = expectThrows( + IllegalArgumentException.class, + () -> indexNameExpressionResolver.concreteIndexNames(state, request) + ); + assertEquals( + "The provided expression [test-alias] matches an alias, specify the corresponding concrete indices instead.", iae.getMessage() ); } @@ -1874,6 +1992,13 @@ public void testDeleteIndexIgnoresAliases() { String[] indices = indexNameExpressionResolver.concreteIndexNames(state, deleteIndexRequest); assertEquals(0, indices.length); } + { + // same request as above but with request options that DO NOT expand wildcards + DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("test-alias"); + deleteIndexRequest.indicesOptions(IndicesOptions.fromOptions(true, true, false, false, false, false, true, false)); + String[] indices = indexNameExpressionResolver.concreteIndexNames(state, deleteIndexRequest); + assertEquals(0, indices.length); + } { DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("test-a*"); deleteIndexRequest.indicesOptions(IndicesOptions.fromOptions(randomBoolean(), false, true, true, false, false, true, false)); @@ -1892,6 +2017,16 @@ public void testDeleteIndexIgnoresAliases() { assertEquals(1, indices.length); assertEquals("test-index", indices[0]); } + { + String[] indices = indexNameExpressionResolver.concreteIndexNames( + state, + new DeleteIndexRequest("test-index").indicesOptions( + IndicesOptions.fromOptions(false, true, false, false, false, false, false, false) + ) + ); + assertEquals(1, indices.length); + assertEquals("test-index", indices[0]); + } { String[] indices = indexNameExpressionResolver.concreteIndexNames(state, new DeleteIndexRequest("test-*")); assertEquals(1, indices.length); @@ -2411,8 +2546,11 @@ public void testDataStreams() { assertThat(result[1].getName(), equalTo(DataStream.getDefaultBackingIndexName(dataStreamName, 2, epochMillis))); } { - // Ignore data streams - IndicesOptions indicesOptions = IndicesOptions.STRICT_EXPAND_OPEN; + // Ignore data streams,allow no indices and expand wildcards + IndicesOptions indicesOptions = randomFrom( + IndicesOptions.STRICT_EXPAND_OPEN, + new IndicesOptions(EnumSet.of(IndicesOptions.Option.ALLOW_NO_INDICES), EnumSet.of(IndicesOptions.WildcardStates.OPEN)) + ); Exception e = expectThrows( IndexNotFoundException.class, () -> indexNameExpressionResolver.concreteIndices(state, indicesOptions, false, "my-data-stream") @@ -2420,10 +2558,10 @@ public void testDataStreams() { assertThat(e.getMessage(), equalTo("no such index [my-data-stream]")); } { - // Ignore data streams and allow no indices + // Ignore data streams and DO NOT expand wildcards IndicesOptions indicesOptions = new IndicesOptions( EnumSet.of(IndicesOptions.Option.ALLOW_NO_INDICES), - EnumSet.of(IndicesOptions.WildcardStates.OPEN) + randomFrom(EnumSet.noneOf(IndicesOptions.WildcardStates.class), EnumSet.of(IndicesOptions.WildcardStates.HIDDEN)) ); Exception e = expectThrows( IndexNotFoundException.class, @@ -2440,11 +2578,29 @@ public void testDataStreams() { Index[] result = indexNameExpressionResolver.concreteIndices(state, indicesOptions, false, "my-data-stream"); assertThat(result.length, equalTo(0)); } + { + // Ignore data streams, allow no indices, ignore unavailable and DO NOT expand wildcards + IndicesOptions indicesOptions = new IndicesOptions( + EnumSet.of(IndicesOptions.Option.ALLOW_NO_INDICES, IndicesOptions.Option.IGNORE_UNAVAILABLE), + randomFrom(EnumSet.noneOf(IndicesOptions.WildcardStates.class), EnumSet.of(IndicesOptions.WildcardStates.HIDDEN)) + ); + Index[] result = indexNameExpressionResolver.concreteIndices(state, indicesOptions, false, "my-data-stream"); + assertThat(result.length, equalTo(0)); + } { IndicesOptions indicesOptions = IndicesOptions.STRICT_EXPAND_OPEN; Index result = indexNameExpressionResolver.concreteWriteIndex(state, indicesOptions, "my-data-stream", false, true); assertThat(result.getName(), equalTo(DataStream.getDefaultBackingIndexName(dataStreamName, 2, epochMillis))); } + { + // same as above but don't expand wildcards + IndicesOptions indicesOptions = new IndicesOptions( + EnumSet.of(IndicesOptions.Option.ALLOW_NO_INDICES), + randomFrom(EnumSet.noneOf(IndicesOptions.WildcardStates.class), EnumSet.of(IndicesOptions.WildcardStates.HIDDEN)) + ); + Index result = indexNameExpressionResolver.concreteWriteIndex(state, indicesOptions, "my-data-stream", false, true); + assertThat(result.getName(), equalTo(DataStream.getDefaultBackingIndexName(dataStreamName, 2, epochMillis))); + } { // Ignore data streams IndicesOptions indicesOptions = new IndicesOptions( @@ -2457,6 +2613,18 @@ public void testDataStreams() { ); assertThat(e.getMessage(), equalTo("no such index [my-data-stream]")); } + { + // same as above but don't expand wildcards + IndicesOptions indicesOptions = new IndicesOptions( + EnumSet.noneOf(IndicesOptions.Option.class), + EnumSet.noneOf(IndicesOptions.WildcardStates.class) + ); + Exception e = expectThrows( + IndexNotFoundException.class, + () -> indexNameExpressionResolver.concreteWriteIndex(state, indicesOptions, "my-data-stream", true, false) + ); + assertThat(e.getMessage(), equalTo("no such index [my-data-stream]")); + } { // Ignore data streams and allow no indices IndicesOptions indicesOptions = IndicesOptions.STRICT_EXPAND_OPEN; @@ -2466,6 +2634,18 @@ public void testDataStreams() { ); assertThat(e.getMessage(), equalTo("no such index [my-data-stream]")); } + { + // same as above but don't expand wildcards + IndicesOptions indicesOptions = new IndicesOptions( + EnumSet.of(IndicesOptions.Option.ALLOW_NO_INDICES), + randomFrom(EnumSet.noneOf(IndicesOptions.WildcardStates.class), EnumSet.of(IndicesOptions.WildcardStates.HIDDEN)) + ); + Exception e = expectThrows( + IndexNotFoundException.class, + () -> indexNameExpressionResolver.concreteWriteIndex(state, indicesOptions, "my-data-stream", false, false) + ); + assertThat(e.getMessage(), equalTo("no such index [my-data-stream]")); + } { // Ignore data streams, allow no indices and ignore unavailable IndicesOptions indicesOptions = new IndicesOptions( @@ -2478,6 +2658,18 @@ public void testDataStreams() { ); assertThat(e.getMessage(), equalTo("no such index [my-data-stream]")); } + { + // same as above but don't expand wildcards + IndicesOptions indicesOptions = new IndicesOptions( + EnumSet.of(IndicesOptions.Option.ALLOW_NO_INDICES, IndicesOptions.Option.IGNORE_UNAVAILABLE), + randomFrom(EnumSet.noneOf(IndicesOptions.WildcardStates.class), EnumSet.of(IndicesOptions.WildcardStates.HIDDEN)) + ); + Exception e = expectThrows( + IndexNotFoundException.class, + () -> indexNameExpressionResolver.concreteWriteIndex(state, indicesOptions, "my-data-stream", false, false) + ); + assertThat(e.getMessage(), equalTo("no such index [my-data-stream]")); + } } public void testDataStreamAliases() { @@ -2511,17 +2703,35 @@ public void testDataStreamAliases() { ClusterState state = ClusterState.builder(new ClusterName("_name")).metadata(mdBuilder).build(); { - IndicesOptions indicesOptions = IndicesOptions.STRICT_EXPAND_OPEN; + IndicesOptions indicesOptions = randomFrom( + IndicesOptions.STRICT_EXPAND_OPEN, + new IndicesOptions( + EnumSet.of(IndicesOptions.Option.ALLOW_NO_INDICES), + randomFrom(EnumSet.noneOf(IndicesOptions.WildcardStates.class), EnumSet.of(IndicesOptions.WildcardStates.HIDDEN)) + ) + ); Index[] result = indexNameExpressionResolver.concreteIndices(state, indicesOptions, true, dataStreamAlias1); assertThat(result, arrayContainingInAnyOrder(index1.getIndex(), index2.getIndex(), index3.getIndex(), index4.getIndex())); } { - IndicesOptions indicesOptions = IndicesOptions.STRICT_EXPAND_OPEN; + IndicesOptions indicesOptions = randomFrom( + IndicesOptions.STRICT_EXPAND_OPEN, + new IndicesOptions( + EnumSet.of(IndicesOptions.Option.ALLOW_NO_INDICES), + randomFrom(EnumSet.noneOf(IndicesOptions.WildcardStates.class), EnumSet.of(IndicesOptions.WildcardStates.HIDDEN)) + ) + ); Index[] result = indexNameExpressionResolver.concreteIndices(state, indicesOptions, true, dataStreamAlias2); assertThat(result, arrayContainingInAnyOrder(index3.getIndex(), index4.getIndex())); } { - IndicesOptions indicesOptions = IndicesOptions.STRICT_EXPAND_OPEN; + IndicesOptions indicesOptions = randomFrom( + IndicesOptions.STRICT_EXPAND_OPEN, + new IndicesOptions( + EnumSet.of(IndicesOptions.Option.ALLOW_NO_INDICES), + randomFrom(EnumSet.noneOf(IndicesOptions.WildcardStates.class), EnumSet.of(IndicesOptions.WildcardStates.HIDDEN)) + ) + ); Index[] result = indexNameExpressionResolver.concreteIndices(state, indicesOptions, true, dataStreamAlias3); assertThat(result, arrayContainingInAnyOrder(index5.getIndex(), index6.getIndex())); } @@ -2533,6 +2743,18 @@ public void testDataStreamAliases() { ); assertThat(e.getMessage(), equalTo("no such index [" + dataStreamAlias1 + "]")); } + { + // same as above but DO NOT expand wildcards + IndicesOptions indicesOptions = new IndicesOptions( + EnumSet.of(IndicesOptions.Option.ALLOW_NO_INDICES), + randomFrom(EnumSet.noneOf(IndicesOptions.WildcardStates.class), EnumSet.of(IndicesOptions.WildcardStates.HIDDEN)) + ); + Exception e = expectThrows( + IndexNotFoundException.class, + () -> indexNameExpressionResolver.concreteIndices(state, indicesOptions, false, dataStreamAlias1) + ); + assertThat(e.getMessage(), equalTo("no such index [" + dataStreamAlias1 + "]")); + } { IndicesOptions indicesOptions = IndicesOptions.STRICT_EXPAND_OPEN; Exception e = expectThrows( @@ -2541,6 +2763,18 @@ public void testDataStreamAliases() { ); assertThat(e.getMessage(), equalTo("no such index [" + dataStreamAlias2 + "]")); } + { + // same as above but DO NOT expand wildcards + IndicesOptions indicesOptions = new IndicesOptions( + EnumSet.of(IndicesOptions.Option.ALLOW_NO_INDICES), + randomFrom(EnumSet.noneOf(IndicesOptions.WildcardStates.class), EnumSet.of(IndicesOptions.WildcardStates.HIDDEN)) + ); + Exception e = expectThrows( + IndexNotFoundException.class, + () -> indexNameExpressionResolver.concreteIndices(state, indicesOptions, false, dataStreamAlias2) + ); + assertThat(e.getMessage(), equalTo("no such index [" + dataStreamAlias2 + "]")); + } { IndicesOptions indicesOptions = IndicesOptions.STRICT_EXPAND_OPEN; Exception e = expectThrows( @@ -2549,6 +2783,18 @@ public void testDataStreamAliases() { ); assertThat(e.getMessage(), equalTo("no such index [" + dataStreamAlias3 + "]")); } + { + // same as above but DO NOT expand wildcards + IndicesOptions indicesOptions = new IndicesOptions( + EnumSet.of(IndicesOptions.Option.ALLOW_NO_INDICES), + randomFrom(EnumSet.noneOf(IndicesOptions.WildcardStates.class), EnumSet.of(IndicesOptions.WildcardStates.HIDDEN)) + ); + Exception e = expectThrows( + IndexNotFoundException.class, + () -> indexNameExpressionResolver.concreteIndices(state, indicesOptions, false, dataStreamAlias3) + ); + assertThat(e.getMessage(), equalTo("no such index [" + dataStreamAlias3 + "]")); + } { IndicesOptions indicesOptions = IndicesOptions.STRICT_EXPAND_OPEN; Index[] result = indexNameExpressionResolver.concreteIndices(state, indicesOptions, true, "my-alias*"); @@ -2575,6 +2821,16 @@ public void testDataStreamAliases() { assertThat(result, notNullValue()); assertThat(result.getName(), backingIndexEqualTo(dataStream2, 2)); } + { + // same as above but DO NOT expand wildcards + IndicesOptions indicesOptions = new IndicesOptions( + EnumSet.of(IndicesOptions.Option.ALLOW_NO_INDICES), + randomFrom(EnumSet.noneOf(IndicesOptions.WildcardStates.class), EnumSet.of(IndicesOptions.WildcardStates.HIDDEN)) + ); + Index result = indexNameExpressionResolver.concreteWriteIndex(state, indicesOptions, dataStreamAlias1, false, true); + assertThat(result, notNullValue()); + assertThat(result.getName(), backingIndexEqualTo(dataStream2, 2)); + } } public void testDataStreamsWithWildcardExpression() { @@ -2842,27 +3098,43 @@ public void testResolveWriteIndexAbstraction() { List.of(new Tuple<>("logs-foobar", 1)), List.of("my-index") ); - state = ClusterState.builder(state) + final ClusterState finalState = ClusterState.builder(state) .metadata( Metadata.builder(state.getMetadata()) .put(IndexMetadata.builder(state.getMetadata().index("my-index")).putAlias(new AliasMetadata.Builder("my-alias"))) .build() ) .build(); - DocWriteRequest request = new IndexRequest("logs-foobar"); - IndexAbstraction result = indexNameExpressionResolver.resolveWriteIndexAbstraction(state, request); - assertThat(result.getType(), equalTo(IndexAbstraction.Type.DATA_STREAM)); - assertThat(result.getName(), equalTo("logs-foobar")); - - request = new IndexRequest("my-index"); - result = indexNameExpressionResolver.resolveWriteIndexAbstraction(state, request); - assertThat(result.getName(), equalTo("my-index")); - assertThat(result.getType(), equalTo(IndexAbstraction.Type.CONCRETE_INDEX)); - - request = new IndexRequest("my-alias"); - result = indexNameExpressionResolver.resolveWriteIndexAbstraction(state, request); - assertThat(result.getName(), equalTo("my-alias")); - assertThat(result.getType(), equalTo(IndexAbstraction.Type.ALIAS)); + Function>> docWriteRequestsForName = (name) -> List.of( + new IndexRequest(name).opType(DocWriteRequest.OpType.INDEX), + new IndexRequest(name).opType(DocWriteRequest.OpType.CREATE), + new DeleteRequest(name), + new UpdateRequest(name, randomAlphaOfLength(8)) + ); + for (DocWriteRequest request : docWriteRequestsForName.apply("logs-foobar")) { + if (request.opType() == DocWriteRequest.OpType.CREATE) { + IndexAbstraction result = indexNameExpressionResolver.resolveWriteIndexAbstraction(finalState, request); + assertThat(result.getType(), equalTo(IndexAbstraction.Type.DATA_STREAM)); + assertThat(result.getName(), equalTo("logs-foobar")); + } else { + IndexNotFoundException infe = expectThrows( + IndexNotFoundException.class, + () -> indexNameExpressionResolver.resolveWriteIndexAbstraction(finalState, request) + ); + assertThat(infe.toString(), containsString("logs-foobar")); + assertThat(infe.getMetadataKeys().contains(IndexNameExpressionResolver.EXCLUDED_DATA_STREAMS_KEY), is(true)); + } + } + for (DocWriteRequest request : docWriteRequestsForName.apply("my-index")) { + IndexAbstraction result = indexNameExpressionResolver.resolveWriteIndexAbstraction(finalState, request); + assertThat(result.getName(), equalTo("my-index")); + assertThat(result.getType(), equalTo(IndexAbstraction.Type.CONCRETE_INDEX)); + } + for (DocWriteRequest request : docWriteRequestsForName.apply("my-alias")) { + IndexAbstraction result = indexNameExpressionResolver.resolveWriteIndexAbstraction(finalState, request); + assertThat(result.getName(), equalTo("my-alias")); + assertThat(result.getType(), equalTo(IndexAbstraction.Type.ALIAS)); + } } public void testResolveWriteIndexAbstractionNoWriteIndexForAlias() { diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/WildcardExpressionResolverTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/WildcardExpressionResolverTests.java index 3356033d5b73..27a431745c3f 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/WildcardExpressionResolverTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/WildcardExpressionResolverTests.java @@ -28,6 +28,7 @@ import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; public class WildcardExpressionResolverTests extends ESTestCase { @@ -178,6 +179,26 @@ public void testConvertWildcardsOpenClosedIndicesTests() { newHashSet(IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, Collections.singletonList("testX*"))), equalTo(newHashSet("testXXX", "testXXY")) ); + context = new IndexNameExpressionResolver.Context( + state, + IndicesOptions.fromOptions(true, true, false, false), + SystemIndexAccessLevel.NONE + ); + assertThat( + IndexNameExpressionResolver.WildcardExpressionResolver.resolve(context, List.of("testX*")), + containsInAnyOrder("testX*") + ); + context = new IndexNameExpressionResolver.Context( + state, + IndicesOptions.fromOptions(false, true, false, false), + SystemIndexAccessLevel.NONE + ); + IndexNameExpressionResolver.Context finalContext = context; + IndexNotFoundException infe = expectThrows( + IndexNotFoundException.class, + () -> IndexNameExpressionResolver.WildcardExpressionResolver.resolve(finalContext, List.of("testX*")) + ); + assertThat(infe.getIndex().getName(), is("testX*")); } // issue #13334 @@ -426,7 +447,48 @@ public void testResolveAliases() { ) ); assertEquals( - "The provided expression [foo_alias] matches an alias, " + "specify the corresponding concrete indices instead.", + "The provided expression [foo_alias] matches an alias, specify the corresponding concrete indices instead.", + iae.getMessage() + ); + } + IndicesOptions noExpandNoAliasesIndicesOptions = IndicesOptions.fromOptions(true, false, false, false, true, false, true, false); + IndexNameExpressionResolver.Context noExpandNoAliasesContext = new IndexNameExpressionResolver.Context( + state, + noExpandNoAliasesIndicesOptions, + SystemIndexAccessLevel.NONE + ); + { + Collection indices = IndexNameExpressionResolver.WildcardExpressionResolver.resolve( + noExpandNoAliasesContext, + List.of("foo_alias") + ); + assertThat(indices, containsInAnyOrder("foo_alias")); + } + IndicesOptions strictNoExpandNoAliasesIndicesOptions = IndicesOptions.fromOptions( + false, + true, + false, + false, + true, + false, + true, + false + ); + IndexNameExpressionResolver.Context strictNoExpandNoAliasesContext = new IndexNameExpressionResolver.Context( + state, + strictNoExpandNoAliasesIndicesOptions, + SystemIndexAccessLevel.NONE + ); + { + IllegalArgumentException iae = expectThrows( + IllegalArgumentException.class, + () -> IndexNameExpressionResolver.WildcardExpressionResolver.resolve( + strictNoExpandNoAliasesContext, + Collections.singletonList("foo_alias") + ) + ); + assertEquals( + "The provided expression [foo_alias] matches an alias, specify the corresponding concrete indices instead.", iae.getMessage() ); } From 163fe0914ef9b4331927fe4b860ae47b76a9a561 Mon Sep 17 00:00:00 2001 From: Joe Gallo Date: Fri, 16 Dec 2022 10:11:06 -0500 Subject: [PATCH 292/919] Speed up ingest set and append processors (#92395) --- docs/changelog/92395.yaml | 11 +++ .../test/ingest/270_set_processor.yml | 50 +++++++++++ .../CustomReflectionObjectHandler.java | 19 +++++ .../script/mustache/MustacheScriptEngine.java | 10 +-- .../plugin-metadata/plugin-security.policy | 12 --- .../mustache/CustomMustacheFactoryTests.java | 14 ++-- .../mustache/MustacheScriptEngineTests.java | 84 ++++++++++++++++--- 7 files changed, 161 insertions(+), 39 deletions(-) create mode 100644 docs/changelog/92395.yaml delete mode 100644 modules/lang-mustache/src/main/plugin-metadata/plugin-security.policy diff --git a/docs/changelog/92395.yaml b/docs/changelog/92395.yaml new file mode 100644 index 000000000000..11d0ecdf758e --- /dev/null +++ b/docs/changelog/92395.yaml @@ -0,0 +1,11 @@ +pr: 92395 +summary: Speed up ingest set and append processors +area: Ingest Node +type: bug +issues: [] +highlight: + title: Speed up ingest set and append processors + body: |- + `set` and `append` ingest processors that use mustache templates are + significantly faster. + notable: true diff --git a/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/270_set_processor.yml b/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/270_set_processor.yml index 61fc876d8180..594ff52c2b27 100644 --- a/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/270_set_processor.yml +++ b/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/270_set_processor.yml @@ -177,3 +177,53 @@ teardown: - match: { _source.copied_foo_number: 3 } - is_true: _source.copied_foo_boolean - match: { _source.foo_nochange: "no change" } + +--- +"Test set processor with reflection attempts": + - do: + ingest.put_pipeline: + id: "1" + body: > + { + "processors" : [ + { + "script": { + "description": "Set a reference to a proper java object so we can attempt reflection", + "lang": "painless", + "source": "ctx.t = metadata().now" + } + }, + { + "set": { + "description": "Attempting to call a method (ZonedDateTime#getHour()) is ignored", + "field": "method_call_is_ignored", + "value": "{{t.hour}}" + } + }, + { + "set": { + "description": "Attempting to call a method that doesn't exist is ignored", + "field": "missing_method_is_ignored", + "value": "{{t.nothing}}" + } + } + ] + } + - match: { acknowledged: true } + + - do: + index: + index: test + id: "1" + pipeline: "1" + body: { + foo: "hello" + } + + - do: + get: + index: test + id: "1" + - match: { _source.foo: "hello" } + - match: { _source.method_call_is_ignored: "" } + - match: { _source.missing_method_is_ignored: "" } diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/CustomReflectionObjectHandler.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/CustomReflectionObjectHandler.java index 12634c053412..fd2ab43f348e 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/CustomReflectionObjectHandler.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/CustomReflectionObjectHandler.java @@ -13,6 +13,7 @@ import org.elasticsearch.common.util.Maps; import org.elasticsearch.common.util.iterable.Iterables; +import java.lang.reflect.AccessibleObject; import java.lang.reflect.Array; import java.util.AbstractMap; import java.util.Collection; @@ -39,6 +40,24 @@ public Object coerce(Object object) { } } + @Override + @SuppressWarnings("rawtypes") + protected AccessibleObject findMember(Class sClass, String name) { + /* + * overriding findMember from BaseObjectHandler (our superclass's superclass) to always return null. + * + * if you trace findMember there, you'll see that it always either returns null or invokes the getMethod + * or getField methods of that class. the last thing that getMethod and getField do is call 'setAccessible' + * but we don't have java.lang.reflect.ReflectPermission/suppressAccessChecks so that will always throw an + * exception. + * + * that is, with the permissions we're running with, it would always return null ('not found!') or throw + * an exception ('found, but you cannot do this!') -- so by overriding to null we're effectively saying + * "you will never find success going down this path, so don't bother trying" + */ + return null; + } + static final class ArrayMap extends AbstractMap implements Iterable { private final Object array; diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MustacheScriptEngine.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MustacheScriptEngine.java index 8abd0d65d58d..243487981c5f 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MustacheScriptEngine.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MustacheScriptEngine.java @@ -13,7 +13,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.elasticsearch.SpecialPermission; import org.elasticsearch.script.GeneralScriptException; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; @@ -24,8 +23,6 @@ import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.Collections; import java.util.Map; import java.util.Set; @@ -108,12 +105,7 @@ private class MustacheExecutableScript extends TemplateScript { public String execute() { final StringWriter writer = new StringWriter(); try { - // crazy reflection here - SpecialPermission.check(); - AccessController.doPrivileged((PrivilegedAction) () -> { - template.execute(writer, params); - return null; - }); + template.execute(writer, params); } catch (Exception e) { logger.error(() -> format("Error running %s", template), e); throw new GeneralScriptException("Error running " + template, e); diff --git a/modules/lang-mustache/src/main/plugin-metadata/plugin-security.policy b/modules/lang-mustache/src/main/plugin-metadata/plugin-security.policy deleted file mode 100644 index 2fb8547e7b3f..000000000000 --- a/modules/lang-mustache/src/main/plugin-metadata/plugin-security.policy +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -grant { - // needed to do crazy reflection - permission java.lang.RuntimePermission "accessDeclaredMembers"; -}; diff --git a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/CustomMustacheFactoryTests.java b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/CustomMustacheFactoryTests.java index ea741e684c07..4845279c4dd8 100644 --- a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/CustomMustacheFactoryTests.java +++ b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/CustomMustacheFactoryTests.java @@ -15,8 +15,6 @@ import java.util.Map; -import static java.util.Collections.emptyMap; -import static java.util.Collections.singletonMap; import static org.elasticsearch.script.mustache.CustomMustacheFactory.JSON_MEDIA_TYPE; import static org.elasticsearch.script.mustache.CustomMustacheFactory.PLAIN_TEXT_MEDIA_TYPE; import static org.elasticsearch.script.mustache.CustomMustacheFactory.X_WWW_FORM_URLENCODED_MEDIA_TYPE; @@ -67,31 +65,31 @@ public void testCreateEncoder() { public void testJsonEscapeEncoder() { final ScriptEngine engine = new MustacheScriptEngine(); - final Map params = randomBoolean() ? singletonMap(Script.CONTENT_TYPE_OPTION, JSON_MEDIA_TYPE) : emptyMap(); + final Map params = randomBoolean() ? Map.of(Script.CONTENT_TYPE_OPTION, JSON_MEDIA_TYPE) : Map.of(); TemplateScript.Factory compiled = engine.compile(null, "{\"field\": \"{{value}}\"}", TemplateScript.CONTEXT, params); - TemplateScript executable = compiled.newInstance(singletonMap("value", "a \"value\"")); + TemplateScript executable = compiled.newInstance(Map.of("value", "a \"value\"")); assertThat(executable.execute(), equalTo("{\"field\": \"a \\\"value\\\"\"}")); } public void testDefaultEncoder() { final ScriptEngine engine = new MustacheScriptEngine(); - final Map params = singletonMap(Script.CONTENT_TYPE_OPTION, PLAIN_TEXT_MEDIA_TYPE); + final Map params = Map.of(Script.CONTENT_TYPE_OPTION, PLAIN_TEXT_MEDIA_TYPE); TemplateScript.Factory compiled = engine.compile(null, "{\"field\": \"{{value}}\"}", TemplateScript.CONTEXT, params); - TemplateScript executable = compiled.newInstance(singletonMap("value", "a \"value\"")); + TemplateScript executable = compiled.newInstance(Map.of("value", "a \"value\"")); assertThat(executable.execute(), equalTo("{\"field\": \"a \"value\"\"}")); } public void testUrlEncoder() { final ScriptEngine engine = new MustacheScriptEngine(); - final Map params = singletonMap(Script.CONTENT_TYPE_OPTION, X_WWW_FORM_URLENCODED_MEDIA_TYPE); + final Map params = Map.of(Script.CONTENT_TYPE_OPTION, X_WWW_FORM_URLENCODED_MEDIA_TYPE); TemplateScript.Factory compiled = engine.compile(null, "{\"field\": \"{{value}}\"}", TemplateScript.CONTEXT, params); - TemplateScript executable = compiled.newInstance(singletonMap("value", "tilde~ AND date:[2016 FROM*]")); + TemplateScript executable = compiled.newInstance(Map.of("value", "tilde~ AND date:[2016 FROM*]")); assertThat(executable.execute(), equalTo("{\"field\": \"tilde%7E+AND+date%3A%5B2016+FROM*%5D\"}")); } } diff --git a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/MustacheScriptEngineTests.java b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/MustacheScriptEngineTests.java index 2420fed44451..812a281d77c1 100644 --- a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/MustacheScriptEngineTests.java +++ b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/MustacheScriptEngineTests.java @@ -18,8 +18,7 @@ import java.io.IOException; import java.io.StringWriter; -import java.util.Collections; -import java.util.HashMap; +import java.util.List; import java.util.Map; import static org.hamcrest.Matchers.equalTo; @@ -38,7 +37,7 @@ public void setup() { } public void testSimpleParameterReplace() { - Map compileParams = Collections.singletonMap("content_type", "application/json"); + Map compileParams = Map.of("content_type", "application/json"); { String template = """ GET _search @@ -61,8 +60,7 @@ public void testSimpleParameterReplace() { } } }"""; - Map vars = new HashMap<>(); - vars.put("boost_val", "0.3"); + Map vars = Map.of("boost_val", "0.3"); String o = qe.compile(null, template, TemplateScript.CONTEXT, compileParams).newInstance(vars).execute(); assertEquals(""" GET _search @@ -108,9 +106,7 @@ public void testSimpleParameterReplace() { } } }"""; - Map vars = new HashMap<>(); - vars.put("boost_val", "0.3"); - vars.put("body_val", "\"quick brown\""); + Map vars = Map.of("boost_val", "0.3", "body_val", "\"quick brown\""); String o = qe.compile(null, template, TemplateScript.CONTEXT, compileParams).newInstance(vars).execute(); assertEquals(""" GET _search @@ -141,7 +137,7 @@ public void testSimple() throws IOException { {"source":{"match_{{template}}": {}},"params":{"template":"all"}}"""; XContentParser parser = createParser(JsonXContent.jsonXContent, templateString); Script script = Script.parse(parser); - TemplateScript.Factory compiled = qe.compile(null, script.getIdOrCode(), TemplateScript.CONTEXT, Collections.emptyMap()); + TemplateScript.Factory compiled = qe.compile(null, script.getIdOrCode(), TemplateScript.CONTEXT, Map.of()); TemplateScript TemplateScript = compiled.newInstance(script.getParams()); assertThat(TemplateScript.execute(), equalTo("{\"match_all\":{}}")); } @@ -157,11 +153,79 @@ public void testParseTemplateAsSingleStringWithConditionalClause() throws IOExce }"""; XContentParser parser = createParser(JsonXContent.jsonXContent, templateString); Script script = Script.parse(parser); - TemplateScript.Factory compiled = qe.compile(null, script.getIdOrCode(), TemplateScript.CONTEXT, Collections.emptyMap()); + TemplateScript.Factory compiled = qe.compile(null, script.getIdOrCode(), TemplateScript.CONTEXT, Map.of()); TemplateScript TemplateScript = compiled.newInstance(script.getParams()); assertThat(TemplateScript.execute(), equalTo("{ \"match_all\":{} }")); } + private static class TestReflection { + + private final int privateField = 1; + + public final int publicField = 2; + + private int getPrivateMethod() { + return 3; + } + + public int getPublicMethod() { + return 4; + } + + @Override + public String toString() { + return List.of(privateField, publicField, getPrivateMethod(), getPublicMethod()).toString(); + } + } + + /** + * BWC test for some odd reflection edge-cases. It's not really expected that customer code would be exercising this, + * but maybe it's out there! Who knows!? + * + * If we change this, we should *know* that we're changing it. + */ + @SuppressWarnings({ "deprecation", "removal" }) + public void testReflection() { + Map vars = Map.of("obj", new TestReflection()); + + { + // non-reflective access calls toString + String templateString = "{{obj}}"; + String o = qe.compile(null, templateString, TemplateScript.CONTEXT, Map.of()).newInstance(vars).execute(); + assertThat(o, equalTo("[1, 2, 3, 4]")); + } + { + // accessing a field/method that *doesn't* exist will give an empty result + String templateString = "{{obj.missing}}"; + String o = qe.compile(null, templateString, TemplateScript.CONTEXT, Map.of()).newInstance(vars).execute(); + assertThat(o, equalTo("")); + } + { + // accessing a private field that does exist will give an empty result + String templateString = "{{obj.privateField}}"; + String o = qe.compile(null, templateString, TemplateScript.CONTEXT, Map.of()).newInstance(vars).execute(); + assertThat(o, equalTo("")); + } + { + // accessing a private method that does exist will give an empty result + String templateString = "{{obj.privateMethod}}"; + String o = qe.compile(null, templateString, TemplateScript.CONTEXT, Map.of()).newInstance(vars).execute(); + assertThat(o, equalTo("")); + } + { + // accessing a public field that does exist will give an empty result + String templateString = "{{obj.publicField}}"; + String o = qe.compile(null, templateString, TemplateScript.CONTEXT, Map.of()).newInstance(vars).execute(); + assertThat(o, equalTo("")); + } + { + // accessing a public method that does exist will give an empty result + String templateString = "{{obj.publicMethod}}"; + String o = qe.compile(null, templateString, TemplateScript.CONTEXT, Map.of()).newInstance(vars).execute(); + assertThat(o, equalTo("")); + } + } + public void testEscapeJson() throws IOException { { StringWriter writer = new StringWriter(); From 89e2f69c15dee5775e004f642c2df3eb30f6f237 Mon Sep 17 00:00:00 2001 From: Alan Woodward Date: Fri, 16 Dec 2022 16:03:17 +0000 Subject: [PATCH 293/919] Add a TSDB rate aggregation (#90447) This commit adds an extension to the existing rate aggregation that, when applied to a field defined as a metric counter, will calculate rates that take into account counter resets. --- docs/changelog/90447.yaml | 5 + .../aggregations/AggregatorTestCase.java | 3 - .../test/InternalAggregationTestCase.java | 11 +- .../xpack/analytics/AnalyticsPlugin.java | 22 ++- .../rate/InternalResetTrackingRate.java | 123 ++++++++++++++ .../analytics/rate/RateAggregatorFactory.java | 7 + .../rate/TimeSeriesRateAggregator.java | 154 +++++++++++++++++ .../rate/InternalResetTrackingRateTests.java | 133 +++++++++++++++ .../rate/TimeSeriesRateAggregatorTests.java | 157 ++++++++++++++++++ .../test/analytics/reset_tracking_rate.yml | 135 +++++++++++++++ 10 files changed, 740 insertions(+), 10 deletions(-) create mode 100644 docs/changelog/90447.yaml create mode 100644 x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/rate/InternalResetTrackingRate.java create mode 100644 x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/rate/TimeSeriesRateAggregator.java create mode 100644 x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/rate/InternalResetTrackingRateTests.java create mode 100644 x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/rate/TimeSeriesRateAggregatorTests.java create mode 100644 x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/analytics/reset_tracking_rate.yml diff --git a/docs/changelog/90447.yaml b/docs/changelog/90447.yaml new file mode 100644 index 000000000000..198bbb781565 --- /dev/null +++ b/docs/changelog/90447.yaml @@ -0,0 +1,5 @@ +pr: 90447 +summary: Add a TSDB rate aggregation +area: "TSDB" +type: feature +issues: [] diff --git a/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java b/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java index bcb65192ab48..9ecbb549f8a3 100644 --- a/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java @@ -449,9 +449,6 @@ protected ScriptService getMockScriptService() { * Collects all documents that match the provided query {@link Query} and * returns the reduced {@link InternalAggregation}. *

- * Half the time it aggregates each leaf individually and reduces all - * results together. The other half the time it aggregates across the entire - * index at once and runs a final reduction on the single resulting agg. * It runs the aggregation as well using a circuit breaker that randomly throws {@link CircuitBreakingException} * in order to mak sure the implementation does not leak. */ diff --git a/test/framework/src/main/java/org/elasticsearch/test/InternalAggregationTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/InternalAggregationTestCase.java index 150eff8f84ee..3406bbf38dab 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/InternalAggregationTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/InternalAggregationTestCase.java @@ -401,6 +401,13 @@ private void collectSubBuilderNames(Map names, InternalAggregati public record BuilderAndToReduce (AggregationBuilder builder, List toReduce) {} + /** + * Does this aggregation support reductions when the internal buckets are not in-order + */ + protected boolean supportsOutOfOrderReduce() { + return true; + } + public void testReduceRandom() throws IOException { String name = randomAlphaOfLength(5); int size = between(1, 200); @@ -412,7 +419,9 @@ public void testReduceRandom() throws IOException { MockBigArrays bigArrays = new MockBigArrays(new MockPageCacheRecycler(Settings.EMPTY), new NoneCircuitBreakerService()); if (randomBoolean() && toReduce.size() > 1) { // sometimes do a partial reduce - Collections.shuffle(toReduce, random()); + if (supportsOutOfOrderReduce()) { + Collections.shuffle(toReduce, random()); + } int r = randomIntBetween(1, toReduce.size()); List toPartialReduce = toReduce.subList(0, r); // Sort aggs so that unmapped come last. This mimicks the behavior of InternalAggregations.reduce() diff --git a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/AnalyticsPlugin.java b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/AnalyticsPlugin.java index fefa87c57714..20f702d54be9 100644 --- a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/AnalyticsPlugin.java +++ b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/AnalyticsPlugin.java @@ -16,6 +16,7 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.env.Environment; import org.elasticsearch.env.NodeEnvironment; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.mapper.Mapper; import org.elasticsearch.plugins.ActionPlugin; import org.elasticsearch.plugins.MapperPlugin; @@ -42,6 +43,7 @@ import org.elasticsearch.xpack.analytics.multiterms.MultiTermsAggregationBuilder; import org.elasticsearch.xpack.analytics.normalize.NormalizePipelineAggregationBuilder; import org.elasticsearch.xpack.analytics.rate.InternalRate; +import org.elasticsearch.xpack.analytics.rate.InternalResetTrackingRate; import org.elasticsearch.xpack.analytics.rate.RateAggregationBuilder; import org.elasticsearch.xpack.analytics.stringstats.InternalStringStats; import org.elasticsearch.xpack.analytics.stringstats.StringStatsAggregationBuilder; @@ -119,19 +121,27 @@ public List getAggregations() { TTestAggregationBuilder::new, usage.track(AnalyticsStatsAction.Item.T_TEST, TTestAggregationBuilder.PARSER) ).addResultReader(InternalTTest::new).setAggregatorRegistrar(TTestAggregationBuilder::registerUsage), - new AggregationSpec( - RateAggregationBuilder.NAME, - RateAggregationBuilder::new, - usage.track(AnalyticsStatsAction.Item.RATE, RateAggregationBuilder.PARSER) - ).addResultReader(InternalRate::new).setAggregatorRegistrar(RateAggregationBuilder::registerAggregators), new AggregationSpec( MultiTermsAggregationBuilder.NAME, MultiTermsAggregationBuilder::new, usage.track(AnalyticsStatsAction.Item.MULTI_TERMS, MultiTermsAggregationBuilder.PARSER) - ).addResultReader(InternalMultiTerms::new).setAggregatorRegistrar(MultiTermsAggregationBuilder::registerAggregators) + ).addResultReader(InternalMultiTerms::new).setAggregatorRegistrar(MultiTermsAggregationBuilder::registerAggregators), + rateAggregation() ); } + private AggregationSpec rateAggregation() { + AggregationSpec rate = new AggregationSpec( + RateAggregationBuilder.NAME, + RateAggregationBuilder::new, + usage.track(AnalyticsStatsAction.Item.RATE, RateAggregationBuilder.PARSER) + ).addResultReader(InternalRate::new).setAggregatorRegistrar(RateAggregationBuilder::registerAggregators); + if (IndexSettings.isTimeSeriesModeEnabled()) { + rate.addResultReader(InternalResetTrackingRate.NAME, InternalResetTrackingRate::new); + } + return rate; + } + @Override public List> getActions() { return List.of( diff --git a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/rate/InternalResetTrackingRate.java b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/rate/InternalResetTrackingRate.java new file mode 100644 index 000000000000..b3cab7189d43 --- /dev/null +++ b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/rate/InternalResetTrackingRate.java @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.analytics.rate; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.search.DocValueFormat; +import org.elasticsearch.search.aggregations.AggregationReduceContext; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +public class InternalResetTrackingRate extends InternalNumericMetricsAggregation.SingleValue implements Rate { + + public static final String NAME = "rate_with_resets"; + + private final double startValue; + private final double endValue; + private final long startTime; + private final long endTime; + private final double resetCompensation; + + protected InternalResetTrackingRate( + String name, + DocValueFormat format, + Map metadata, + double startValue, + double endValue, + long startTime, + long endTime, + double resetCompensation + ) { + super(name, format, metadata); + this.startValue = startValue; + this.endValue = endValue; + this.startTime = startTime; + this.endTime = endTime; + this.resetCompensation = resetCompensation; + } + + public InternalResetTrackingRate(StreamInput in) throws IOException { + super(in, false); + this.startValue = in.readDouble(); + this.endValue = in.readDouble(); + this.startTime = in.readLong(); + this.endTime = in.readLong(); + this.resetCompensation = in.readDouble(); + } + + @Override + public String getWriteableName() { + return NAME; + } + + @Override + protected void doWriteTo(StreamOutput out) throws IOException { + out.writeDouble(startValue); + out.writeDouble(endValue); + out.writeLong(startTime); + out.writeLong(endTime); + out.writeDouble(resetCompensation); + } + + @Override + public InternalAggregation reduce(List aggregations, AggregationReduceContext reduceContext) { + List toReduce = aggregations.stream() + .map(r -> (InternalResetTrackingRate) r) + .sorted(Comparator.comparingLong(o -> o.startTime)) + .toList(); + double resetComp = toReduce.get(0).resetCompensation; + double startValue = toReduce.get(0).startValue; + double endValue = toReduce.get(0).endValue; + final int endIndex = toReduce.size() - 1; + for (int i = 1; i < endIndex + 1; i++) { + InternalResetTrackingRate rate = toReduce.get(i); + assert rate.startTime >= toReduce.get(i - 1).endTime; + resetComp += rate.resetCompensation; + if (endValue > rate.startValue) { + resetComp += endValue; + } + endValue = rate.endValue; + } + return new InternalResetTrackingRate( + name, + format, + metadata, + startValue, + endValue, + toReduce.get(0).startTime, + toReduce.get(endIndex).endTime, + resetComp + ); + } + + @Override + public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { + return builder.field(CommonFields.VALUE.getPreferredName(), value()); + } + + @Override + public double value() { + return (endValue - startValue + resetCompensation) / (endTime - startTime); + } + + @Override + public double getValue() { + return value(); + } + + boolean includes(InternalResetTrackingRate other) { + return this.startTime < other.startTime && this.endTime > other.endTime; + } +} diff --git a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/rate/RateAggregatorFactory.java b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/rate/RateAggregatorFactory.java index 01c69bbdbd2c..ab0c428ac421 100644 --- a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/rate/RateAggregatorFactory.java +++ b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/rate/RateAggregatorFactory.java @@ -16,6 +16,7 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.CoreValuesSourceType; +import org.elasticsearch.search.aggregations.support.TimeSeriesValuesSourceType; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; import org.elasticsearch.search.aggregations.support.ValuesSourceRegistry; @@ -64,6 +65,12 @@ static void registerAggregators(ValuesSourceRegistry.Builder builder) { HistogramRateAggregator::new, true ); + builder.register( + RateAggregationBuilder.REGISTRY_KEY, + Collections.singletonList(TimeSeriesValuesSourceType.COUNTER), + TimeSeriesRateAggregator::new, + true + ); } @Override diff --git a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/rate/TimeSeriesRateAggregator.java b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/rate/TimeSeriesRateAggregator.java new file mode 100644 index 000000000000..bf2fdc982422 --- /dev/null +++ b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/rate/TimeSeriesRateAggregator.java @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.analytics.rate; + +import org.elasticsearch.common.Rounding; +import org.elasticsearch.common.util.DoubleArray; +import org.elasticsearch.common.util.LongArray; +import org.elasticsearch.core.Releasables; +import org.elasticsearch.index.fielddata.NumericDoubleValues; +import org.elasticsearch.search.DocValueFormat; +import org.elasticsearch.search.MultiValueMode; +import org.elasticsearch.search.aggregations.AggregationExecutionContext; +import org.elasticsearch.search.aggregations.Aggregator; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.LeafBucketCollector; +import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; +import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregator; +import org.elasticsearch.search.aggregations.support.AggregationContext; +import org.elasticsearch.search.aggregations.support.ValuesSource; +import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; + +import java.io.IOException; +import java.util.Map; + +public class TimeSeriesRateAggregator extends NumericMetricsAggregator.SingleValue { + + protected final ValuesSource.Numeric valuesSource; + + protected DoubleArray startValues; + protected DoubleArray endValues; + protected LongArray startTimes; + protected LongArray endTimes; + protected DoubleArray resetCompensations; + + private long currentBucket = -1; + private long currentEndTime = -1; + private long currentStartTime = -1; + private double resetCompensation = 0; + private double currentEndValue = -1; + private double currentStartValue = -1; + private int currentTsid = -1; + + // Unused parameters are so that the constructor implements `RateAggregatorSupplier` + protected TimeSeriesRateAggregator( + String name, + ValuesSourceConfig valuesSourceConfig, + Rounding.DateTimeUnit rateUnit, + RateMode rateMode, + AggregationContext context, + Aggregator parent, + Map metadata + ) throws IOException { + super(name, context, parent, metadata); + this.valuesSource = (ValuesSource.Numeric) valuesSourceConfig.getValuesSource(); + this.startValues = bigArrays().newDoubleArray(1, true); + this.endValues = bigArrays().newDoubleArray(1, true); + this.startTimes = bigArrays().newLongArray(1, true); + this.endTimes = bigArrays().newLongArray(1, true); + this.resetCompensations = bigArrays().newDoubleArray(1, true); + } + + @Override + public InternalAggregation buildEmptyAggregation() { + return new InternalResetTrackingRate(name, DocValueFormat.RAW, metadata(), 0, 0, 0, 0, 0); + } + + private void calculateLastBucket() { + if (currentBucket != -1) { + startValues.set(currentBucket, currentStartValue); + endValues.set(currentBucket, currentEndValue); + startTimes.set(currentBucket, currentStartTime); + endTimes.set(currentBucket, currentEndTime); + resetCompensations.set(currentBucket, resetCompensation); + currentBucket = -1; + } + } + + private double checkForResets(double latestValue) { + if (latestValue > currentStartValue) { + // reset detected + resetCompensation += currentEndValue; + currentEndValue = latestValue; + } + return latestValue; + } + + @Override + protected LeafBucketCollector getLeafCollector(AggregationExecutionContext aggCtx, LeafBucketCollector sub) throws IOException { + NumericDoubleValues leafValues = MultiValueMode.MAX.select(valuesSource.doubleValues(aggCtx.getLeafReaderContext())); + return new LeafBucketCollectorBase(sub, null) { + @Override + public void collect(int doc, long bucket) throws IOException { + leafValues.advanceExact(doc); // TODO handle missing values + double latestValue = leafValues.doubleValue(); + + if (bucket != currentBucket) { + startValues = bigArrays().grow(startValues, bucket + 1); + endValues = bigArrays().grow(endValues, bucket + 1); + startTimes = bigArrays().grow(startTimes, bucket + 1); + endTimes = bigArrays().grow(endTimes, bucket + 1); + resetCompensations = bigArrays().grow(resetCompensations, bucket + 1); + if (currentTsid != aggCtx.getTsidOrd()) { + // if we're on a new tsid then we need to calculate the last bucket + calculateLastBucket(); + currentTsid = aggCtx.getTsidOrd(); + } else { + // if we're in a new bucket but in the same tsid then we update the + // timestamp and last value before we calculate the last bucket + currentStartTime = aggCtx.getTimestamp(); + currentStartValue = checkForResets(latestValue); + calculateLastBucket(); + } + currentBucket = bucket; + currentStartTime = currentEndTime = aggCtx.getTimestamp(); + currentStartValue = currentEndValue = latestValue; + resetCompensation = 0; + } else { + currentStartTime = aggCtx.getTimestamp(); + currentStartValue = checkForResets(latestValue); + } + } + }; + } + + @Override + public InternalResetTrackingRate buildAggregation(long owningBucketOrd) { + calculateLastBucket(); + return new InternalResetTrackingRate( + name, + DocValueFormat.RAW, + metadata(), + startValues.get(owningBucketOrd), + endValues.get(owningBucketOrd), + startTimes.get(owningBucketOrd), + endTimes.get(owningBucketOrd), + resetCompensations.get(owningBucketOrd) + ); + } + + @Override + protected void doClose() { + Releasables.close(startValues, endValues, startTimes, endTimes, resetCompensations); + } + + @Override + public double metric(long owningBucketOrd) { + return buildAggregation(owningBucketOrd).getValue(); + } +} diff --git a/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/rate/InternalResetTrackingRateTests.java b/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/rate/InternalResetTrackingRateTests.java new file mode 100644 index 000000000000..726f6bb5821f --- /dev/null +++ b/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/rate/InternalResetTrackingRateTests.java @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.analytics.rate; + +import org.elasticsearch.common.util.CollectionUtils; +import org.elasticsearch.plugins.SearchPlugin; +import org.elasticsearch.search.aggregations.Aggregation; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.ParsedAggregation; +import org.elasticsearch.test.InternalAggregationTestCase; +import org.elasticsearch.xcontent.NamedXContentRegistry; +import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xpack.analytics.AnalyticsPlugin; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.mockito.Mockito.mock; + +public class InternalResetTrackingRateTests extends InternalAggregationTestCase { + + @Override + protected SearchPlugin registerPlugin() { + return new AnalyticsPlugin(); + } + + @Override + protected InternalResetTrackingRate createTestInstance(String name, Map metadata) { + return new InternalResetTrackingRate(name, null, metadata, 0, 0, 0, 0, 0); + } + + private static InternalResetTrackingRate rate(double startValue, double endValue, long startTime, long endTime, double resetComp) { + return new InternalResetTrackingRate("n", null, null, startValue, endValue, startTime, endTime, resetComp); + } + + public void testReduction() { + List rates = List.of( + rate(0, 10, 1000, 2000, 0), + rate(10, 20, 2000, 3000, 0), + rate(20, 5, 3000, 4000, 25), // internal reset + rate(5, 15, 4000, 5000, 0), + rate(0, 10, 5000, 6000, 0) // cross-boundary reset + ); + InternalAggregation reduced = rates.get(0).reduce(rates, null); + assertThat(reduced, instanceOf(Rate.class)); + assertThat(((Rate) reduced).getValue(), equalTo(0.01)); + } + + @Override + protected void assertReduced(InternalResetTrackingRate reduced, List inputs) { + for (InternalResetTrackingRate input : inputs) { + assertEquals(0.01f, input.getValue(), 0.001); + } + assertEquals(0.01f, reduced.getValue(), 0.001); + } + + // Buckets must always be in-order so that we can detect resets between consecutive buckets + @Override + protected boolean supportsOutOfOrderReduce() { + return false; + } + + @Override + protected BuilderAndToReduce randomResultsToReduce(String name, int size) { + // generate a monotonically increasing counter, starting at 0 finishing at 1000 and increasing + // by 10 each time + // randomly reset to 0 + // randomly break to a new rate + List internalRates = new ArrayList<>(); + double startValue = 0, currentValue = 0; + double resetComp = 0; + long startTime = 0; + long endTime = 0; + while (internalRates.size() < size - 1) { + endTime += 1000; + currentValue += 10; + if (randomInt(30) == 0) { + resetComp += currentValue; + currentValue = 0; + } + if (randomInt(45) == 0) { + internalRates.add(rate(startValue, currentValue, startTime, endTime, resetComp)); + startValue = currentValue; + resetComp = 0; + startTime = endTime; + } + } + if (startTime == endTime) { + endTime += 1000; + currentValue += 10; + } + internalRates.add(rate(startValue, currentValue, startTime, endTime, resetComp)); + return new BuilderAndToReduce<>(mock(RateAggregationBuilder.class), internalRates); + } + + @Override + protected void assertFromXContent(InternalResetTrackingRate aggregation, ParsedAggregation parsedAggregation) throws IOException { + + } + + @Override + protected List getNamedXContents() { + return CollectionUtils.appendToCopy( + super.getNamedXContents(), + new NamedXContentRegistry.Entry(Aggregation.class, new ParseField(InternalResetTrackingRate.NAME), (p, c) -> { + assumeTrue("There is no ParsedRate yet", false); + return null; + }) + ); + } + + public void testIncludes() { + InternalResetTrackingRate big = new InternalResetTrackingRate("n", null, null, 0, 0, 1000, 3000, 0); + InternalResetTrackingRate small = new InternalResetTrackingRate("n", null, null, 0, 0, 1500, 2500, 0); + assertTrue(big.includes(small)); + assertFalse(small.includes(big)); + + InternalResetTrackingRate unrelated = new InternalResetTrackingRate("n", null, null, 0, 0, 100000, 1000010, 0); + assertFalse(big.includes(unrelated)); + assertFalse(unrelated.includes(big)); + assertFalse(small.includes(unrelated)); + assertFalse(unrelated.includes(small)); + } +} diff --git a/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/rate/TimeSeriesRateAggregatorTests.java b/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/rate/TimeSeriesRateAggregatorTests.java new file mode 100644 index 000000000000..9b7c0cf123c4 --- /dev/null +++ b/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/rate/TimeSeriesRateAggregatorTests.java @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.analytics.rate; + +import org.apache.lucene.document.Document; +import org.apache.lucene.document.NumericDocValuesField; +import org.apache.lucene.document.SortedDocValuesField; +import org.apache.lucene.document.SortedNumericDocValuesField; +import org.elasticsearch.aggregations.AggregationsPlugin; +import org.elasticsearch.aggregations.bucket.timeseries.InternalTimeSeries; +import org.elasticsearch.aggregations.bucket.timeseries.TimeSeriesAggregationBuilder; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.index.mapper.DateFieldMapper; +import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.mapper.NumberFieldMapper; +import org.elasticsearch.index.mapper.TimeSeriesIdFieldMapper; +import org.elasticsearch.index.mapper.TimeSeriesParams; +import org.elasticsearch.plugins.SearchPlugin; +import org.elasticsearch.search.aggregations.AggregatorTestCase; +import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; +import org.elasticsearch.search.aggregations.bucket.histogram.InternalDateHistogram; +import org.elasticsearch.xpack.analytics.AnalyticsPlugin; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; + +public class TimeSeriesRateAggregatorTests extends AggregatorTestCase { + + @Override + protected List getSearchPlugins() { + return List.of(new AggregationsPlugin(), new AnalyticsPlugin()); + } + + public void testSimple() throws IOException { + RateAggregationBuilder builder = new RateAggregationBuilder("counter_field").field("counter_field"); + TimeSeriesAggregationBuilder tsBuilder = new TimeSeriesAggregationBuilder("tsid"); + tsBuilder.subAggregation(builder); + Consumer verifier = r -> { + assertThat(r.getBuckets(), hasSize(2)); + assertThat(((Rate) r.getBucketByKey("{dim=1}").getAggregations().asList().get(0)).getValue(), closeTo(59.0 / 3000.0, 0.00001)); + assertThat(((Rate) r.getBucketByKey("{dim=2}").getAggregations().asList().get(0)).getValue(), closeTo(206.0 / 4000.0, 0.00001)); + }; + AggTestConfig aggTestConfig = new AggTestConfig(tsBuilder, timeStampField(), counterField("counter_field")) + .withSplitLeavesIntoSeperateAggregators(false); + testCase(iw -> { + iw.addDocuments(docs(1000, "1", 15, 37, 60, /*reset*/ 14)); + iw.addDocuments(docs(1000, "2", 74, 150, /*reset*/ 50, 90, /*reset*/ 40)); + }, verifier, aggTestConfig); + } + + public void testNestedWithinDateHistogram() throws IOException { + RateAggregationBuilder builder = new RateAggregationBuilder("counter_field").field("counter_field"); + DateHistogramAggregationBuilder dateBuilder = new DateHistogramAggregationBuilder("date"); + dateBuilder.field("@timestamp"); + dateBuilder.fixedInterval(DateHistogramInterval.seconds(2)); + dateBuilder.subAggregation(builder); + TimeSeriesAggregationBuilder tsBuilder = new TimeSeriesAggregationBuilder("tsid"); + tsBuilder.subAggregation(dateBuilder); + + Consumer verifier = r -> { + assertThat(r.getBuckets(), hasSize(2)); + assertThat(r.getBucketByKey("{dim=1}"), instanceOf(InternalTimeSeries.InternalBucket.class)); + InternalDateHistogram hb = r.getBucketByKey("{dim=1}").getAggregations().get("date"); + { + Rate rate = hb.getBuckets().get(1).getAggregations().get("counter_field"); + assertThat(rate.getValue(), closeTo((60 - 37 + 14) / 2000.0, 0.00001)); + } + { + Rate rate = hb.getBuckets().get(0).getAggregations().get("counter_field"); + assertThat(rate.getValue(), closeTo((37 - 15) / 1000.0, 0.00001)); + } + hb = r.getBucketByKey("{dim=2}").getAggregations().get("date"); + { + Rate rate = hb.getBuckets().get(0).getAggregations().get("counter_field"); + assertThat(rate.getValue(), closeTo((150 - 74) / 1000.0, 0.00001)); + } + { + Rate rate = hb.getBuckets().get(1).getAggregations().get("counter_field"); + assertThat(rate.getValue(), closeTo(90 / 2000.0, 0.00001)); + } + }; + + AggTestConfig aggTestConfig = new AggTestConfig(tsBuilder, timeStampField(), counterField("counter_field")) + .withSplitLeavesIntoSeperateAggregators(false); + testCase(iw -> { + iw.addDocuments(docs(2000, "1", 15, 37, 60, /*reset*/ 14)); + iw.addDocuments(docs(2000, "2", 74, 150, /*reset*/ 50, 90, /*reset*/ 40)); + }, verifier, aggTestConfig); + } + + private List docs(long startTimestamp, String dim, long... values) throws IOException { + + List documents = new ArrayList<>(); + for (int i = 0; i < values.length; i++) { + documents.add(doc(startTimestamp + (i * 1000L), tsid(dim), values[i])); + } + return documents; + } + + private static BytesReference tsid(String dim) throws IOException { + TimeSeriesIdFieldMapper.TimeSeriesIdBuilder idBuilder = new TimeSeriesIdFieldMapper.TimeSeriesIdBuilder(null); + idBuilder.addString("dim", dim); + return idBuilder.build(); + } + + private Document doc(long timestamp, BytesReference tsid, long counterValue) { + Document doc = new Document(); + doc.add(new SortedNumericDocValuesField("@timestamp", timestamp)); + doc.add(new SortedDocValuesField("_tsid", tsid.toBytesRef())); + doc.add(new NumericDocValuesField("counter_field", counterValue)); + return doc; + } + + private MappedFieldType counterField(String name) { + return new NumberFieldMapper.NumberFieldType( + name, + NumberFieldMapper.NumberType.LONG, + true, + false, + true, + false, + null, + Collections.emptyMap(), + null, + false, + TimeSeriesParams.MetricType.counter + ); + } + + private DateFieldMapper.DateFieldType timeStampField() { + return new DateFieldMapper.DateFieldType( + "@timestamp", + true, + false, + true, + DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER, + DateFieldMapper.Resolution.MILLISECONDS, + null, + null, + Collections.emptyMap() + ); + } + +} diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/analytics/reset_tracking_rate.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/analytics/reset_tracking_rate.yml new file mode 100644 index 000000000000..7027e0f90391 --- /dev/null +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/analytics/reset_tracking_rate.yml @@ -0,0 +1,135 @@ +setup: + - skip: + version: " - 8.5.0" + reason: Reset tracking rate agg added in 8.6 + + - do: + indices.create: + index: tsdb-01 + body: + settings: + number_of_replicas: 0 + mode: time_series + routing_path: [key] + time_series: + start_time: "2021-01-01T00:00:00Z" + end_time: "2021-01-01T01:00:00Z" + mappings: + properties: + val: + type: long + time_series_metric: counter + key: + type: keyword + time_series_dimension: true + "@timestamp": + type: date + - do: + indices.create: + index: tsdb-02 + body: + settings: + number_of_replicas: 0 + mode: time_series + routing_path: [key] + time_series: + start_time: "2021-01-01T00:01:00Z" + end_time: "2021-01-01T02:00:00Z" + mappings: + properties: + val: + type: long + time_series_metric: counter + key: + type: keyword + time_series_dimension: true + "@timestamp": + type: date + - do: + indices.create: + index: tsdb-03 + body: + settings: + number_of_replicas: 0 + mode: time_series + routing_path: [key] + time_series: + start_time: "2021-01-01T00:02:00Z" + end_time: "2021-01-01T03:00:00Z" + mappings: + properties: + val: + type: long + time_series_metric: counter + key: + type: keyword + time_series_dimension: true + "@timestamp": + type: date + - do: + cluster.health: + wait_for_status: green + + - do: + bulk: + index: tsdb-01 + refresh: true + body: + - '{ "index": {} }' + - '{ "key": "bar", "val": 40, "@timestamp": "2021-01-01T00:00:00Z" }' + - '{ "index": {}}' + - '{ "key": "bar", "val": 50, "@timestamp": "2021-01-01T00:20:00Z" }' + - '{ "index": {}}' + - '{ "key": "bar", "val": 60, "@timestamp": "2021-01-01T00:40:00Z" }' + - do: + bulk: + index: tsdb-02 + refresh: true + body: + - '{ "index": {} }' + - '{ "key": "bar", "val": 70, "@timestamp": "2021-01-01T01:00:00Z" }' + - '{ "index": {}}' + - '{ "key": "bar", "val": 0, "@timestamp": "2021-01-01T01:20:00Z" }' + - '{ "index": {}}' + - '{ "key": "bar", "val": 10, "@timestamp": "2021-01-01T01:40:00Z" }' + + - do: + bulk: + index: tsdb-03 + refresh: true + body: + - '{ "index": {} }' + - '{ "key": "bar", "val": 0, "@timestamp": "2021-01-01T02:00:00Z" }' + - '{ "index": {}}' + - '{ "key": "bar", "val": 10, "@timestamp": "2021-01-01T02:20:00Z" }' + - '{ "index": {}}' + - '{ "key": "bar", "val": 20, "@timestamp": "2021-01-01T02:40:00Z" }' +--- +"test resets do not lead to negative rate": + - do: + search: + index: tsdb-* + body: + query: + range: + "@timestamp": + gte: "2020-01-01T00:10:00Z" + size: 0 + aggs: + ts: + time_series: + keyed: false + aggs: + rate: + rate: + field: val + + + + - match: { hits.total.value: 9 } + - length: { aggregations.ts.buckets: 1 } + + - match: { aggregations.ts.buckets.0.key: { "key": "bar" } } + - match: { aggregations.ts.buckets.0.doc_count: 9 } + - gte: { aggregations.ts.buckets.0.rate.value: 0.0 } + From 3723af3ccdd935de8e32c2d54f38efda7358b921 Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Fri, 16 Dec 2022 16:15:01 +0000 Subject: [PATCH 294/919] [HealthAPI] Add size parameter that controls the number of affected resources returned (#92399) This adds a `size` parameter that controls the maximum number of returned affected resources. The parameter defaults to `1000`, must be positive, and less than `10_000` --- docs/changelog/92399.yaml | 7 + docs/reference/health/health.asciidoc | 6 + .../http/HealthRestCancellationIT.java | 2 +- .../rest-api-spec/api/_internal.health.json | 19 ++- .../discovery/StableMasterDisruptionIT.java | 3 +- .../health/GetHealthActionIT.java | 12 +- .../elasticsearch/health/HealthServiceIT.java | 4 +- .../node/DiskHealthIndicatorServiceIT.java | 2 +- ...toryIntegrityHealthIndicatorServiceIT.java | 2 +- .../StableMasterHealthIndicatorService.java | 2 +- ...rdsAvailabilityHealthIndicatorService.java | 8 +- .../elasticsearch/health/GetHealthAction.java | 38 +++-- .../health/HealthIndicatorService.java | 6 +- .../elasticsearch/health/HealthService.java | 19 ++- .../health/RestGetHealthAction.java | 5 +- .../node/DiskHealthIndicatorService.java | 23 +-- ...sitoryIntegrityHealthIndicatorService.java | 9 +- ...ailabilityHealthIndicatorServiceTests.java | 142 +++++++++++++----- .../health/GetHealthRequestTests.java | 27 ++++ .../health/HealthIndicatorServiceTests.java | 2 +- .../health/HealthServiceTests.java | 22 ++- .../node/DiskHealthIndicatorServiceTests.java | 81 ++++++++++ ...yIntegrityHealthIndicatorServiceTests.java | 37 +++++ ...ierShardAvailabilityHealthIndicatorIT.java | 6 +- .../xpack/ilm/IlmHealthIndicatorService.java | 2 +- .../xpack/slm/SlmHealthIndicatorService.java | 7 +- 26 files changed, 391 insertions(+), 102 deletions(-) create mode 100644 docs/changelog/92399.yaml create mode 100644 server/src/test/java/org/elasticsearch/health/GetHealthRequestTests.java diff --git a/docs/changelog/92399.yaml b/docs/changelog/92399.yaml new file mode 100644 index 000000000000..027f3ac85969 --- /dev/null +++ b/docs/changelog/92399.yaml @@ -0,0 +1,7 @@ +pr: 92399 +summary: "[HealthAPI] Add size parameter that controls the number of affected resources\ + \ returned" +area: Health +type: feature +issues: + - 91930 diff --git a/docs/reference/health/health.asciidoc b/docs/reference/health/health.asciidoc index be310ca85ea0..579c20a96862 100644 --- a/docs/reference/health/health.asciidoc +++ b/docs/reference/health/health.asciidoc @@ -92,6 +92,12 @@ for health status set `verbose` to `false` to disable the more expensive analysi These details include additional troubleshooting metrics and sometimes a root cause analysis of a health status. Defaults to `true`. +`size`:: + (Optional, integer) The maximum number of affected resources to return. + As a diagnosis can return multiple types of affected resources this parameter will limit the number of resources returned for each type to the configured value (e.g. a diagnosis could return + `1000` affected indices and `1000` affected nodes). + Defaults to `1000`. + [role="child_attributes"] [[health-api-response-body]] ==== {api-response-body-title} diff --git a/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/HealthRestCancellationIT.java b/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/HealthRestCancellationIT.java index e0d6a75495de..9e3b884622d1 100644 --- a/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/HealthRestCancellationIT.java +++ b/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/HealthRestCancellationIT.java @@ -111,7 +111,7 @@ public String name() { } @Override - public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { + public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) { try { operationBlock.acquire(); } catch (InterruptedException e) { diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/_internal.health.json b/rest-api-spec/src/main/resources/rest-api-spec/api/_internal.health.json index 4528c33b0853..615f072a80aa 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/_internal.health.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/_internal.health.json @@ -32,14 +32,19 @@ ] }, "params":{ - "timeout":{ - "type":"time", - "description":"Explicit operation timeout" + "timeout": { + "type": "time", + "description": "Explicit operation timeout" }, - "verbose":{ - "type":"boolean", - "description":"Opt in for more information about the health of the system", - "default":true + "verbose": { + "type": "boolean", + "description": "Opt in for more information about the health of the system", + "default": true + }, + "size": { + "type": "int", + "description": "Limit the number of affected resources the health API returns", + "default": 1000 } } } diff --git a/server/src/internalClusterTest/java/org/elasticsearch/discovery/StableMasterDisruptionIT.java b/server/src/internalClusterTest/java/org/elasticsearch/discovery/StableMasterDisruptionIT.java index 9508d550404b..941400f7ce48 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/discovery/StableMasterDisruptionIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/discovery/StableMasterDisruptionIT.java @@ -135,7 +135,8 @@ private void assertGreenMasterStability(Client client) throws Exception { private void assertMasterStability(Client client, HealthStatus expectedStatus, Matcher expectedMatcher) throws Exception { assertBusy(() -> { - GetHealthAction.Response healthResponse = client.execute(GetHealthAction.INSTANCE, new GetHealthAction.Request(true)).get(); + GetHealthAction.Response healthResponse = client.execute(GetHealthAction.INSTANCE, new GetHealthAction.Request(true, 1000)) + .get(); String debugInformation = xContentToString(healthResponse); assertThat(debugInformation, healthResponse.findIndicator("master_is_stable").status(), equalTo(expectedStatus)); assertThat(debugInformation, healthResponse.findIndicator("master_is_stable").symptom(), expectedMatcher); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/health/GetHealthActionIT.java b/server/src/internalClusterTest/java/org/elasticsearch/health/GetHealthActionIT.java index 858c783b0cb1..25fb69a4dab7 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/health/GetHealthActionIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/health/GetHealthActionIT.java @@ -144,7 +144,7 @@ public String name() { } @Override - public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { + public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) { var status = clusterService.getClusterSettings().get(statusSetting); return createIndicator( status, @@ -203,8 +203,10 @@ public void testGetHealth() throws Exception { { ExecutionException exception = expectThrows( ExecutionException.class, - () -> client.execute(GetHealthAction.INSTANCE, new GetHealthAction.Request(NONEXISTENT_INDICATOR_NAME, randomBoolean())) - .get() + () -> client.execute( + GetHealthAction.INSTANCE, + new GetHealthAction.Request(NONEXISTENT_INDICATOR_NAME, randomBoolean(), 1000) + ).get() ); assertThat(exception.getCause(), instanceOf(ResourceNotFoundException.class)); } @@ -258,7 +260,7 @@ private void testRootLevel( HealthStatus clusterCoordinationIndicatorStatus, boolean verbose ) throws Exception { - var response = client.execute(GetHealthAction.INSTANCE, new GetHealthAction.Request(verbose)).get(); + var response = client.execute(GetHealthAction.INSTANCE, new GetHealthAction.Request(verbose, 1000)).get(); assertThat( response.getStatus(), @@ -294,7 +296,7 @@ private void testRootLevel( } private void testIndicator(Client client, HealthStatus ilmIndicatorStatus, boolean verbose) throws Exception { - var response = client.execute(GetHealthAction.INSTANCE, new GetHealthAction.Request(ILM_INDICATOR_NAME, verbose)).get(); + var response = client.execute(GetHealthAction.INSTANCE, new GetHealthAction.Request(ILM_INDICATOR_NAME, verbose, 1000)).get(); assertNull(response.getStatus()); assertThat(response.getClusterName(), equalTo(new ClusterName(cluster().getClusterName()))); assertThat( diff --git a/server/src/internalClusterTest/java/org/elasticsearch/health/HealthServiceIT.java b/server/src/internalClusterTest/java/org/elasticsearch/health/HealthServiceIT.java index bc59dd27dc70..79f69a497288 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/health/HealthServiceIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/health/HealthServiceIT.java @@ -92,7 +92,7 @@ public void onFailure(Exception e) { throw new RuntimeException(e); } }; - healthService.getHealth(internalCluster.client(node), TestHealthIndicatorService.NAME, true, listener); + healthService.getHealth(internalCluster.client(node), TestHealthIndicatorService.NAME, true, 1000, listener); assertBusy(() -> assertThat(onResponseCalled.get(), equalTo(true))); } } @@ -158,7 +158,7 @@ public String name() { } @Override - public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { + public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) { assertThat(healthInfo.diskInfoByNode().size(), equalTo(internalCluster().getNodeNames().length)); for (DiskHealthInfo diskHealthInfo : healthInfo.diskInfoByNode().values()) { assertThat(diskHealthInfo.healthStatus(), equalTo(HealthStatus.GREEN)); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/health/node/DiskHealthIndicatorServiceIT.java b/server/src/internalClusterTest/java/org/elasticsearch/health/node/DiskHealthIndicatorServiceIT.java index 9741f5c791ab..1cab207fda30 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/health/node/DiskHealthIndicatorServiceIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/health/node/DiskHealthIndicatorServiceIT.java @@ -81,7 +81,7 @@ public void onFailure(Exception e) { throw new RuntimeException(e); } }; - healthService.getHealth(internalCluster().client(node), DiskHealthIndicatorService.NAME, true, listener); + healthService.getHealth(internalCluster().client(node), DiskHealthIndicatorService.NAME, true, 1000, listener); assertBusy(() -> assertNotNull(resultListReference.get())); return resultListReference.get(); } diff --git a/server/src/internalClusterTest/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorServiceIT.java b/server/src/internalClusterTest/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorServiceIT.java index 36ecee336712..80b9c437a5dd 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorServiceIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorServiceIT.java @@ -67,7 +67,7 @@ public void testRepositoryIntegrityHealthIndicator() throws IOException, Interru } private void assertSnapshotRepositoryHealth(String message, Client client, HealthStatus status) { - var response = client.execute(GetHealthAction.INSTANCE, new GetHealthAction.Request(randomBoolean())).actionGet(); + var response = client.execute(GetHealthAction.INSTANCE, new GetHealthAction.Request(randomBoolean(), 1000)).actionGet(); assertThat(message, response.findIndicator(NAME).status(), equalTo(status)); } diff --git a/server/src/main/java/org/elasticsearch/cluster/coordination/StableMasterHealthIndicatorService.java b/server/src/main/java/org/elasticsearch/cluster/coordination/StableMasterHealthIndicatorService.java index 2ed8dca833e5..f7e4f886eb70 100644 --- a/server/src/main/java/org/elasticsearch/cluster/coordination/StableMasterHealthIndicatorService.java +++ b/server/src/main/java/org/elasticsearch/cluster/coordination/StableMasterHealthIndicatorService.java @@ -104,7 +104,7 @@ public String name() { } @Override - public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { + public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) { CoordinationDiagnosticsService.CoordinationDiagnosticsResult coordinationDiagnosticsResult = coordinationDiagnosticsService .diagnoseMasterStability(verbose); return getHealthIndicatorResult(coordinationDiagnosticsResult, verbose); diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java index 2752c7b6ad93..f5e72c9785c7 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java @@ -107,7 +107,7 @@ public String name() { } @Override - public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { + public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) { var state = clusterService.state(); var shutdown = state.getMetadata().custom(NodesShutdownMetadata.TYPE, NodesShutdownMetadata.EMPTY); var status = new ShardAllocationStatus(state.getMetadata()); @@ -126,7 +126,7 @@ public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { status.getSymptom(), status.getDetails(verbose), status.getImpacts(), - status.getDiagnosis(verbose) + status.getDiagnosis(verbose, maxAffectedResourcesCount) ); } @@ -893,9 +893,10 @@ public List getImpacts() { /** * Returns the diagnosis for unassigned primary and replica shards. * @param verbose true if the diagnosis should be generated, false if they should be omitted. + * @param maxAffectedResourcesCount the max number of affected resources to be returned as part of the diagnosis * @return The diagnoses list the indicator identified. Alternatively, an empty list if none were found or verbose is false. */ - public List getDiagnosis(boolean verbose) { + public List getDiagnosis(boolean verbose, int maxAffectedResourcesCount) { if (verbose) { Map> diagnosisToAffectedIndices = new HashMap<>(primaries.diagnosisDefinitions); replicas.diagnosisDefinitions.forEach((diagnosisDef, indicesWithReplicasUnassigned) -> { @@ -920,6 +921,7 @@ public List getDiagnosis(boolean verbose) { e.getValue() .stream() .sorted(indicesComparatorByPriorityAndName(clusterMetadata)) + .limit(Math.min(e.getValue().size(), maxAffectedResourcesCount)) .collect(Collectors.toList()) ) ) diff --git a/server/src/main/java/org/elasticsearch/health/GetHealthAction.java b/server/src/main/java/org/elasticsearch/health/GetHealthAction.java index 2029717b3545..5e60b2b6c87b 100644 --- a/server/src/main/java/org/elasticsearch/health/GetHealthAction.java +++ b/server/src/main/java/org/elasticsearch/health/GetHealthAction.java @@ -37,6 +37,8 @@ import java.util.NoSuchElementException; import java.util.Objects; +import static org.elasticsearch.action.ValidateActions.addValidationError; + public class GetHealthAction extends ActionType { public static final GetHealthAction INSTANCE = new GetHealthAction(); @@ -146,21 +148,25 @@ public String toString() { public static class Request extends ActionRequest { private final String indicatorName; private final boolean verbose; + private final int size; - public Request(boolean verbose) { - // We never compute details if no indicator name is given because of the runtime cost: - this.indicatorName = null; - this.verbose = verbose; + public Request(boolean verbose, int size) { + this(null, verbose, size); } - public Request(String indicatorName, boolean verbose) { + public Request(String indicatorName, boolean verbose, int size) { this.indicatorName = indicatorName; this.verbose = verbose; + this.size = size; } @Override public ActionRequestValidationException validate() { - return null; + ActionRequestValidationException validationException = null; + if (size < 0) { + validationException = addValidationError("The size parameter must be a positive integer", validationException); + } + return validationException; } @Override @@ -195,11 +201,21 @@ public TransportAction( @Override protected void doExecute(Task task, Request request, ActionListener responseListener) { assert task instanceof CancellableTask; - healthService.getHealth(client, request.indicatorName, request.verbose, responseListener.map(healthIndicatorResults -> { - Response response = new Response(clusterService.getClusterName(), healthIndicatorResults, request.indicatorName == null); - healthApiStats.track(request.verbose, response); - return response; - })); + healthService.getHealth( + client, + request.indicatorName, + request.verbose, + request.size, + responseListener.map(healthIndicatorResults -> { + Response response = new Response( + clusterService.getClusterName(), + healthIndicatorResults, + request.indicatorName == null + ); + healthApiStats.track(request.verbose, response); + return response; + }) + ); } } } diff --git a/server/src/main/java/org/elasticsearch/health/HealthIndicatorService.java b/server/src/main/java/org/elasticsearch/health/HealthIndicatorService.java index 1c0972f8cb35..b2342057e0f5 100644 --- a/server/src/main/java/org/elasticsearch/health/HealthIndicatorService.java +++ b/server/src/main/java/org/elasticsearch/health/HealthIndicatorService.java @@ -22,7 +22,11 @@ public interface HealthIndicatorService { String name(); - HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo); + default HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { + return calculate(verbose, 1000, healthInfo); + } + + HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo); /** * This method creates a HealthIndicatorResult with the given information. Note that it sorts the impacts by severity (the lower the diff --git a/server/src/main/java/org/elasticsearch/health/HealthService.java b/server/src/main/java/org/elasticsearch/health/HealthService.java index c3079c6a7170..7828f12e7aba 100644 --- a/server/src/main/java/org/elasticsearch/health/HealthService.java +++ b/server/src/main/java/org/elasticsearch/health/HealthService.java @@ -76,20 +76,25 @@ public HealthService( * * @param client A client to be used to fetch the health data from the health node * @param indicatorName If not null, the returned results will only have this indicator - * @param explain Whether to compute the details portion of the results + * @param verbose Whether to compute the details portion of the results * @param listener A listener to be notified of the list of all HealthIndicatorResult if indicatorName is null, or one * HealthIndicatorResult if indicatorName is not null + * @param maxAffectedResourcesCount The maximum number of affected resources to return per each type. * @throws ResourceNotFoundException if an indicator name is given and the indicator is not found */ public void getHealth( Client client, @Nullable String indicatorName, - boolean explain, + boolean verbose, + int maxAffectedResourcesCount, ActionListener> listener ) { + if (maxAffectedResourcesCount < 0) { + throw new IllegalArgumentException("The max number of resources must be a positive integer"); + } // Determine if cluster is stable enough to calculate health before running other indicators List preflightResults = preflightHealthIndicatorServices.stream() - .map(service -> service.calculate(explain, HealthInfo.EMPTY_HEALTH_INFO)) + .map(service -> service.calculate(verbose, maxAffectedResourcesCount, HealthInfo.EMPTY_HEALTH_INFO)) .toList(); // If any of these are not GREEN, then we cannot obtain health from other indicators @@ -113,7 +118,7 @@ public void onResponse(FetchHealthInfoCacheAction.Response response) { ActionRunnable> calculateFilteredIndicatorsRunnable = calculateFilteredIndicatorsRunnable( indicatorName, healthInfo, - explain, + verbose, listener ); @@ -131,7 +136,7 @@ public void onFailure(Exception e) { ActionRunnable> calculateFilteredIndicatorsRunnable = calculateFilteredIndicatorsRunnable( indicatorName, HealthInfo.EMPTY_HEALTH_INFO, - explain, + verbose, listener ); try { @@ -150,7 +155,7 @@ private ActionRunnable> calculateFilteredIndicatorsR return ActionRunnable.wrap(listener, l -> { List results = Stream.concat( filteredPreflightResults, - filteredIndicators.map(service -> service.calculate(explain, healthInfo)) + filteredIndicators.map(service -> service.calculate(explain, maxAffectedResourcesCount, healthInfo)) ).toList(); validateResultsAndNotifyListener(indicatorName, results, l); @@ -160,7 +165,7 @@ private ActionRunnable> calculateFilteredIndicatorsR } else { // Mark remaining indicators as UNKNOWN - HealthIndicatorDetails unknownDetails = healthUnknownReason(preflightResults, explain); + HealthIndicatorDetails unknownDetails = healthUnknownReason(preflightResults, verbose); Stream filteredIndicatorResults = filteredIndicators.map( service -> generateUnknownResult(service, UNKNOWN_RESULT_SUMMARY_PREFLIGHT_FAILED, unknownDetails) ); diff --git a/server/src/main/java/org/elasticsearch/health/RestGetHealthAction.java b/server/src/main/java/org/elasticsearch/health/RestGetHealthAction.java index b1ab2e6dea74..813668f9aa39 100644 --- a/server/src/main/java/org/elasticsearch/health/RestGetHealthAction.java +++ b/server/src/main/java/org/elasticsearch/health/RestGetHealthAction.java @@ -23,6 +23,8 @@ public class RestGetHealthAction extends BaseRestHandler { private static final String VERBOSE_PARAM = "verbose"; + private static final String SIZE_PARAM = "size"; + @Override public String getName() { // TODO: Existing - "cluster_health_action", "cat_health_action" @@ -38,7 +40,8 @@ public List routes() { protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { String indicatorName = request.param("indicator"); boolean verbose = request.paramAsBoolean(VERBOSE_PARAM, true); - GetHealthAction.Request getHealthRequest = new GetHealthAction.Request(indicatorName, verbose); + int size = request.paramAsInt(SIZE_PARAM, 1000); + GetHealthAction.Request getHealthRequest = new GetHealthAction.Request(indicatorName, verbose, size); return channel -> new RestCancellableNodeClient(client, request.getHttpChannel()).execute( GetHealthAction.INSTANCE, getHealthRequest, diff --git a/server/src/main/java/org/elasticsearch/health/node/DiskHealthIndicatorService.java b/server/src/main/java/org/elasticsearch/health/node/DiskHealthIndicatorService.java index 0c96c6807a65..2b64c5811c9a 100644 --- a/server/src/main/java/org/elasticsearch/health/node/DiskHealthIndicatorService.java +++ b/server/src/main/java/org/elasticsearch/health/node/DiskHealthIndicatorService.java @@ -41,6 +41,7 @@ import java.util.stream.Stream; import static org.elasticsearch.cluster.node.DiscoveryNode.DISCOVERY_NODE_COMPARATOR; +import static org.elasticsearch.common.util.CollectionUtils.limitSize; import static org.elasticsearch.health.node.HealthIndicatorDisplayValues.are; import static org.elasticsearch.health.node.HealthIndicatorDisplayValues.getSortedUniqueValuesString; import static org.elasticsearch.health.node.HealthIndicatorDisplayValues.getTruncatedIndices; @@ -68,7 +69,6 @@ public class DiskHealthIndicatorService implements HealthIndicatorService { private static final String IMPACT_INGEST_AT_RISK_ID = "ingest_capability_at_risk"; private static final String IMPACT_CLUSTER_STABILITY_AT_RISK_ID = "cluster_stability_at_risk"; private static final String IMPACT_CLUSTER_FUNCTIONALITY_UNAVAILABLE_ID = "cluster_functionality_unavailable"; - private static final String IMPACT_DATA_NODE_WITHOUT_DISK_SPACE = "data_node_without_disk_space"; private final ClusterService clusterService; @@ -82,7 +82,7 @@ public String name() { } @Override - public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { + public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) { Map diskHealthInfoMap = healthInfo.diskInfoByNode(); if (diskHealthInfoMap == null || diskHealthInfoMap.isEmpty()) { /* @@ -107,7 +107,7 @@ public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { diskHealthAnalyzer.getSymptom(), diskHealthAnalyzer.getDetails(verbose), diskHealthAnalyzer.getImpacts(), - diskHealthAnalyzer.getDiagnoses() + diskHealthAnalyzer.getDiagnoses(maxAffectedResourcesCount) ); } @@ -344,7 +344,7 @@ List getImpacts() { return impacts; } - private List getDiagnoses() { + private List getDiagnoses(int size) { if (healthStatus == HealthStatus.GREEN) { return List.of(); } @@ -353,7 +353,7 @@ private List getDiagnoses() { Set affectedIndices = Sets.union(blockedIndices, indicesAtRisk); List affectedResources = new ArrayList<>(); if (dataNodes.size() > 0) { - Diagnosis.Resource nodeResources = new Diagnosis.Resource(dataNodes); + Diagnosis.Resource nodeResources = new Diagnosis.Resource(limitSize(dataNodes, size)); affectedResources.add(nodeResources); } if (affectedIndices.size() > 0) { @@ -361,6 +361,7 @@ private List getDiagnoses() { Diagnosis.Resource.Type.INDEX, affectedIndices.stream() .sorted(indicesComparatorByPriorityAndName(clusterState.metadata())) + .limit(Math.min(affectedIndices.size(), size)) .collect(Collectors.toList()) ); affectedResources.add(indexResources); @@ -405,16 +406,16 @@ private List getDiagnoses() { } } if (masterNodes.containsKey(HealthStatus.RED)) { - diagnosisList.add(createNonDataNodeDiagnosis(HealthStatus.RED, masterNodes.get(HealthStatus.RED), true)); + diagnosisList.add(createNonDataNodeDiagnosis(HealthStatus.RED, masterNodes.get(HealthStatus.RED), size, true)); } if (masterNodes.containsKey(HealthStatus.YELLOW)) { - diagnosisList.add(createNonDataNodeDiagnosis(HealthStatus.YELLOW, masterNodes.get(HealthStatus.YELLOW), true)); + diagnosisList.add(createNonDataNodeDiagnosis(HealthStatus.YELLOW, masterNodes.get(HealthStatus.YELLOW), size, true)); } if (otherNodes.containsKey(HealthStatus.RED)) { - diagnosisList.add(createNonDataNodeDiagnosis(HealthStatus.RED, otherNodes.get(HealthStatus.RED), false)); + diagnosisList.add(createNonDataNodeDiagnosis(HealthStatus.RED, otherNodes.get(HealthStatus.RED), size, false)); } if (otherNodes.containsKey(HealthStatus.YELLOW)) { - diagnosisList.add(createNonDataNodeDiagnosis(HealthStatus.YELLOW, otherNodes.get(HealthStatus.YELLOW), false)); + diagnosisList.add(createNonDataNodeDiagnosis(HealthStatus.YELLOW, otherNodes.get(HealthStatus.YELLOW), size, false)); } return diagnosisList; } @@ -487,7 +488,7 @@ static Set getIndicesForNodes(List nodes, ClusterState cl .collect(Collectors.toSet()); } - private Diagnosis createNonDataNodeDiagnosis(HealthStatus healthStatus, Collection nodes, boolean isMaster) { + private Diagnosis createNonDataNodeDiagnosis(HealthStatus healthStatus, List nodes, int size, boolean isMaster) { return new Diagnosis( new Diagnosis.Definition( NAME, @@ -496,7 +497,7 @@ private Diagnosis createNonDataNodeDiagnosis(HealthStatus healthStatus, Collecti "Please add capacity to the current nodes, or replace them with ones with higher capacity.", isMaster ? "https://ela.st/fix-master-disk" : "https://ela.st/fix-disk-space" ), - List.of(new Diagnosis.Resource(nodes)) + List.of(new Diagnosis.Resource(limitSize(nodes, size))) ); } diff --git a/server/src/main/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorService.java b/server/src/main/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorService.java index 8ce0500c8225..59d84f396e02 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorService.java @@ -72,7 +72,7 @@ public String name() { } @Override - public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { + public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) { var snapshotMetadata = clusterService.state().metadata().custom(RepositoriesMetadata.TYPE, RepositoriesMetadata.EMPTY); if (snapshotMetadata.repositories().isEmpty()) { @@ -133,7 +133,12 @@ public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { ) : HealthIndicatorDetails.EMPTY, impacts, - List.of(new Diagnosis(CORRUPTED_REPOSITORY, List.of(new Diagnosis.Resource(Type.SNAPSHOT_REPOSITORY, corrupted)))) + List.of( + new Diagnosis( + CORRUPTED_REPOSITORY, + List.of(new Diagnosis.Resource(Type.SNAPSHOT_REPOSITORY, limitSize(corrupted, maxAffectedResourcesCount))) + ) + ) ); } diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorServiceTests.java index 4a412010482f..c7cdba228495 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorServiceTests.java @@ -105,7 +105,7 @@ public void testShouldBeGreenWhenAllPrimariesAndReplicasAreStarted() { ), List.of() ); - var service = createAllocationHealthIndicatorService(clusterState); + var service = createShardsAvailabilityIndicatorService(clusterState); assertThat( service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO), @@ -135,7 +135,7 @@ public void testShouldBeYellowWhenThereAreUnassignedReplicas() { ), List.of() ); - var service = createAllocationHealthIndicatorService(clusterState); + var service = createShardsAvailabilityIndicatorService(clusterState); assertThat( service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO), @@ -175,7 +175,7 @@ public void testShouldBeRedWhenThereAreUnassignedPrimariesAndAssignedReplicas() List.of(index("red-index", new ShardAllocation(randomNodeId(), UNAVAILABLE), new ShardAllocation(randomNodeId(), AVAILABLE))), List.of() ); - var service = createAllocationHealthIndicatorService(clusterState); + var service = createShardsAvailabilityIndicatorService(clusterState); assertThat( service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO), @@ -203,7 +203,7 @@ public void testShouldBeRedWhenThereAreUnassignedPrimariesAndAssignedReplicas() public void testShouldBeRedWhenThereAreUnassignedPrimariesAndNoReplicas() { var clusterState = createClusterStateWith(List.of(index("red-index", new ShardAllocation(randomNodeId(), UNAVAILABLE))), List.of()); - var service = createAllocationHealthIndicatorService(clusterState); + var service = createShardsAvailabilityIndicatorService(clusterState); assertThat( service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO), @@ -234,7 +234,7 @@ public void testShouldBeRedWhenThereAreUnassignedPrimariesAndUnassignedReplicasO List.of(index("red-index", new ShardAllocation(randomNodeId(), UNAVAILABLE), new ShardAllocation(randomNodeId(), UNAVAILABLE))), List.of() ); - var service = createAllocationHealthIndicatorService(clusterState); + var service = createShardsAvailabilityIndicatorService(clusterState); HealthIndicatorResult result = service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO); assertEquals(RED, result.status()); @@ -266,7 +266,7 @@ public void testShouldBeRedWhenThereAreUnassignedPrimariesAndUnassignedReplicasO List.of(), List.of() ); - var service = createAllocationHealthIndicatorService(clusterState); + var service = createShardsAvailabilityIndicatorService(clusterState); HealthIndicatorResult result = service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO); assertEquals(RED, result.status()); @@ -314,7 +314,7 @@ public void testSortByIndexPriority() { List.of(), List.of() ); - var service = createAllocationHealthIndicatorService(clusterState); + var service = createShardsAvailabilityIndicatorService(clusterState); HealthIndicatorResult result = service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO); // index-2 has the higher priority so it ought to be listed first, followed by index-1 then index-3 which have the same priority: @@ -344,7 +344,7 @@ public void testShouldBeGreenWhenThereAreRestartingReplicas() { ), List.of(new NodeShutdown("node-0", RESTART, 60)) ); - var service = createAllocationHealthIndicatorService(clusterState); + var service = createShardsAvailabilityIndicatorService(clusterState); assertThat( service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO), @@ -365,7 +365,7 @@ public void testShouldBeGreenWhenThereAreNoReplicasExpected() { List.of(index("primaries-only-index", new ShardAllocation(randomNodeId(), AVAILABLE))), List.of(new NodeShutdown("node-0", RESTART, 60)) ); - var service = createAllocationHealthIndicatorService(clusterState); + var service = createShardsAvailabilityIndicatorService(clusterState); assertThat( service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO), @@ -392,7 +392,7 @@ public void testShouldBeYellowWhenRestartingReplicasReachedAllocationDelay() { ), List.of(new NodeShutdown("node-0", RESTART, 60)) ); - var service = createAllocationHealthIndicatorService(clusterState); + var service = createShardsAvailabilityIndicatorService(clusterState); assertThat( service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO), @@ -427,7 +427,7 @@ public void testShouldBeGreenWhenThereAreInitializingPrimaries() { List.of(index("restarting-index", new ShardAllocation("node-0", INITIALIZING))), List.of() ); - var service = createAllocationHealthIndicatorService(clusterState); + var service = createShardsAvailabilityIndicatorService(clusterState); assertThat( service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO), @@ -448,7 +448,7 @@ public void testShouldBeGreenWhenThereAreRestartingPrimaries() { List.of(index("restarting-index", new ShardAllocation("node-0", RESTARTING, System.nanoTime()))), List.of(new NodeShutdown("node-0", RESTART, 60)) ); - var service = createAllocationHealthIndicatorService(clusterState); + var service = createShardsAvailabilityIndicatorService(clusterState); assertThat( service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO), @@ -474,7 +474,7 @@ public void testShouldBeRedWhenRestartingPrimariesReachedAllocationDelayAndNoRep ), List.of(new NodeShutdown("node-0", RESTART, 60)) ); - var service = createAllocationHealthIndicatorService(clusterState); + var service = createShardsAvailabilityIndicatorService(clusterState); assertThat( service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO), @@ -519,7 +519,7 @@ public void testUserActionsNotGeneratedWhenNotDrillingDown() { List.of() ); - var service = createAllocationHealthIndicatorService(clusterState); + var service = createShardsAvailabilityIndicatorService(clusterState); assertThat( service.calculate(false, HealthInfo.EMPTY_HEALTH_INFO), @@ -555,7 +555,7 @@ public void testDiagnoseRestoreIndexAfterDataLoss() { new ShardAllocation(randomNodeId(), UNAVAILABLE, noShardCopy()) ); - var service = createAllocationHealthIndicatorService(); + var service = createShardsAvailabilityIndicatorService(); List definitions = service.diagnoseUnassignedShardRouting(shardRouting, ClusterState.EMPTY_STATE); assertThat(definitions, hasSize(1)); @@ -601,7 +601,7 @@ public void testDiagnoseUnknownAllocationDeciderIssue() { MoveDecision.NOT_TAKEN ) ); - var service = createAllocationHealthIndicatorService(clusterState, decisionMap); + var service = createShardsAvailabilityIndicatorService(clusterState, decisionMap); // Get the list of user actions that are generated for this unassigned index shard ShardRouting shardRouting = clusterState.routingTable().index(indexMetadata.getIndex()).shard(0).primaryShard(); @@ -624,7 +624,7 @@ public void testDiagnoseEnableIndexAllocation() { .numberOfReplicas(0) .build(); - var service = createAllocationHealthIndicatorService(); + var service = createShardsAvailabilityIndicatorService(); // Get the list of user actions that are generated for this unassigned index shard List actions = service.checkIsAllocationDisabled( @@ -652,7 +652,7 @@ public void testDiagnoseEnableClusterAllocation() { .build(); // Disallow allocations in cluster settings - var service = createAllocationHealthIndicatorService( + var service = createShardsAvailabilityIndicatorService( Settings.builder().put(EnableAllocationDecider.CLUSTER_ROUTING_ALLOCATION_ENABLE_SETTING.getKey(), "none").build(), ClusterState.EMPTY_STATE, Map.of() @@ -689,7 +689,7 @@ public void testDiagnoseEnableRoutingAllocation() { .build(); // Disallow allocations in cluster settings - var service = createAllocationHealthIndicatorService( + var service = createShardsAvailabilityIndicatorService( Settings.builder().put(EnableAllocationDecider.CLUSTER_ROUTING_ALLOCATION_ENABLE_SETTING.getKey(), "none").build(), ClusterState.EMPTY_STATE, Map.of() @@ -726,7 +726,7 @@ public void testDiagnoseEnableDataTiers() { .numberOfReplicas(0) .build(); - var service = createAllocationHealthIndicatorService(); + var service = createShardsAvailabilityIndicatorService(); // Get the list of user actions that are generated for this unassigned index shard List actions = service.checkDataTierRelatedIssues( @@ -785,7 +785,7 @@ public void testDiagnoseIncreaseShardLimitIndexSettingInTier() { List.of(), List.of(hotNode) ); - var service = createAllocationHealthIndicatorService(); + var service = createShardsAvailabilityIndicatorService(); // Get the list of user actions that are generated for this unassigned index shard List actions = service.checkDataTierRelatedIssues( @@ -846,7 +846,7 @@ public void testDiagnoseIncreaseShardLimitClusterSettingInTier() { ); // Configure at most 1 shard per node - var service = createAllocationHealthIndicatorService( + var service = createShardsAvailabilityIndicatorService( Settings.builder().put(CLUSTER_TOTAL_SHARDS_PER_NODE_SETTING.getKey(), 1).build(), clusterState, Map.of() @@ -910,7 +910,7 @@ public void testDiagnoseIncreaseShardLimitIndexSettingInGeneral() { List.of(), List.of(dataNode) ); - var service = createAllocationHealthIndicatorService(); + var service = createShardsAvailabilityIndicatorService(); // Get the list of user actions that are generated for this unassigned index shard List actions = service.checkDataTierRelatedIssues( @@ -971,7 +971,7 @@ public void testDiagnoseIncreaseShardLimitClusterSettingInGeneral() { ); // Configure at most 1 shard per node - var service = createAllocationHealthIndicatorService( + var service = createShardsAvailabilityIndicatorService( Settings.builder().put(CLUSTER_TOTAL_SHARDS_PER_NODE_SETTING.getKey(), 1).build(), clusterState, Map.of() @@ -1010,7 +1010,7 @@ public void testDiagnoseMigrateDataRequiredToDataTiers() { .numberOfReplicas(0) .build(); - var service = createAllocationHealthIndicatorService(); + var service = createShardsAvailabilityIndicatorService(); // Get the list of user actions that are generated for this unassigned index shard List actions = service.checkDataTierRelatedIssues( @@ -1046,7 +1046,7 @@ public void testDiagnoseMigrateDataIncludedToDataTiers() { .numberOfReplicas(0) .build(); - var service = createAllocationHealthIndicatorService(); + var service = createShardsAvailabilityIndicatorService(); // Get the list of user actions that are generated for this unassigned index shard List actions = service.checkDataTierRelatedIssues( @@ -1081,7 +1081,7 @@ public void testDiagnoseOtherFilteringIssue() { .numberOfReplicas(0) .build(); - var service = createAllocationHealthIndicatorService(); + var service = createShardsAvailabilityIndicatorService(); // Get the list of user actions that are generated for this unassigned index shard List actions = service.checkDataTierRelatedIssues( @@ -1116,7 +1116,7 @@ public void testDiagnoseIncreaseTierCapacity() { .numberOfReplicas(0) .build(); - var service = createAllocationHealthIndicatorService(); + var service = createShardsAvailabilityIndicatorService(); // Get the list of user actions that are generated for this unassigned index shard List actions = service.checkDataTierRelatedIssues( @@ -1156,7 +1156,7 @@ public void testDiagnoseIncreaseNodeCapacity() { .numberOfReplicas(0) .build(); - var service = createAllocationHealthIndicatorService(); + var service = createShardsAvailabilityIndicatorService(); // Get the list of user actions that are generated for this unassigned index shard List actions = service.checkDataTierRelatedIssues( @@ -1183,6 +1183,76 @@ public void testDiagnoseIncreaseNodeCapacity() { assertThat(actions, contains(ACTION_INCREASE_NODE_CAPACITY)); } + public void testLimitNumberOfAffectedResources() { + var clusterState = createClusterStateWith( + List.of( + index("red-index1", new ShardAllocation(randomNodeId(), UNAVAILABLE), new ShardAllocation(randomNodeId(), AVAILABLE)), + index("red-index2", new ShardAllocation(randomNodeId(), UNAVAILABLE), new ShardAllocation(randomNodeId(), AVAILABLE)), + index("red-index3", new ShardAllocation(randomNodeId(), UNAVAILABLE), new ShardAllocation(randomNodeId(), AVAILABLE)), + index("red-index4", new ShardAllocation(randomNodeId(), UNAVAILABLE), new ShardAllocation(randomNodeId(), AVAILABLE)), + index("red-index5", new ShardAllocation(randomNodeId(), UNAVAILABLE), new ShardAllocation(randomNodeId(), AVAILABLE)) + ), + List.of() + ); + var service = createShardsAvailabilityIndicatorService(clusterState); + + { + // assert the full result to check that details, impacts, and symptoms use the correct count of affected indices (5) + assertThat( + service.calculate(true, 2, HealthInfo.EMPTY_HEALTH_INFO), + equalTo( + createExpectedResult( + RED, + "This cluster has 5 unavailable primary shards.", + Map.of("unassigned_primaries", 5, "started_replicas", 5), + List.of( + new HealthIndicatorImpact( + NAME, + ShardsAvailabilityHealthIndicatorService.PRIMARY_UNASSIGNED_IMPACT_ID, + 1, + "Cannot add data to 5 indices [red-index1, red-index2, red-index3, red-index4, red-index5]. Searches might " + + "return incomplete results.", + List.of(ImpactArea.INGEST, ImpactArea.SEARCH) + ) + ), + List.of( + new Diagnosis( + ACTION_CHECK_ALLOCATION_EXPLAIN_API, + List.of(new Diagnosis.Resource(INDEX, List.of("red-index1", "red-index2"))) + ) + ) + ) + ) + ); + } + + { + // larger number of affected resources + assertThat( + service.calculate(true, 2_000, HealthInfo.EMPTY_HEALTH_INFO).diagnosisList(), + equalTo( + List.of( + new Diagnosis( + ACTION_CHECK_ALLOCATION_EXPLAIN_API, + List.of( + new Diagnosis.Resource(INDEX, List.of("red-index1", "red-index2", "red-index3", "red-index4", "red-index5")) + ) + ) + ) + ) + ); + + } + + { + // 0 affected resources + assertThat( + service.calculate(true, 0, HealthInfo.EMPTY_HEALTH_INFO).diagnosisList(), + equalTo(List.of(new Diagnosis(ACTION_CHECK_ALLOCATION_EXPLAIN_API, List.of(new Diagnosis.Resource(INDEX, List.of()))))) + ); + } + } + private HealthIndicatorResult createExpectedResult( HealthStatus status, String symptom, @@ -1452,22 +1522,22 @@ private static UnassignedInfo decidersNo() { private record ShardRoutingKey(String index, int shard, boolean primary) {} - private static ShardsAvailabilityHealthIndicatorService createAllocationHealthIndicatorService() { - return createAllocationHealthIndicatorService(ClusterState.EMPTY_STATE, Collections.emptyMap()); + private static ShardsAvailabilityHealthIndicatorService createShardsAvailabilityIndicatorService() { + return createShardsAvailabilityIndicatorService(ClusterState.EMPTY_STATE, Collections.emptyMap()); } - private static ShardsAvailabilityHealthIndicatorService createAllocationHealthIndicatorService(ClusterState clusterState) { - return createAllocationHealthIndicatorService(clusterState, Collections.emptyMap()); + private static ShardsAvailabilityHealthIndicatorService createShardsAvailabilityIndicatorService(ClusterState clusterState) { + return createShardsAvailabilityIndicatorService(clusterState, Collections.emptyMap()); } - private static ShardsAvailabilityHealthIndicatorService createAllocationHealthIndicatorService( + private static ShardsAvailabilityHealthIndicatorService createShardsAvailabilityIndicatorService( ClusterState clusterState, final Map decisions ) { - return createAllocationHealthIndicatorService(Settings.EMPTY, clusterState, decisions); + return createShardsAvailabilityIndicatorService(Settings.EMPTY, clusterState, decisions); } - private static ShardsAvailabilityHealthIndicatorService createAllocationHealthIndicatorService( + private static ShardsAvailabilityHealthIndicatorService createShardsAvailabilityIndicatorService( Settings nodeSettings, ClusterState clusterState, final Map decisions diff --git a/server/src/test/java/org/elasticsearch/health/GetHealthRequestTests.java b/server/src/test/java/org/elasticsearch/health/GetHealthRequestTests.java new file mode 100644 index 000000000000..da0717967352 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/health/GetHealthRequestTests.java @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.health; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.test.ESTestCase; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +public class GetHealthRequestTests extends ESTestCase { + + public void testValidation() { + var req = new GetHealthAction.Request(true, -1); + ActionRequestValidationException validationException = req.validate(); + assertThat(validationException, notNullValue()); + assertThat(validationException.validationErrors().size(), is(1)); + assertThat(validationException.validationErrors().get(0), containsString("The size parameter must be a positive integer")); + } +} diff --git a/server/src/test/java/org/elasticsearch/health/HealthIndicatorServiceTests.java b/server/src/test/java/org/elasticsearch/health/HealthIndicatorServiceTests.java index 1d6b4505ac07..557e6fee722d 100644 --- a/server/src/test/java/org/elasticsearch/health/HealthIndicatorServiceTests.java +++ b/server/src/test/java/org/elasticsearch/health/HealthIndicatorServiceTests.java @@ -90,7 +90,7 @@ public String name() { } @Override - public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { + public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) { return null; } }; diff --git a/server/src/test/java/org/elasticsearch/health/HealthServiceTests.java b/server/src/test/java/org/elasticsearch/health/HealthServiceTests.java index 14c69ed90d37..bc1d30976bf3 100644 --- a/server/src/test/java/org/elasticsearch/health/HealthServiceTests.java +++ b/server/src/test/java/org/elasticsearch/health/HealthServiceTests.java @@ -88,6 +88,7 @@ private void assertExpectedHealthIndicatorResults( client, indicatorName, false, + 1000, getExpectedHealthIndicatorResultsActionListener(onResponseCalled, expectedHealthIndicatorResults) ); assertBusy(() -> assertThat(onResponseCalled.get(), equalTo(true))); @@ -138,11 +139,24 @@ public void testMissingIndicator() throws Exception { ); } + @SuppressWarnings("unchecked") + public void testValidateSize() { + var shardsAvailable = new HealthIndicatorResult("shards_availability", GREEN, null, null, null, null); + + var service = new HealthService(Collections.emptyList(), List.of(createMockHealthIndicatorService(shardsAvailable)), threadPool); + NodeClient client = getTestClient(HealthInfo.EMPTY_HEALTH_INFO); + IllegalArgumentException illegalArgumentException = expectThrows( + IllegalArgumentException.class, + () -> service.getHealth(client, null, true, -1, ActionListener.NOOP) + ); + assertThat(illegalArgumentException.getMessage(), is("The max number of resources must be a positive integer")); + } + private void assertGetHealthThrowsException( HealthService service, NodeClient client, String indicatorName, - boolean explain, + boolean verbose, Class expectedType, String expectedMessage, boolean expectOnFailCalled @@ -154,7 +168,7 @@ private void assertGetHealthThrowsException( expectedMessage ); try { - service.getHealth(client, indicatorName, explain, listener); + service.getHealth(client, indicatorName, verbose, 1000, listener); } catch (Throwable t) { if (expectOnFailCalled || (expectedType.isInstance(t) == false)) { throw new RuntimeException("Unexpected throwable", t); @@ -325,7 +339,7 @@ public void onFailure(Exception e) { throw new RuntimeException(e); } }; - service.getHealth(client, indicatorName, false, listener); + service.getHealth(client, indicatorName, false, 1000, listener); assertBusy(() -> assertNotNull(resultReference.get())); return resultReference.get(); } @@ -366,7 +380,7 @@ public String name() { } @Override - public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { + public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) { if (expectedHealthInfo != null) { assertThat(healthInfo, equalTo(expectedHealthInfo)); } diff --git a/server/src/test/java/org/elasticsearch/health/node/DiskHealthIndicatorServiceTests.java b/server/src/test/java/org/elasticsearch/health/node/DiskHealthIndicatorServiceTests.java index d0f04588cd53..f3358eaba9dd 100644 --- a/server/src/test/java/org/elasticsearch/health/node/DiskHealthIndicatorServiceTests.java +++ b/server/src/test/java/org/elasticsearch/health/node/DiskHealthIndicatorServiceTests.java @@ -882,6 +882,87 @@ public void testNodeHealthStatusCounts() { } } + public void testLimitNumberOfAffectedResources() { + Set otherRoles = new HashSet<>(randomNonEmptySubsetOf(OTHER_ROLES)); + Set dataRoles = new HashSet<>(randomNonEmptySubsetOf(DATA_ROLES)); + Set masterRole = Set.of(DiscoveryNodeRole.MASTER_ROLE); + Set dataNodes = createNodes(30, dataRoles); + Set masterNodes = createNodes(20, masterRole); + Set otherNodes = createNodes(10, otherRoles); + ClusterService clusterService = createClusterService(Sets.union(Sets.union(dataNodes, masterNodes), otherNodes), true); + DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService); + int numberOfRedMasterNodes = masterNodes.size(); + int numberOfRedOtherNodes = otherNodes.size(); + int numberOfYellowDataNodes = dataNodes.size(); + HealthInfo healthInfo = createHealthInfo( + List.of( + new HealthInfoConfig(HealthStatus.YELLOW, numberOfYellowDataNodes, dataNodes), + new HealthInfoConfig(HealthStatus.RED, numberOfRedMasterNodes, masterNodes), + new HealthInfoConfig(HealthStatus.RED, numberOfRedOtherNodes, otherNodes) + ) + ); + { + HealthIndicatorResult result = diskHealthIndicatorService.calculate(true, 0, healthInfo); + List diagnosisList = result.diagnosisList(); + assertThat(diagnosisList.size(), equalTo(3)); + { + Diagnosis diagnosis = diagnosisList.get(0); + List dataAffectedResources = diagnosis.affectedResources(); + assertThat(dataAffectedResources.size(), equalTo(2)); + assertThat(dataAffectedResources.get(0).getType(), is(Diagnosis.Resource.Type.NODE)); + assertThat(dataAffectedResources.get(0).getNodes().size(), is(0)); + assertThat(dataAffectedResources.get(1).getType(), is(Diagnosis.Resource.Type.INDEX)); + assertThat(dataAffectedResources.get(1).getValues().size(), is(0)); + } + { + Diagnosis diagnosis = diagnosisList.get(1); + List masterAffectedResources = diagnosis.affectedResources(); + assertThat(masterAffectedResources.size(), equalTo(1)); + assertThat(masterAffectedResources.get(0).getType(), is(Diagnosis.Resource.Type.NODE)); + assertThat(masterAffectedResources.get(0).getNodes().size(), is(0)); + } + + { + Diagnosis diagnosis = diagnosisList.get(2); + List nonDataNonMasterAffectedResources = diagnosis.affectedResources(); + assertThat(nonDataNonMasterAffectedResources.size(), equalTo(1)); + assertThat(nonDataNonMasterAffectedResources.get(0).getType(), is(Diagnosis.Resource.Type.NODE)); + assertThat(nonDataNonMasterAffectedResources.get(0).getNodes().size(), is(0)); + } + } + + { + HealthIndicatorResult result = diskHealthIndicatorService.calculate(true, 10, healthInfo); + List diagnosisList = result.diagnosisList(); + assertThat(diagnosisList.size(), equalTo(3)); + { + Diagnosis diagnosis = diagnosisList.get(0); + List dataAffectedResources = diagnosis.affectedResources(); + assertThat(dataAffectedResources.size(), equalTo(2)); + assertThat(dataAffectedResources.get(0).getType(), is(Diagnosis.Resource.Type.NODE)); + assertThat(dataAffectedResources.get(0).getNodes().size(), is(10)); + assertThat(dataAffectedResources.get(1).getType(), is(Diagnosis.Resource.Type.INDEX)); + assertThat(dataAffectedResources.get(1).getValues().size(), is(1)); + } + { + Diagnosis diagnosis = diagnosisList.get(1); + List masterAffectedResources = diagnosis.affectedResources(); + assertThat(masterAffectedResources.size(), equalTo(1)); + assertThat(masterAffectedResources.get(0).getType(), is(Diagnosis.Resource.Type.NODE)); + assertThat(masterAffectedResources.get(0).getNodes().size(), is(10)); + } + + { + Diagnosis diagnosis = diagnosisList.get(2); + List nonDataNonMasterAffectedResources = diagnosis.affectedResources(); + assertThat(nonDataNonMasterAffectedResources.size(), equalTo(1)); + assertThat(nonDataNonMasterAffectedResources.get(0).getType(), is(Diagnosis.Resource.Type.NODE)); + assertThat(nonDataNonMasterAffectedResources.get(0).getNodes().size(), is(10)); + } + } + + } + private Set createNodesWithAllRoles() { return createNodes(DiscoveryNodeRole.roles()); } diff --git a/server/src/test/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorServiceTests.java b/server/src/test/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorServiceTests.java index 002b695897ec..592b5a6c3b67 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorServiceTests.java +++ b/server/src/test/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorServiceTests.java @@ -28,6 +28,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.stream.Stream; import static org.elasticsearch.common.util.CollectionUtils.appendToCopy; import static org.elasticsearch.health.HealthStatus.GREEN; @@ -115,6 +116,42 @@ public void testIsGreenWhenNoMetadata() { ); } + public void testLimitNumberOfAffectedResources() { + List repos = Stream.iterate(0, n -> n + 1) + .limit(20) + .map(i -> createRepositoryMetadata("corrupted-repo" + i, true)) + .toList(); + var clusterState = createClusterStateWith(new RepositoriesMetadata(repos)); + var service = createRepositoryCorruptionHealthIndicatorService(clusterState); + + { + assertThat( + service.calculate(true, 10, HealthInfo.EMPTY_HEALTH_INFO).diagnosisList(), + equalTo( + List.of( + new Diagnosis( + CORRUPTED_REPOSITORY, + List.of( + new Diagnosis.Resource( + Type.SNAPSHOT_REPOSITORY, + repos.stream().limit(10).map(RepositoryMetadata::name).toList() + ) + ) + ) + ) + ) + ); + } + + { + assertThat( + service.calculate(true, 0, HealthInfo.EMPTY_HEALTH_INFO).diagnosisList(), + equalTo(List.of(new Diagnosis(CORRUPTED_REPOSITORY, List.of(new Diagnosis.Resource(Type.SNAPSHOT_REPOSITORY, List.of()))))) + ); + } + + } + private static ClusterState createClusterStateWith(RepositoriesMetadata metadata) { var builder = ClusterState.builder(new ClusterName("test-cluster")); if (metadata != null) { diff --git a/x-pack/plugin/core/src/internalClusterTest/java/org/elasticsearch/xpack/cluster/routing/allocation/DataTierShardAvailabilityHealthIndicatorIT.java b/x-pack/plugin/core/src/internalClusterTest/java/org/elasticsearch/xpack/cluster/routing/allocation/DataTierShardAvailabilityHealthIndicatorIT.java index 93479a061426..12b327cfad37 100644 --- a/x-pack/plugin/core/src/internalClusterTest/java/org/elasticsearch/xpack/cluster/routing/allocation/DataTierShardAvailabilityHealthIndicatorIT.java +++ b/x-pack/plugin/core/src/internalClusterTest/java/org/elasticsearch/xpack/cluster/routing/allocation/DataTierShardAvailabilityHealthIndicatorIT.java @@ -70,7 +70,7 @@ public void testIncreaseTierCapacityDiagnosisWhenCreated() throws Exception { ensureYellow("test"); GetHealthAction.Response healthResponse = client().execute( GetHealthAction.INSTANCE, - new GetHealthAction.Request(ShardsAvailabilityHealthIndicatorService.NAME, true) + new GetHealthAction.Request(ShardsAvailabilityHealthIndicatorService.NAME, true, 1000) ).get(); HealthIndicatorResult indicatorResult = healthResponse.findIndicator(ShardsAvailabilityHealthIndicatorService.NAME); assertThat(indicatorResult.status(), equalTo(HealthStatus.YELLOW)); @@ -107,7 +107,7 @@ public void testIncreaseTierCapacityDiagnosisWhenTierShrinksUnexpectedly() throw ensureYellow("test"); GetHealthAction.Response healthResponse = client().execute( GetHealthAction.INSTANCE, - new GetHealthAction.Request(ShardsAvailabilityHealthIndicatorService.NAME, true) + new GetHealthAction.Request(ShardsAvailabilityHealthIndicatorService.NAME, true, 1000) ).get(); ClusterAllocationExplanation explain = client().admin() .cluster() @@ -152,7 +152,7 @@ public void testRemovingNodeReturnsYellowForDelayedIndex() throws Exception { ensureYellow("test"); GetHealthAction.Response healthResponse = client().execute( GetHealthAction.INSTANCE, - new GetHealthAction.Request(ShardsAvailabilityHealthIndicatorService.NAME, true) + new GetHealthAction.Request(ShardsAvailabilityHealthIndicatorService.NAME, true, 1000) ).get(); HealthIndicatorResult indicatorResult = healthResponse.findIndicator(ShardsAvailabilityHealthIndicatorService.NAME); assertThat(indicatorResult.status(), equalTo(HealthStatus.YELLOW)); diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IlmHealthIndicatorService.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IlmHealthIndicatorService.java index 6a0411a0296e..e9e48b0099fc 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IlmHealthIndicatorService.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IlmHealthIndicatorService.java @@ -66,7 +66,7 @@ public String name() { } @Override - public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { + public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) { final ClusterState currentState = clusterService.state(); var ilmMetadata = currentState.metadata().custom(IndexLifecycleMetadata.TYPE, IndexLifecycleMetadata.EMPTY); final OperationMode currentMode = currentILMMode(currentState); diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SlmHealthIndicatorService.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SlmHealthIndicatorService.java index 773052f84293..e5854c6de092 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SlmHealthIndicatorService.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SlmHealthIndicatorService.java @@ -99,7 +99,7 @@ public String name() { } @Override - public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { + public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) { final ClusterState currentState = clusterService.state(); var slmMetadata = currentState.metadata().custom(SnapshotLifecycleMetadata.TYPE, SnapshotLifecycleMetadata.EMPTY); final OperationMode currentMode = currentSLMMode(currentState); @@ -181,7 +181,10 @@ public HealthIndicatorResult calculate(boolean verbose, HealthInfo healthInfo) { List.of( new Diagnosis.Resource( Diagnosis.Resource.Type.SLM_POLICY, - unhealthyPolicies.stream().map(SnapshotLifecyclePolicyMetadata::getName).toList() + unhealthyPolicies.stream() + .map(SnapshotLifecyclePolicyMetadata::getName) + .limit(Math.min(unhealthyPolicies.size(), maxAffectedResourcesCount)) + .toList() ) ) ) From f46a487d643a2c6adb3d68d6acad910648209512 Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Fri, 16 Dec 2022 16:48:23 +0000 Subject: [PATCH 295/919] [HealthAPI] Slm indicator reports API call for getting SLM policy details (#92166) --- .../org/elasticsearch/xpack/slm/SlmHealthIndicatorService.java | 2 +- .../elasticsearch/xpack/slm/SlmHealthIndicatorServiceTests.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SlmHealthIndicatorService.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SlmHealthIndicatorService.java index e5854c6de092..6358744f597a 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SlmHealthIndicatorService.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SlmHealthIndicatorService.java @@ -163,7 +163,7 @@ public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResources : "An automated snapshot policy is unhealthy:\n") + unhealthyPolicyCauses; String unhealthyPolicyActions = unhealthyPolicies.stream() - .map(policy -> "- /_slm/policy/" + policy.getPolicy().getId() + "?human") + .map(policy -> "- GET /_slm/policy/" + policy.getPolicy().getId() + "?human") .collect(Collectors.joining("\n")); String action = "Check the snapshot lifecycle " + (unhealthyPolicies.size() > 1 ? "policies" : "policy") diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/slm/SlmHealthIndicatorServiceTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/slm/SlmHealthIndicatorServiceTests.java index 089413d7b045..5a973c834fa8 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/slm/SlmHealthIndicatorServiceTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/slm/SlmHealthIndicatorServiceTests.java @@ -223,7 +223,7 @@ public void testIsYellowWhenPoliciesHaveFailedForMoreThanWarningThreshold() { + "] repeated failures without successful execution since [" + FORMATTER.formatMillis(execTime) + "]", - "Check the snapshot lifecycle policy for detailed failure info:\n- /_slm/policy/policy-id?human" + "Check the snapshot lifecycle policy for detailed failure info:\n- GET /_slm/policy/policy-id?human" ), List.of(new Diagnosis.Resource(Type.SLM_POLICY, List.of("test-policy"))) ) From acbd2777beac118861e7380592819b102c98e724 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Fri, 16 Dec 2022 09:20:44 -0800 Subject: [PATCH 296/919] Make FilterStreamInput less trappy (#92422) When version is set on FilterStreamInput, it is passed to the delegate stream. However, any uses of this.version directly on the stream are not correct. While it would be better to force using getVersion() consistently, this commit makes the situation less trappy by also setting the version on the wrapper stream. relates #88686 --- docs/changelog/92422.yaml | 5 +++++ .../elasticsearch/common/io/stream/FilterStreamInput.java | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 docs/changelog/92422.yaml diff --git a/docs/changelog/92422.yaml b/docs/changelog/92422.yaml new file mode 100644 index 000000000000..5d77e03e2eb8 --- /dev/null +++ b/docs/changelog/92422.yaml @@ -0,0 +1,5 @@ +pr: 92422 +summary: Make `FilterStreamInput` less trappy +area: Infra/Core +type: bug +issues: [] diff --git a/server/src/main/java/org/elasticsearch/common/io/stream/FilterStreamInput.java b/server/src/main/java/org/elasticsearch/common/io/stream/FilterStreamInput.java index 79fa2995146b..f46b6c309ae2 100644 --- a/server/src/main/java/org/elasticsearch/common/io/stream/FilterStreamInput.java +++ b/server/src/main/java/org/elasticsearch/common/io/stream/FilterStreamInput.java @@ -93,6 +93,8 @@ public Version getVersion() { @Override public void setVersion(Version version) { delegate.setVersion(version); + // also set the version on this stream directly, so that any uses of this.version are still correct + super.setVersion(version); } @Override From a852c905c942a6e782f7752c6b77c0be896e235d Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Fri, 16 Dec 2022 17:24:36 +0000 Subject: [PATCH 297/919] Don't account for the unassigned reason when diagnosing NO_VALID_SHARD_COPY (#92416) The UnassignedInfo.Reason indicates what triggered the shard unassignment, as opposed to what the course of action should be. This extends the `restore-from-snapshot` diagnosis to not take the shard unassigned info reason into account. --- docs/changelog/92416.yaml | 5 +++++ .../allocation/ShardsAvailabilityHealthIndicatorService.java | 4 +--- .../ShardsAvailabilityHealthIndicatorServiceTests.java | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 docs/changelog/92416.yaml diff --git a/docs/changelog/92416.yaml b/docs/changelog/92416.yaml new file mode 100644 index 000000000000..1a85e7b20fa3 --- /dev/null +++ b/docs/changelog/92416.yaml @@ -0,0 +1,5 @@ +pr: 92416 +summary: Don't account for the unassigned reason when diagnosing NO_VALID_SHARD_COPY +area: Health +type: bug +issues: [] diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java index f5e72c9785c7..00863ba2d59d 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java @@ -441,9 +441,7 @@ List diagnoseUnassignedShardRouting(ShardRouting shardRout LOGGER.trace("Diagnosing unassigned shard [{}] due to reason [{}]", shardRouting.shardId(), shardRouting.unassignedInfo()); switch (shardRouting.unassignedInfo().getLastAllocationStatus()) { case NO_VALID_SHARD_COPY: - if (UnassignedInfo.Reason.NODE_LEFT == shardRouting.unassignedInfo().getReason()) { - diagnosisDefs.add(ACTION_RESTORE_FROM_SNAPSHOT); - } + diagnosisDefs.add(ACTION_RESTORE_FROM_SNAPSHOT); break; case NO_ATTEMPT: if (shardRouting.unassignedInfo().isDelayed()) { diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorServiceTests.java index c7cdba228495..7682a10483e7 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorServiceTests.java @@ -1477,7 +1477,7 @@ private static String randomNodeId() { private static UnassignedInfo noShardCopy() { return new UnassignedInfo( - UnassignedInfo.Reason.NODE_LEFT, + randomBoolean() ? UnassignedInfo.Reason.NODE_LEFT : UnassignedInfo.Reason.CLUSTER_RECOVERED, null, null, 0, From 0cbe1581dedd537a6be0da2d20bcce206eabf82d Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 16 Dec 2022 18:31:00 +0000 Subject: [PATCH 298/919] Avoid capturing cluster state in TBbNA (#92255) Similar to #90724, today all `TransportBroadcastByNodeAction` implementations capture the cluster state at invocation time and hold it until completion, but most of them don't need anything from the cluster state and the couple that do certainly don't need the whole thing. This commit avoids that unnecessary capture. --- docs/changelog/92255.yaml | 5 + .../DataStreamsStatsTransportAction.java | 109 +++++++++++------- .../TransportClearIndicesCacheAction.java | 16 ++- .../forcemerge/TransportForceMergeAction.java | 19 ++- .../recovery/TransportRecoveryAction.java | 43 +++---- .../TransportIndicesSegmentsAction.java | 13 +-- .../indices/stats/IndicesStatsResponse.java | 13 ++- .../stats/TransportFieldUsageAction.java | 20 ++-- .../stats/TransportIndicesStatsAction.java | 23 ++-- .../node/TransportBroadcastByNodeAction.java | 67 ++++++----- .../stats/IndicesStatsResponseTests.java | 16 ++- .../indices/stats/IndicesStatsTests.java | 10 +- .../TransportBroadcastByNodeActionTests.java | 41 +++---- .../action/TransportForgetFollowerAction.java | 19 ++- .../TransportReloadAnalyzersAction.java | 33 +++--- ...rtClearSearchableSnapshotsCacheAction.java | 16 ++- ...ansportSearchableSnapshotsStatsAction.java | 17 ++- 17 files changed, 246 insertions(+), 234 deletions(-) create mode 100644 docs/changelog/92255.yaml diff --git a/docs/changelog/92255.yaml b/docs/changelog/92255.yaml new file mode 100644 index 000000000000..abbb68ce4903 --- /dev/null +++ b/docs/changelog/92255.yaml @@ -0,0 +1,5 @@ +pr: 92255 +summary: Avoid capturing cluster state in TBbNA +area: Stats +type: bug +issues: [] diff --git a/modules/data-streams/src/main/java/org/elasticsearch/datastreams/action/DataStreamsStatsTransportAction.java b/modules/data-streams/src/main/java/org/elasticsearch/datastreams/action/DataStreamsStatsTransportAction.java index a2c9959f62b8..4e51f605fc3c 100644 --- a/modules/data-streams/src/main/java/org/elasticsearch/datastreams/action/DataStreamsStatsTransportAction.java +++ b/modules/data-streams/src/main/java/org/elasticsearch/datastreams/action/DataStreamsStatsTransportAction.java @@ -168,18 +168,11 @@ protected DataStreamsStatsAction.DataStreamShardStats readShardResult(StreamInpu } @Override - protected DataStreamsStatsAction.Response newResponse( - DataStreamsStatsAction.Request request, - int totalShards, - int successfulShards, - int failedShards, - List dataStreamShardStats, - List shardFailures, - ClusterState clusterState - ) { + protected + TransportBroadcastByNodeAction.ResponseFactory + getResponseFactory(DataStreamsStatsAction.Request request, ClusterState clusterState) { Map aggregatedDataStreamsStats = new HashMap<>(); Set allBackingIndices = new HashSet<>(); - long totalStoreSizeBytes = 0L; SortedMap indicesLookup = clusterState.getMetadata().getIndicesLookup(); // Collect the number of backing indices from the cluster state. If every shard operation for an index fails, @@ -203,43 +196,75 @@ protected DataStreamsStatsAction.Response newResponse( } } - for (DataStreamsStatsAction.DataStreamShardStats shardStat : dataStreamShardStats) { - String indexName = shardStat.getShardRouting().getIndexName(); - IndexAbstraction indexAbstraction = indicesLookup.get(indexName); - IndexAbstraction.DataStream dataStream = indexAbstraction.getParentDataStream(); - assert dataStream != null; + return new ResponseFactory(indicesLookup, allBackingIndices, aggregatedDataStreamsStats); + } + + private class ResponseFactory + implements + TransportBroadcastByNodeAction.ResponseFactory { - // Aggregate global stats - totalStoreSizeBytes += shardStat.getStoreStats().sizeInBytes(); + private final SortedMap indicesLookup; + private final Set allBackingIndices; + private final Map aggregatedDataStreamsStats; - // Aggregate data stream stats - AggregatedStats stats = aggregatedDataStreamsStats.computeIfAbsent(dataStream.getName(), s -> new AggregatedStats()); - stats.storageBytes += shardStat.getStoreStats().sizeInBytes(); - stats.maxTimestamp = Math.max(stats.maxTimestamp, shardStat.getMaxTimestamp()); + ResponseFactory( + SortedMap indicesLookup, + Set allBackingIndices, + Map aggregatedDataStreamsStats + ) { + this.indicesLookup = indicesLookup; + this.allBackingIndices = allBackingIndices; + this.aggregatedDataStreamsStats = aggregatedDataStreamsStats; } - DataStreamsStatsAction.DataStreamStats[] dataStreamStats = aggregatedDataStreamsStats.entrySet() - .stream() - .map( - entry -> new DataStreamsStatsAction.DataStreamStats( - entry.getKey(), - entry.getValue().backingIndices.size(), - ByteSizeValue.ofBytes(entry.getValue().storageBytes), - entry.getValue().maxTimestamp + @Override + public DataStreamsStatsAction.Response newResponse( + int totalShards, + int successfulShards, + int failedShards, + List dataStreamShardStats, + List shardFailures + ) { + long totalStoreSizeBytes = 0L; + + for (DataStreamsStatsAction.DataStreamShardStats shardStat : dataStreamShardStats) { + String indexName = shardStat.getShardRouting().getIndexName(); + IndexAbstraction indexAbstraction = indicesLookup.get(indexName); + IndexAbstraction.DataStream dataStream = indexAbstraction.getParentDataStream(); + assert dataStream != null; + + // Aggregate global stats + totalStoreSizeBytes += shardStat.getStoreStats().sizeInBytes(); + + // Aggregate data stream stats + AggregatedStats stats = aggregatedDataStreamsStats.computeIfAbsent(dataStream.getName(), s -> new AggregatedStats()); + stats.storageBytes += shardStat.getStoreStats().sizeInBytes(); + stats.maxTimestamp = Math.max(stats.maxTimestamp, shardStat.getMaxTimestamp()); + } + + DataStreamsStatsAction.DataStreamStats[] dataStreamStats = aggregatedDataStreamsStats.entrySet() + .stream() + .map( + entry -> new DataStreamsStatsAction.DataStreamStats( + entry.getKey(), + entry.getValue().backingIndices.size(), + ByteSizeValue.ofBytes(entry.getValue().storageBytes), + entry.getValue().maxTimestamp + ) ) - ) - .toArray(DataStreamsStatsAction.DataStreamStats[]::new); - - return new DataStreamsStatsAction.Response( - totalShards, - successfulShards, - failedShards, - shardFailures, - aggregatedDataStreamsStats.size(), - allBackingIndices.size(), - ByteSizeValue.ofBytes(totalStoreSizeBytes), - dataStreamStats - ); + .toArray(DataStreamsStatsAction.DataStreamStats[]::new); + + return new DataStreamsStatsAction.Response( + totalShards, + successfulShards, + failedShards, + shardFailures, + aggregatedDataStreamsStats.size(), + allBackingIndices.size(), + ByteSizeValue.ofBytes(totalStoreSizeBytes), + dataStreamStats + ); + } } private static class AggregatedStats { diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/cache/clear/TransportClearIndicesCacheAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/cache/clear/TransportClearIndicesCacheAction.java index 098c0de05877..fe6c3dae7879 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/cache/clear/TransportClearIndicesCacheAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/cache/clear/TransportClearIndicesCacheAction.java @@ -10,7 +10,6 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.DefaultShardOperationFailedException; import org.elasticsearch.action.support.broadcast.node.TransportBroadcastByNodeAction; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.block.ClusterBlockException; @@ -27,7 +26,6 @@ import org.elasticsearch.transport.TransportService; import java.io.IOException; -import java.util.List; /** * Indices clear cache action. @@ -66,16 +64,16 @@ protected EmptyResult readShardResult(StreamInput in) throws IOException { } @Override - protected ClearIndicesCacheResponse newResponse( + protected ResponseFactory getResponseFactory( ClearIndicesCacheRequest request, - int totalShards, - int successfulShards, - int failedShards, - List responses, - List shardFailures, ClusterState clusterState ) { - return new ClearIndicesCacheResponse(totalShards, successfulShards, failedShards, shardFailures); + return (totalShards, successfulShards, failedShards, responses, shardFailures) -> new ClearIndicesCacheResponse( + totalShards, + successfulShards, + failedShards, + shardFailures + ); } @Override diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/forcemerge/TransportForceMergeAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/forcemerge/TransportForceMergeAction.java index cd5713e69bdd..b92de4164aaa 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/forcemerge/TransportForceMergeAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/forcemerge/TransportForceMergeAction.java @@ -11,7 +11,6 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRunnable; import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.DefaultShardOperationFailedException; import org.elasticsearch.action.support.broadcast.node.TransportBroadcastByNodeAction; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.block.ClusterBlockException; @@ -30,7 +29,6 @@ import org.elasticsearch.transport.TransportService; import java.io.IOException; -import java.util.List; /** * ForceMerge index/indices action. @@ -70,16 +68,13 @@ protected EmptyResult readShardResult(StreamInput in) throws IOException { } @Override - protected ForceMergeResponse newResponse( - ForceMergeRequest request, - int totalShards, - int successfulShards, - int failedShards, - List responses, - List shardFailures, - ClusterState clusterState - ) { - return new ForceMergeResponse(totalShards, successfulShards, failedShards, shardFailures); + protected ResponseFactory getResponseFactory(ForceMergeRequest request, ClusterState clusterState) { + return (totalShards, successfulShards, failedShards, responses, shardFailures) -> new ForceMergeResponse( + totalShards, + successfulShards, + failedShards, + shardFailures + ); } @Override diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/recovery/TransportRecoveryAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/recovery/TransportRecoveryAction.java index c5d39cdb5c19..e6b1758b1c28 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/recovery/TransportRecoveryAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/recovery/TransportRecoveryAction.java @@ -10,7 +10,6 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.DefaultShardOperationFailedException; import org.elasticsearch.action.support.broadcast.node.TransportBroadcastByNodeAction; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.block.ClusterBlockException; @@ -71,33 +70,27 @@ protected RecoveryState readShardResult(StreamInput in) throws IOException { } @Override - protected RecoveryResponse newResponse( - RecoveryRequest request, - int totalShards, - int successfulShards, - int failedShards, - List responses, - List shardFailures, - ClusterState clusterState - ) { - Map> shardResponses = new HashMap<>(); - for (RecoveryState recoveryState : responses) { - if (recoveryState == null) { - continue; - } - String indexName = recoveryState.getShardId().getIndexName(); - if (shardResponses.containsKey(indexName) == false) { - shardResponses.put(indexName, new ArrayList<>()); - } - if (request.activeOnly()) { - if (recoveryState.getStage() != RecoveryState.Stage.DONE) { + protected ResponseFactory getResponseFactory(RecoveryRequest request, ClusterState clusterState) { + return (totalShards, successfulShards, failedShards, responses, shardFailures) -> { + Map> shardResponses = new HashMap<>(); + for (RecoveryState recoveryState : responses) { + if (recoveryState == null) { + continue; + } + String indexName = recoveryState.getShardId().getIndexName(); + if (shardResponses.containsKey(indexName) == false) { + shardResponses.put(indexName, new ArrayList<>()); + } + if (request.activeOnly()) { + if (recoveryState.getStage() != RecoveryState.Stage.DONE) { + shardResponses.get(indexName).add(recoveryState); + } + } else { shardResponses.get(indexName).add(recoveryState); } - } else { - shardResponses.get(indexName).add(recoveryState); } - } - return new RecoveryResponse(totalShards, successfulShards, failedShards, shardResponses, shardFailures); + return new RecoveryResponse(totalShards, successfulShards, failedShards, shardResponses, shardFailures); + }; } @Override diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/segments/TransportIndicesSegmentsAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/segments/TransportIndicesSegmentsAction.java index 470aedb2895f..0b035af943b5 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/segments/TransportIndicesSegmentsAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/segments/TransportIndicesSegmentsAction.java @@ -10,7 +10,6 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.DefaultShardOperationFailedException; import org.elasticsearch.action.support.broadcast.node.TransportBroadcastByNodeAction; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.block.ClusterBlockException; @@ -30,7 +29,6 @@ import org.elasticsearch.transport.TransportService; import java.io.IOException; -import java.util.List; public class TransportIndicesSegmentsAction extends TransportBroadcastByNodeAction< IndicesSegmentsRequest, @@ -83,17 +81,12 @@ protected ShardSegments readShardResult(StreamInput in) throws IOException { } @Override - protected IndicesSegmentResponse newResponse( + protected ResponseFactory getResponseFactory( IndicesSegmentsRequest request, - int totalShards, - int successfulShards, - int failedShards, - List results, - List shardFailures, ClusterState clusterState ) { - return new IndicesSegmentResponse( - results.toArray(new ShardSegments[results.size()]), + return (totalShards, successfulShards, failedShards, results, shardFailures) -> new IndicesSegmentResponse( + results.toArray(new ShardSegments[0]), totalShards, successfulShards, failedShards, diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsResponse.java b/server/src/main/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsResponse.java index 85c28d57820b..2c6d357443ac 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsResponse.java @@ -12,10 +12,11 @@ import org.elasticsearch.action.admin.indices.stats.IndexStats.IndexStatsBuilder; import org.elasticsearch.action.support.DefaultShardOperationFailedException; import org.elasticsearch.action.support.broadcast.ChunkedBroadcastResponse; -import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.health.ClusterIndexHealth; import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.cluster.routing.RoutingTable; import org.elasticsearch.cluster.routing.ShardRouting; import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.Iterators; @@ -64,21 +65,23 @@ public class IndicesStatsResponse extends ChunkedBroadcastResponse { int successfulShards, int failedShards, List shardFailures, - ClusterState clusterState + Metadata metadata, + RoutingTable routingTable ) { super(totalShards, successfulShards, failedShards, shardFailures); this.shards = shards; - Objects.requireNonNull(clusterState); + Objects.requireNonNull(metadata); + Objects.requireNonNull(routingTable); Objects.requireNonNull(shards); Map indexHealthModifiableMap = new HashMap<>(); Map indexStateModifiableMap = new HashMap<>(); for (ShardStats shard : shards) { Index index = shard.getShardRouting().index(); - IndexMetadata indexMetadata = clusterState.getMetadata().index(index); + IndexMetadata indexMetadata = metadata.index(index); if (indexMetadata != null) { indexHealthModifiableMap.computeIfAbsent( index.getName(), - ignored -> new ClusterIndexHealth(indexMetadata, clusterState.routingTable().index(index)).getStatus() + ignored -> new ClusterIndexHealth(indexMetadata, routingTable.index(index)).getStatus() ); indexStateModifiableMap.computeIfAbsent(index.getName(), ignored -> indexMetadata.getState()); } diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/stats/TransportFieldUsageAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/stats/TransportFieldUsageAction.java index b458b24c7b5e..9b3214787105 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/stats/TransportFieldUsageAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/stats/TransportFieldUsageAction.java @@ -10,7 +10,6 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.DefaultShardOperationFailedException; import org.elasticsearch.action.support.broadcast.node.TransportBroadcastByNodeAction; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.block.ClusterBlockException; @@ -67,20 +66,17 @@ protected FieldUsageShardResponse readShardResult(StreamInput in) throws IOExcep } @Override - protected FieldUsageStatsResponse newResponse( + protected ResponseFactory getResponseFactory( FieldUsageStatsRequest request, - int totalShards, - int successfulShards, - int failedShards, - List fieldUsages, - List shardFailures, ClusterState clusterState ) { - final Map> combined = new HashMap<>(); - for (FieldUsageShardResponse response : fieldUsages) { - combined.computeIfAbsent(response.shardRouting.shardId().getIndexName(), i -> new ArrayList<>()).add(response); - } - return new FieldUsageStatsResponse(totalShards, successfulShards, shardFailures.size(), shardFailures, combined); + return (totalShards, successfulShards, failedShards, fieldUsages, shardFailures) -> { + final Map> combined = new HashMap<>(); + for (FieldUsageShardResponse response : fieldUsages) { + combined.computeIfAbsent(response.shardRouting.shardId().getIndexName(), i -> new ArrayList<>()).add(response); + } + return new FieldUsageStatsResponse(totalShards, successfulShards, shardFailures.size(), shardFailures, combined); + }; } @Override diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/stats/TransportIndicesStatsAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/stats/TransportIndicesStatsAction.java index 75e0d3c878ab..3d799e7efc42 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/stats/TransportIndicesStatsAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/stats/TransportIndicesStatsAction.java @@ -11,7 +11,6 @@ import org.apache.lucene.store.AlreadyClosedException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.DefaultShardOperationFailedException; import org.elasticsearch.action.support.broadcast.node.TransportBroadcastByNodeAction; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.block.ClusterBlockException; @@ -35,7 +34,6 @@ import org.elasticsearch.transport.TransportService; import java.io.IOException; -import java.util.List; public class TransportIndicesStatsAction extends TransportBroadcastByNodeAction { @@ -85,22 +83,19 @@ protected ShardStats readShardResult(StreamInput in) throws IOException { } @Override - protected IndicesStatsResponse newResponse( - IndicesStatsRequest request, - int totalShards, - int successfulShards, - int failedShards, - List responses, - List shardFailures, - ClusterState clusterState - ) { - return new IndicesStatsResponse( - responses.toArray(new ShardStats[responses.size()]), + protected ResponseFactory getResponseFactory(IndicesStatsRequest request, ClusterState clusterState) { + // NB avoid capture of full cluster state + final var metadata = clusterState.getMetadata(); + final var routingTable = clusterState.routingTable(); + + return (totalShards, successfulShards, failedShards, responses, shardFailures) -> new IndicesStatsResponse( + responses.toArray(new ShardStats[0]), totalShards, successfulShards, failedShards, shardFailures, - clusterState + metadata, + routingTable ); } diff --git a/server/src/main/java/org/elasticsearch/action/support/broadcast/node/TransportBroadcastByNodeAction.java b/server/src/main/java/org/elasticsearch/action/support/broadcast/node/TransportBroadcastByNodeAction.java index 18e8b8b0f07c..3790df94fb4d 100644 --- a/server/src/main/java/org/elasticsearch/action/support/broadcast/node/TransportBroadcastByNodeAction.java +++ b/server/src/main/java/org/elasticsearch/action/support/broadcast/node/TransportBroadcastByNodeAction.java @@ -118,11 +118,10 @@ public TransportBroadcastByNodeAction( } private Response newResponse( - Request request, NodeResponseTracker nodeResponseTracker, int unavailableShardCount, Map> nodes, - ClusterState clusterState + ResponseFactory responseFactory ) throws NodeResponseTracker.DiscardedResponsesException { int totalShards = 0; int successfulShards = 0; @@ -156,7 +155,7 @@ private Response newResponse( } totalShards += unavailableShardCount; int failedShards = exceptions.size(); - return newResponse(request, totalShards, successfulShards, failedShards, broadcastByNodeResponses, exceptions, clusterState); + return responseFactory.newResponse(totalShards, successfulShards, failedShards, broadcastByNodeResponses, exceptions); } /** @@ -167,27 +166,31 @@ private Response newResponse( */ protected abstract ShardOperationResult readShardResult(StreamInput in) throws IOException; + public interface ResponseFactory { + /** + * Creates a new response to the underlying request. + * + * @param totalShards the total number of shards considered for execution of the operation + * @param successfulShards the total number of shards for which execution of the operation was successful + * @param failedShards the total number of shards for which execution of the operation failed + * @param results the per-node aggregated shard-level results + * @param shardFailures the exceptions corresponding to shard operation failures + * @return the response + */ + Response newResponse( + int totalShards, + int successfulShards, + int failedShards, + List results, + List shardFailures + ); + } + /** - * Creates a new response to the underlying request. - * - * @param request the underlying request - * @param totalShards the total number of shards considered for execution of the operation - * @param successfulShards the total number of shards for which execution of the operation was successful - * @param failedShards the total number of shards for which execution of the operation failed - * @param results the per-node aggregated shard-level results - * @param shardFailures the exceptions corresponding to shard operation failures - * @param clusterState the cluster state - * @return the response + * Create a response factory based on the requst and the cluster state captured at the time the request was handled. Implementations + * must avoid capturing the full cluster state if possible. */ - protected abstract Response newResponse( - Request request, - int totalShards, - int successfulShards, - int failedShards, - List results, - List shardFailures, - ClusterState clusterState - ); + protected abstract ResponseFactory getResponseFactory(Request request, ClusterState clusterState); /** * Deserialize a request from an input stream @@ -255,25 +258,33 @@ protected String[] resolveConcreteIndexNames(ClusterState clusterState, Request @Override protected void doExecute(Task task, Request request, ActionListener listener) { - new AsyncAction(task, request, listener).start(); + final var clusterState = clusterService.state(); + final var responseFactory = getResponseFactory(request, clusterState); + new AsyncAction(task, request, clusterState, responseFactory, listener).start(); } protected class AsyncAction implements CancellableTask.CancellationListener { private final Task task; private final Request request; private final ActionListener listener; - private final ClusterState clusterState; private final DiscoveryNodes nodes; private final Map> nodeIds; private final int unavailableShardCount; private final NodeResponseTracker nodeResponseTracker; - - protected AsyncAction(Task task, Request request, ActionListener listener) { + private final ResponseFactory responseFactory; + + protected AsyncAction( + Task task, + Request request, + ClusterState clusterState, + ResponseFactory responseFactory, + ActionListener listener + ) { this.task = task; this.request = request; this.listener = listener; + this.responseFactory = responseFactory; - clusterState = clusterService.state(); nodes = clusterState.nodes(); ClusterBlockException globalBlockException = checkGlobalBlock(clusterState, request); @@ -400,7 +411,7 @@ protected void onCompletion() { Response response = null; try { - response = newResponse(request, nodeResponseTracker, unavailableShardCount, nodeIds, clusterState); + response = newResponse(nodeResponseTracker, unavailableShardCount, nodeIds, responseFactory); } catch (NodeResponseTracker.DiscardedResponsesException e) { // We propagate the reason that the results, in this case the task cancellation, in case the listener needs to take // follow-up actions diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsResponseTests.java index cec44f8fd063..4ee3408a7e1e 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsResponseTests.java @@ -40,7 +40,15 @@ public class IndicesStatsResponseTests extends ESTestCase { public void testInvalidLevel() { - final IndicesStatsResponse response = new IndicesStatsResponse(new ShardStats[0], 0, 0, 0, null, ClusterState.EMPTY_STATE); + final IndicesStatsResponse response = new IndicesStatsResponse( + new ShardStats[0], + 0, + 0, + 0, + null, + ClusterState.EMPTY_STATE.getMetadata(), + ClusterState.EMPTY_STATE.routingTable() + ); final String level = randomAlphaOfLength(16); final ToXContent.Params params = new ToXContent.MapParams(Collections.singletonMap("level", level)); final IllegalArgumentException e = expectThrows( @@ -87,7 +95,8 @@ public void testGetIndices() { 0, 0, null, - ClusterState.EMPTY_STATE + ClusterState.EMPTY_STATE.getMetadata(), + ClusterState.EMPTY_STATE.routingTable() ); Map indexStats = indicesStatsResponse.getIndices(); @@ -125,7 +134,8 @@ public void testChunkedEncodingPerIndex() throws IOException { shards, 0, null, - ClusterState.EMPTY_STATE + ClusterState.EMPTY_STATE.getMetadata(), + ClusterState.EMPTY_STATE.routingTable() ); final ToXContent.Params paramsClusterLevel = new ToXContent.MapParams(Map.of("level", "cluster")); final var iteratorClusterLevel = indicesStatsResponse.toXContentChunked(paramsClusterLevel); diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsTests.java index 1ba21924a36c..25d04275ae28 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/stats/IndicesStatsTests.java @@ -195,6 +195,14 @@ public static IndicesStatsResponse newIndicesStatsResponse( List shardFailures, ClusterState clusterState ) { - return new IndicesStatsResponse(shards, totalShards, successfulShards, failedShards, shardFailures, clusterState); + return new IndicesStatsResponse( + shards, + totalShards, + successfulShards, + failedShards, + shardFailures, + clusterState.getMetadata(), + clusterState.routingTable() + ); } } diff --git a/server/src/test/java/org/elasticsearch/action/support/broadcast/node/TransportBroadcastByNodeActionTests.java b/server/src/test/java/org/elasticsearch/action/support/broadcast/node/TransportBroadcastByNodeActionTests.java index 44e4e80652e3..97bdf3b3fc7a 100644 --- a/server/src/test/java/org/elasticsearch/action/support/broadcast/node/TransportBroadcastByNodeActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/support/broadcast/node/TransportBroadcastByNodeActionTests.java @@ -126,33 +126,22 @@ class TestTransportBroadcastByNodeAction extends TransportBroadcastByNodeAction< Writeable.Reader request, String executor ) { - super( - "indices:admin/test", - TransportBroadcastByNodeActionTests.this.clusterService, - transportService, - actionFilters, - indexNameExpressionResolver, - request, - executor - ); + super("indices:admin/test", clusterService, transportService, actionFilters, indexNameExpressionResolver, request, executor); } @Override - protected EmptyResult readShardResult(StreamInput in) throws IOException { + protected EmptyResult readShardResult(StreamInput in) { return EmptyResult.readEmptyResultFrom(in); } @Override - protected Response newResponse( - Request request, - int totalShards, - int successfulShards, - int failedShards, - List emptyResults, - List shardFailures, - ClusterState clusterState - ) { - return new Response(totalShards, successfulShards, failedShards, shardFailures); + protected ResponseFactory getResponseFactory(Request request, ClusterState clusterState) { + return (totalShards, successfulShards, failedShards, emptyResults, shardFailures) -> new Response( + totalShards, + successfulShards, + failedShards, + shardFailures + ); } @Override @@ -301,7 +290,7 @@ public void testGlobalBlock() { .addGlobalBlock(new ClusterBlock(1, "test-block", false, true, false, RestStatus.SERVICE_UNAVAILABLE, ClusterBlockLevel.ALL)); setState(clusterService, ClusterState.builder(clusterService.state()).blocks(block)); try { - action.new AsyncAction(null, request, listener).start(); + action.doExecute(null, request, listener); fail("expected ClusterBlockException"); } catch (ClusterBlockException expected) { assertEquals("blocked by: [SERVICE_UNAVAILABLE/1/test-block];", expected.getMessage()); @@ -319,7 +308,7 @@ public void testRequestBlock() { ); setState(clusterService, ClusterState.builder(clusterService.state()).blocks(block)); try { - action.new AsyncAction(null, request, listener).start(); + action.doExecute(null, request, listener); fail("expected ClusterBlockException"); } catch (ClusterBlockException expected) { assertEquals("index [" + TEST_INDEX + "] blocked by: [SERVICE_UNAVAILABLE/1/test-block];", expected.getMessage()); @@ -330,7 +319,7 @@ public void testOneRequestIsSentToEachNodeHoldingAShard() { Request request = new Request(new String[] { TEST_INDEX }); PlainActionFuture listener = new PlainActionFuture<>(); - action.new AsyncAction(null, request, listener).start(); + action.doExecute(null, request, listener); Map> capturedRequests = transport.getCapturedRequestsByTargetNodeAndClear(); ShardsIterator shardIt = clusterService.state().routingTable().allShards(new String[] { TEST_INDEX }); @@ -388,7 +377,7 @@ public void testRequestsAreNotSentToFailedMaster() { setState(clusterService, ClusterState.builder(clusterService.state()).nodes(builder)); - action.new AsyncAction(null, request, listener).start(); + action.doExecute(null, request, listener); Map> capturedRequests = transport.getCapturedRequestsByTargetNodeAndClear(); @@ -481,7 +470,7 @@ public void testResultAggregation() throws ExecutionException, InterruptedExcept setState(clusterService, ClusterState.builder(clusterService.state()).nodes(builder)); } - action.new AsyncAction(null, request, listener).start(); + action.doExecute(null, request, listener); Map> capturedRequests = transport.getCapturedRequestsByTargetNodeAndClear(); ShardsIterator shardIt = clusterService.state().getRoutingTable().allShards(new String[] { TEST_INDEX }); @@ -539,7 +528,7 @@ public void testNoResultAggregationIfTaskCancelled() { PlainActionFuture listener = new PlainActionFuture<>(); final CancellableTask task = new CancellableTask(randomLong(), "transport", "action", "", null, emptyMap()); TransportBroadcastByNodeAction.AsyncAction asyncAction = - action.new AsyncAction(task, request, listener); + action.new AsyncAction(task, request, clusterService.state(), null, listener); asyncAction.start(); Map> capturedRequests = transport.getCapturedRequestsByTargetNodeAndClear(); int cancelAt = randomIntBetween(0, Math.max(0, capturedRequests.size() - 2)); diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportForgetFollowerAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportForgetFollowerAction.java index 70eeff821d1f..050dbb81da65 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportForgetFollowerAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportForgetFollowerAction.java @@ -10,7 +10,6 @@ import org.elasticsearch.Assertions; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.DefaultShardOperationFailedException; import org.elasticsearch.action.support.broadcast.BroadcastResponse; import org.elasticsearch.action.support.broadcast.node.TransportBroadcastByNodeAction; import org.elasticsearch.action.support.replication.ReplicationResponse; @@ -76,16 +75,16 @@ protected EmptyResult readShardResult(final StreamInput in) { } @Override - protected BroadcastResponse newResponse( - final ForgetFollowerAction.Request request, - final int totalShards, - final int successfulShards, - final int failedShards, - List emptyResults, - final List shardFailures, - final ClusterState clusterState + protected ResponseFactory getResponseFactory( + ForgetFollowerAction.Request request, + ClusterState clusterState ) { - return new BroadcastResponse(totalShards, successfulShards, failedShards, shardFailures); + return (totalShards, successfulShards, failedShards, emptyResults, shardFailures) -> new BroadcastResponse( + totalShards, + successfulShards, + failedShards, + shardFailures + ); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/TransportReloadAnalyzersAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/TransportReloadAnalyzersAction.java index 9e37b9a77135..aa4e8a324615 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/TransportReloadAnalyzersAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/TransportReloadAnalyzersAction.java @@ -10,7 +10,6 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.DefaultShardOperationFailedException; import org.elasticsearch.action.support.broadcast.node.TransportBroadcastByNodeAction; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.block.ClusterBlockException; @@ -81,28 +80,24 @@ protected ReloadResult readShardResult(StreamInput in) throws IOException { } @Override - protected ReloadAnalyzersResponse newResponse( + protected ResponseFactory getResponseFactory( ReloadAnalyzersRequest request, - int totalShards, - int successfulShards, - int failedShards, - List responses, - List shardFailures, ClusterState clusterState ) { - Map reloadedIndicesDetails = new HashMap(); - for (ReloadResult result : responses) { - if (reloadedIndicesDetails.containsKey(result.index)) { - reloadedIndicesDetails.get(result.index).merge(result); - ; - } else { - HashSet nodeIds = new HashSet(); - nodeIds.add(result.nodeId); - ReloadDetails details = new ReloadDetails(result.index, nodeIds, new HashSet(result.reloadedSearchAnalyzers)); - reloadedIndicesDetails.put(result.index, details); + return (totalShards, successfulShards, failedShards, responses, shardFailures) -> { + Map reloadedIndicesDetails = new HashMap<>(); + for (ReloadResult result : responses) { + if (reloadedIndicesDetails.containsKey(result.index)) { + reloadedIndicesDetails.get(result.index).merge(result); + } else { + HashSet nodeIds = new HashSet<>(); + nodeIds.add(result.nodeId); + ReloadDetails details = new ReloadDetails(result.index, nodeIds, new HashSet<>(result.reloadedSearchAnalyzers)); + reloadedIndicesDetails.put(result.index, details); + } } - } - return new ReloadAnalyzersResponse(totalShards, successfulShards, failedShards, shardFailures, reloadedIndicesDetails); + return new ReloadAnalyzersResponse(totalShards, successfulShards, failedShards, shardFailures, reloadedIndicesDetails); + }; } @Override diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/TransportClearSearchableSnapshotsCacheAction.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/TransportClearSearchableSnapshotsCacheAction.java index eaea799278a8..dd4b154a28d7 100644 --- a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/TransportClearSearchableSnapshotsCacheAction.java +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/TransportClearSearchableSnapshotsCacheAction.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.searchablesnapshots.action; import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.DefaultShardOperationFailedException; import org.elasticsearch.action.support.broadcast.node.TransportBroadcastByNodeAction.EmptyResult; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; @@ -22,7 +21,6 @@ import org.elasticsearch.xpack.searchablesnapshots.store.SearchableSnapshotDirectory; import java.io.IOException; -import java.util.List; public class TransportClearSearchableSnapshotsCacheAction extends AbstractTransportSearchableSnapshotsAction< ClearSearchableSnapshotsCacheRequest, @@ -58,16 +56,16 @@ protected EmptyResult readShardResult(StreamInput in) { } @Override - protected ClearSearchableSnapshotsCacheResponse newResponse( + protected ResponseFactory getResponseFactory( ClearSearchableSnapshotsCacheRequest request, - int totalShards, - int successfulShards, - int failedShards, - List responses, - List shardFailures, ClusterState clusterState ) { - return new ClearSearchableSnapshotsCacheResponse(totalShards, successfulShards, failedShards, shardFailures); + return (totalShards, successfulShards, failedShards, emptyResults, shardFailures) -> new ClearSearchableSnapshotsCacheResponse( + totalShards, + successfulShards, + failedShards, + shardFailures + ); } @Override diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/TransportSearchableSnapshotsStatsAction.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/TransportSearchableSnapshotsStatsAction.java index fd37a420fb14..3d518c4359b3 100644 --- a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/TransportSearchableSnapshotsStatsAction.java +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/TransportSearchableSnapshotsStatsAction.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.searchablesnapshots.action; import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.DefaultShardOperationFailedException; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.routing.ShardRouting; @@ -27,7 +26,6 @@ import org.elasticsearch.xpack.searchablesnapshots.store.SearchableSnapshotDirectory; import java.io.IOException; -import java.util.List; import java.util.stream.Collectors; public class TransportSearchableSnapshotsStatsAction extends AbstractTransportSearchableSnapshotsAction< @@ -62,16 +60,17 @@ protected SearchableSnapshotShardStats readShardResult(StreamInput in) throws IO } @Override - protected SearchableSnapshotsStatsResponse newResponse( + protected ResponseFactory getResponseFactory( SearchableSnapshotsStatsRequest request, - int totalShards, - int successfulShards, - int failedShards, - List shardsStats, - List shardFailures, ClusterState clusterState ) { - return new SearchableSnapshotsStatsResponse(shardsStats, totalShards, successfulShards, failedShards, shardFailures); + return (totalShards, successfulShards, failedShards, shardsStats, shardFailures) -> new SearchableSnapshotsStatsResponse( + shardsStats, + totalShards, + successfulShards, + failedShards, + shardFailures + ); } @Override From 006e2acee33fc535578fbbb271f78fd77331d909 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski <6207777+grcevski@users.noreply.github.com> Date: Sat, 17 Dec 2022 15:20:16 -0500 Subject: [PATCH 299/919] Wait until security index is ready for role mappings (#92173) --- docs/changelog/92173.yaml | 6 + .../RoleMappingFileSettingsIT.java | 9 +- .../FileSettingsRoleMappingsRestartIT.java | 150 ++++++++++++++++++ .../FileSettingsRoleMappingsStartupIT.java | 102 ++++++++++++ .../xpack/security/Security.java | 1 + .../ReservedRoleMappingAction.java | 17 +- ...lReservedSecurityStateHandlerProvider.java | 2 +- ...dUnstableSecurityStateHandlerProvider.java | 28 ++++ .../security/UnstableLocalStateSecurity.java | 97 +++++++++++ .../ReservedRoleMappingActionTests.java | 3 + ...dstate.ReservedClusterStateHandlerProvider | 1 + 11 files changed, 408 insertions(+), 8 deletions(-) create mode 100644 docs/changelog/92173.yaml create mode 100644 x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/FileSettingsRoleMappingsRestartIT.java create mode 100644 x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/FileSettingsRoleMappingsStartupIT.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/LocalReservedUnstableSecurityStateHandlerProvider.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/UnstableLocalStateSecurity.java diff --git a/docs/changelog/92173.yaml b/docs/changelog/92173.yaml new file mode 100644 index 000000000000..c8c787d78a44 --- /dev/null +++ b/docs/changelog/92173.yaml @@ -0,0 +1,6 @@ +pr: 92173 +summary: In file based settings, wait until security index is ready for role mappings +area: Infra/Core +type: bug +issues: + - 91939 diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/integration/RoleMappingFileSettingsIT.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/integration/RoleMappingFileSettingsIT.java index 5e6f63f04ff5..47603fb93662 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/integration/RoleMappingFileSettingsIT.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/integration/RoleMappingFileSettingsIT.java @@ -35,7 +35,6 @@ import org.junit.After; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -138,7 +137,7 @@ public class RoleMappingFileSettingsIT extends NativeRealmIntegTestCase { }"""; @After - public void cleanUp() throws IOException { + public void cleanUp() { ClusterUpdateSettingsResponse settingsResponse = client().admin() .cluster() .prepareUpdateSettings() @@ -164,7 +163,7 @@ private void writeJSONFile(String node, String json) throws Exception { Files.move(tempFilePath, fileSettingsService.operatorSettingsFile(), StandardCopyOption.ATOMIC_MOVE); } - private Tuple setupClusterStateListener(String node) { + private Tuple setupClusterStateListener(String node, String expectedKey) { ClusterService clusterService = internalCluster().clusterService(node); CountDownLatch savedClusterState = new CountDownLatch(1); AtomicLong metadataVersion = new AtomicLong(-1); @@ -174,7 +173,7 @@ public void clusterChanged(ClusterChangedEvent event) { ReservedStateMetadata reservedState = event.state().metadata().reservedStateMetadata().get(FileSettingsService.NAMESPACE); if (reservedState != null) { ReservedStateHandlerMetadata handlerMetadata = reservedState.handlers().get(ReservedRoleMappingAction.NAME); - if (handlerMetadata != null && handlerMetadata.keys().contains("everyone_kibana")) { + if (handlerMetadata != null && handlerMetadata.keys().contains(expectedKey)) { clusterService.removeListener(this); metadataVersion.set(event.state().metadata().version()); savedClusterState.countDown(); @@ -280,7 +279,7 @@ private void assertRoleMappingsSaveOK(CountDownLatch savedClusterState, AtomicLo public void testRoleMappingsApplied() throws Exception { ensureGreen(); - var savedClusterState = setupClusterStateListener(internalCluster().getMasterName()); + var savedClusterState = setupClusterStateListener(internalCluster().getMasterName(), "everyone_kibana"); writeJSONFile(internalCluster().getMasterName(), testJSON); assertRoleMappingsSaveOK(savedClusterState.v1(), savedClusterState.v2()); diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/FileSettingsRoleMappingsRestartIT.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/FileSettingsRoleMappingsRestartIT.java new file mode 100644 index 000000000000..05c117d08711 --- /dev/null +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/FileSettingsRoleMappingsRestartIT.java @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security; + +import org.elasticsearch.action.admin.cluster.state.ClusterStateRequest; +import org.elasticsearch.cluster.ClusterChangedEvent; +import org.elasticsearch.cluster.ClusterStateListener; +import org.elasticsearch.cluster.metadata.ReservedStateHandlerMetadata; +import org.elasticsearch.cluster.metadata.ReservedStateMetadata; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.core.Strings; +import org.elasticsearch.core.Tuple; +import org.elasticsearch.reservedstate.service.FileSettingsService; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.test.SecurityIntegTestCase; +import org.elasticsearch.xpack.core.security.action.rolemapping.GetRoleMappingsAction; +import org.elasticsearch.xpack.core.security.action.rolemapping.GetRoleMappingsRequest; +import org.elasticsearch.xpack.security.action.rolemapping.ReservedRoleMappingAction; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.notNullValue; + +@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 0, autoManageMasterNodes = false) +public class FileSettingsRoleMappingsRestartIT extends SecurityIntegTestCase { + private static AtomicLong versionCounter = new AtomicLong(1); + + private static String testJSONOnlyRoleMappings = """ + { + "metadata": { + "version": "%s", + "compatibility": "8.4.0" + }, + "state": { + "role_mappings": { + "everyone_kibana_alone": { + "enabled": true, + "roles": [ "kibana_user" ], + "rules": { "field": { "username": "*" } }, + "metadata": { + "uuid" : "b9a59ba9-6b92-4be2-bb8d-02bb270cb3a7", + "_foo": "something" + } + }, + "everyone_fleet_alone": { + "enabled": true, + "roles": [ "fleet_user" ], + "rules": { "field": { "username": "*" } }, + "metadata": { + "uuid" : "b9a59ba9-6b92-4be3-bb8d-02bb270cb3a7", + "_foo": "something_else" + } + } + } + } + }"""; + + private void writeJSONFile(String node, String json) throws Exception { + long version = versionCounter.incrementAndGet(); + + FileSettingsService fileSettingsService = internalCluster().getInstance(FileSettingsService.class, node); + + Files.deleteIfExists(fileSettingsService.operatorSettingsFile()); + + Files.createDirectories(fileSettingsService.operatorSettingsDir()); + Path tempFilePath = createTempFile(); + + logger.info("--> writing JSON config to node {} with path {}", node, tempFilePath); + logger.info(Strings.format(json, version)); + Files.write(tempFilePath, Strings.format(json, version).getBytes(StandardCharsets.UTF_8)); + Files.move(tempFilePath, fileSettingsService.operatorSettingsFile(), StandardCopyOption.ATOMIC_MOVE); + } + + private Tuple setupClusterStateListener(String node, String expectedKey) { + ClusterService clusterService = internalCluster().clusterService(node); + CountDownLatch savedClusterState = new CountDownLatch(1); + AtomicLong metadataVersion = new AtomicLong(-1); + clusterService.addListener(new ClusterStateListener() { + @Override + public void clusterChanged(ClusterChangedEvent event) { + ReservedStateMetadata reservedState = event.state().metadata().reservedStateMetadata().get(FileSettingsService.NAMESPACE); + if (reservedState != null) { + ReservedStateHandlerMetadata handlerMetadata = reservedState.handlers().get(ReservedRoleMappingAction.NAME); + if (handlerMetadata != null && handlerMetadata.keys().contains(expectedKey)) { + clusterService.removeListener(this); + metadataVersion.set(event.state().metadata().version()); + savedClusterState.countDown(); + } + } + } + }); + + return new Tuple<>(savedClusterState, metadataVersion); + } + + public void testReservedStatePersistsOnRestart() throws Exception { + internalCluster().setBootstrapMasterNodeIndex(0); + + final String masterNode = internalCluster().getMasterName(); + var savedClusterState = setupClusterStateListener(masterNode, "everyone_kibana_alone"); + + FileSettingsService masterFileSettingsService = internalCluster().getInstance(FileSettingsService.class, masterNode); + + assertTrue(masterFileSettingsService.watching()); + + logger.info("--> write some role mappings, no other file settings"); + writeJSONFile(masterNode, testJSONOnlyRoleMappings); + boolean awaitSuccessful = savedClusterState.v1().await(20, TimeUnit.SECONDS); + assertTrue(awaitSuccessful); + + logger.info("--> restart master"); + internalCluster().restartNode(masterNode); + + var clusterStateResponse = client().admin().cluster().state(new ClusterStateRequest()).actionGet(); + assertThat( + clusterStateResponse.getState() + .metadata() + .reservedStateMetadata() + .get(FileSettingsService.NAMESPACE) + .handlers() + .get(ReservedRoleMappingAction.NAME) + .keys(), + containsInAnyOrder("everyone_fleet_alone", "everyone_kibana_alone") + ); + + var request = new GetRoleMappingsRequest(); + request.setNames("everyone_kibana_alone", "everyone_fleet_alone"); + var response = client().execute(GetRoleMappingsAction.INSTANCE, request).get(); + assertTrue(response.hasMappings()); + assertThat( + Arrays.stream(response.mappings()).map(r -> r.getName()).collect(Collectors.toSet()), + allOf(notNullValue(), containsInAnyOrder("everyone_kibana_alone", "everyone_fleet_alone")) + ); + } +} diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/FileSettingsRoleMappingsStartupIT.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/FileSettingsRoleMappingsStartupIT.java new file mode 100644 index 000000000000..dffc8d26d46b --- /dev/null +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/FileSettingsRoleMappingsStartupIT.java @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security; + +import org.elasticsearch.analysis.common.CommonAnalysisPlugin; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.Strings; +import org.elasticsearch.index.mapper.extras.MapperExtrasPlugin; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.reindex.ReindexPlugin; +import org.elasticsearch.reservedstate.service.FileSettingsService; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.test.InternalSettingsPlugin; +import org.elasticsearch.transport.netty4.Netty4Plugin; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicLong; + +import static org.elasticsearch.test.NodeRoles.dataOnlyNode; + +@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 0, autoManageMasterNodes = false) +public class FileSettingsRoleMappingsStartupIT extends ESIntegTestCase { + private static AtomicLong versionCounter = new AtomicLong(1); + private static String testJSONForFailedCase = """ + { + "metadata": { + "version": "%s", + "compatibility": "8.4.0" + }, + "state": { + "role_mappings": { + "everyone_kibana_2": { + "enabled": true, + "roles": [ "kibana_user" ], + "rules": { "field": { "username": "*" } }, + "metadata": { + "uuid" : "b9a59ba9-6b92-4be2-bb8d-02bb270cb3a7", + "_foo": "something" + } + } + } + } + }"""; + + private void writeJSONFile(String node, String json) throws Exception { + long version = versionCounter.incrementAndGet(); + + FileSettingsService fileSettingsService = internalCluster().getInstance(FileSettingsService.class, node); + + Files.deleteIfExists(fileSettingsService.operatorSettingsFile()); + + Files.createDirectories(fileSettingsService.operatorSettingsDir()); + Path tempFilePath = createTempFile(); + + logger.info("--> writing JSON config to node {} with path {}", node, tempFilePath); + logger.info(Strings.format(json, version)); + Files.write(tempFilePath, Strings.format(json, version).getBytes(StandardCharsets.UTF_8)); + Files.move(tempFilePath, fileSettingsService.operatorSettingsFile(), StandardCopyOption.ATOMIC_MOVE); + } + + public void testFailsOnStartMasterNodeWithError() throws Exception { + internalCluster().setBootstrapMasterNodeIndex(0); + + String dataNode = internalCluster().startNode(Settings.builder().put(dataOnlyNode()).put("discovery.initial_state_timeout", "1s")); + logger.info("--> write some role mappings, no other file settings"); + writeJSONFile(dataNode, testJSONForFailedCase); + + logger.info("--> stop data node"); + internalCluster().stopNode(dataNode); + logger.info("--> start master node"); + assertEquals( + "unable to launch a new watch service", + expectThrows(IllegalStateException.class, () -> internalCluster().startMasterOnlyNode()).getMessage() + ); + } + + public Collection> nodePlugins() { + return Arrays.asList( + UnstableLocalStateSecurity.class, + Netty4Plugin.class, + ReindexPlugin.class, + CommonAnalysisPlugin.class, + InternalSettingsPlugin.class, + MapperExtrasPlugin.class + ); + } + + @Override + protected boolean addMockTransportService() { + return false; // security has its own transport service + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 986c877dcafe..e3606b1b2775 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -937,6 +937,7 @@ Collection createComponents( components.add(new SecurityUsageServices(realms, allRolesStore, nativeRoleMappingStore, ipFilter.get(), profileService)); reservedRoleMappingAction.set(new ReservedRoleMappingAction(nativeRoleMappingStore)); + systemIndices.getMainIndexManager().onStateRecovered(state -> reservedRoleMappingAction.get().securityIndexRecovered()); cacheInvalidatorRegistry.validate(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/rolemapping/ReservedRoleMappingAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/rolemapping/ReservedRoleMappingAction.java index 86b28056a22f..2328c8478deb 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/rolemapping/ReservedRoleMappingAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/rolemapping/ReservedRoleMappingAction.java @@ -9,6 +9,7 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.GroupedActionListener; +import org.elasticsearch.common.util.concurrent.ListenableFuture; import org.elasticsearch.reservedstate.NonStateTransformResult; import org.elasticsearch.reservedstate.ReservedClusterStateHandler; import org.elasticsearch.reservedstate.TransformState; @@ -41,6 +42,7 @@ public class ReservedRoleMappingAction implements ReservedClusterStateHandler
  • securityIndexRecoveryListener = new ListenableFuture<>(); /** * Creates a ReservedRoleMappingAction @@ -84,10 +86,17 @@ public TransformState transform(Object source, TransformState prevState) throws // non cluster state transform call. @SuppressWarnings("unchecked") var requests = prepare((List) source); - return new TransformState(prevState.state(), prevState.keys(), l -> nonStateTransform(requests, prevState, l)); + return new TransformState( + prevState.state(), + prevState.keys(), + l -> securityIndexRecoveryListener.addListener( + ActionListener.wrap(ignored -> nonStateTransform(requests, prevState, l), l::onFailure) + ) + ); } - private void nonStateTransform( + // Exposed for testing purposes + protected void nonStateTransform( Collection requests, TransformState prevState, ActionListener listener @@ -144,4 +153,8 @@ public List fromXContent(XContentParser parser) throws IO return result; } + + public void securityIndexRecovered() { + securityIndexRecoveryListener.onResponse(null); + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/LocalReservedSecurityStateHandlerProvider.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/LocalReservedSecurityStateHandlerProvider.java index e63f5993a041..619a3b73017b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/LocalReservedSecurityStateHandlerProvider.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/LocalReservedSecurityStateHandlerProvider.java @@ -20,7 +20,7 @@ * for {@link org.elasticsearch.test.ESIntegTestCase} because the Security Plugin is really LocalStateSecurity in those tests. */ public class LocalReservedSecurityStateHandlerProvider implements ReservedClusterStateHandlerProvider { - private final LocalStateSecurity plugin; + protected final LocalStateSecurity plugin; public LocalReservedSecurityStateHandlerProvider() { throw new IllegalStateException("Provider must be constructed using PluginsService"); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/LocalReservedUnstableSecurityStateHandlerProvider.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/LocalReservedUnstableSecurityStateHandlerProvider.java new file mode 100644 index 000000000000..b4a07093e49c --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/LocalReservedUnstableSecurityStateHandlerProvider.java @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security; + +import org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider; + +/** + * Mock Security Provider implementation for the {@link ReservedClusterStateHandlerProvider} service interface. This is used + * for {@link org.elasticsearch.test.ESIntegTestCase} because the Security Plugin is really LocalStateSecurity in those tests. + *

    + * Unlike {@link LocalReservedSecurityStateHandlerProvider} this implementation is mocked to implement the + * {@link UnstableLocalStateSecurity}. Separate implementation is needed, because the SPI creation code matches the constructor + * signature when instantiating. E.g. we need to match {@link UnstableLocalStateSecurity} instead of {@link LocalStateSecurity} + */ +public class LocalReservedUnstableSecurityStateHandlerProvider extends LocalReservedSecurityStateHandlerProvider { + public LocalReservedUnstableSecurityStateHandlerProvider() { + throw new IllegalStateException("Provider must be constructed using PluginsService"); + } + + public LocalReservedUnstableSecurityStateHandlerProvider(UnstableLocalStateSecurity plugin) { + super(plugin); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/UnstableLocalStateSecurity.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/UnstableLocalStateSecurity.java new file mode 100644 index 000000000000..e6321af58c56 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/UnstableLocalStateSecurity.java @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.reservedstate.NonStateTransformResult; +import org.elasticsearch.reservedstate.ReservedClusterStateHandler; +import org.elasticsearch.reservedstate.TransformState; +import org.elasticsearch.xpack.core.security.action.rolemapping.PutRoleMappingRequest; +import org.elasticsearch.xpack.core.ssl.SSLService; +import org.elasticsearch.xpack.security.action.rolemapping.ReservedRoleMappingAction; + +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +/** + * A test class that allows us to Inject new type of Reserved Handler that can + * simulate errors in saving role mappings. + *

    + * We can't use our regular path to simply make an extension of LocalStateSecurity + * in an integration test class, because the reserved handlers are injected through + * SPI. (see {@link LocalReservedUnstableSecurityStateHandlerProvider}) + */ +public class UnstableLocalStateSecurity extends LocalStateSecurity { + + public UnstableLocalStateSecurity(Settings settings, Path configPath) throws Exception { + super(settings, configPath); + // We reuse most of the initialization of LocalStateSecurity, we then just overwrite + // the security plugin with an extra method to give us a fake RoleMappingAction. + Optional security = plugins.stream().filter(p -> p instanceof Security).findFirst(); + if (security.isPresent()) { + plugins.remove(security.get()); + } + + UnstableLocalStateSecurity thisVar = this; + var action = new ReservedUnstableRoleMappingAction(); + + plugins.add(new Security(settings, super.securityExtensions()) { + @Override + protected SSLService getSslService() { + return thisVar.getSslService(); + } + + @Override + protected XPackLicenseState getLicenseState() { + return thisVar.getLicenseState(); + } + + @Override + List> reservedClusterStateHandlers() { + // pretend the security index is initialized after 2 seconds + var timer = new java.util.Timer(); + timer.schedule(new java.util.TimerTask() { + @Override + public void run() { + action.securityIndexRecovered(); + timer.cancel(); + } + }, 2_000); + return List.of(action); + } + }); + } + + public static class ReservedUnstableRoleMappingAction extends ReservedRoleMappingAction { + /** + * Creates a fake ReservedRoleMappingAction that doesn't actually use the role mapping store + */ + public ReservedUnstableRoleMappingAction() { + // we don't actually need a NativeRoleMappingStore + super(null); + } + + /** + * The nonStateTransform method is the only one that uses the native store, we simply pretend + * something has called the onFailure method of the listener. + */ + @Override + protected void nonStateTransform( + Collection requests, + TransformState prevState, + ActionListener listener + ) { + listener.onFailure(new IllegalStateException("Fake exception")); + } + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/reservedstate/ReservedRoleMappingActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/reservedstate/ReservedRoleMappingActionTests.java index b6d1a0d12679..6cdca0cb3b24 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/reservedstate/ReservedRoleMappingActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/reservedstate/ReservedRoleMappingActionTests.java @@ -76,6 +76,7 @@ public void testValidation() { ClusterState state = ClusterState.builder(new ClusterName("elasticsearch")).build(); TransformState prevState = new TransformState(state, Collections.emptySet()); ReservedRoleMappingAction action = new ReservedRoleMappingAction(nativeRoleMappingStore); + action.securityIndexRecovered(); String badPolicyJSON = """ { @@ -109,6 +110,7 @@ public void testAddRemoveRoleMapping() throws Exception { ClusterState state = ClusterState.builder(new ClusterName("elasticsearch")).build(); TransformState prevState = new TransformState(state, Collections.emptySet()); ReservedRoleMappingAction action = new ReservedRoleMappingAction(nativeRoleMappingStore); + action.securityIndexRecovered(); String emptyJSON = ""; @@ -181,6 +183,7 @@ public void testNonStateTransformWaitsOnAsyncActions() throws Exception { ClusterState state = ClusterState.builder(new ClusterName("elasticsearch")).build(); TransformState updatedState = new TransformState(state, Collections.emptySet()); ReservedRoleMappingAction action = new ReservedRoleMappingAction(nativeRoleMappingStore); + action.securityIndexRecovered(); String json = """ { diff --git a/x-pack/plugin/security/src/test/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider b/x-pack/plugin/security/src/test/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider index 3d17572429ba..77c38d302d9c 100644 --- a/x-pack/plugin/security/src/test/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider +++ b/x-pack/plugin/security/src/test/resources/META-INF/services/org.elasticsearch.reservedstate.ReservedClusterStateHandlerProvider @@ -6,3 +6,4 @@ # org.elasticsearch.xpack.security.LocalReservedSecurityStateHandlerProvider +org.elasticsearch.xpack.security.LocalReservedUnstableSecurityStateHandlerProvider From 82ed1fbcc9b3efea0104871657b2024663eef74c Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 19 Dec 2022 09:04:29 +0000 Subject: [PATCH 300/919] Clarify use of S3 lifecycle policies (#92427) Clarifies that it doesn't work to transition to Glacier tiers, nor does it work to use object expiry, and that the consequences can be severe. --- .../snapshot-restore/repository-s3.asciidoc | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/reference/snapshot-restore/repository-s3.asciidoc b/docs/reference/snapshot-restore/repository-s3.asciidoc index 097f60d36a5a..ebef18f3d247 100644 --- a/docs/reference/snapshot-restore/repository-s3.asciidoc +++ b/docs/reference/snapshot-restore/repository-s3.asciidoc @@ -327,13 +327,14 @@ include::repository-shared-settings.asciidoc[] Sets the S3 storage class for objects stored in the snapshot repository. Values may be `standard`, `reduced_redundancy`, `standard_ia`, `onezone_ia` - and `intelligent_tiering`. Defaults to `standard`. - Changing this setting on an existing repository only affects the - storage class for newly created objects, resulting in a mixed usage of - storage classes. Additionally, S3 Lifecycle Policies can be used to manage - the storage class of existing objects. Due to the extra complexity with the - Glacier class lifecycle, it is not currently supported by this - repository type. For more information about the different classes, see + and `intelligent_tiering`. Defaults to `standard`. Changing this setting on + an existing repository only affects the storage class for newly created + objects, resulting in a mixed usage of storage classes. You may use an S3 + Lifecycle Policy to adjust the storage class of existing objects in your + repository, but you must not transition objects to Glacier classes and you + must not expire objects. If you use Glacier storage classes or object + expiry then you may permanently lose access to your repository contents. + For more information about S3 storage classes, see https://docs.aws.amazon.com/AmazonS3/latest/dev/storage-class-intro.html[AWS Storage Classes Guide] From 427298a97631e53a0e1eb8c8902964855bcaedd3 Mon Sep 17 00:00:00 2001 From: Pooya Salehi Date: Mon, 19 Dec 2022 11:16:55 +0100 Subject: [PATCH 301/919] fix ScalingThreadPoolTests testScalingThreadPoolConfiguration (#92441) --- .../java/org/elasticsearch/threadpool/ThreadPool.java | 10 +++++++--- .../threadpool/ScalingThreadPoolTests.java | 5 ++++- .../org/elasticsearch/threadpool/ThreadPoolTests.java | 10 +++++----- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/threadpool/ThreadPool.java b/server/src/main/java/org/elasticsearch/threadpool/ThreadPool.java index 017ba1bae822..5950635fd319 100644 --- a/server/src/main/java/org/elasticsearch/threadpool/ThreadPool.java +++ b/server/src/main/java/org/elasticsearch/threadpool/ThreadPool.java @@ -208,8 +208,7 @@ public ThreadPool(final Settings settings, final ExecutorBuilder... customBui builders.put(Names.FLUSH, new ScalingExecutorBuilder(Names.FLUSH, 1, halfProcMaxAt5, TimeValue.timeValueMinutes(5), false)); builders.put(Names.REFRESH, new ScalingExecutorBuilder(Names.REFRESH, 1, halfProcMaxAt10, TimeValue.timeValueMinutes(5), false)); builders.put(Names.WARMER, new ScalingExecutorBuilder(Names.WARMER, 1, halfProcMaxAt5, TimeValue.timeValueMinutes(5), false)); - ByteSizeValue maxHeapSize = ByteSizeValue.ofBytes(Runtime.getRuntime().maxMemory()); - final int maxSnapshotCores = getMaxSnapshotCores(allocatedProcessors, maxHeapSize); + final int maxSnapshotCores = getMaxSnapshotThreadPoolSize(allocatedProcessors); builders.put(Names.SNAPSHOT, new ScalingExecutorBuilder(Names.SNAPSHOT, 1, maxSnapshotCores, TimeValue.timeValueMinutes(5), false)); builders.put( Names.SNAPSHOT_META, @@ -568,7 +567,12 @@ public static int searchOrGetThreadPoolSize(final int allocatedProcessors) { return ((allocatedProcessors * 3) / 2) + 1; } - static int getMaxSnapshotCores(int allocatedProcessors, final ByteSizeValue maxHeapSize) { + static int getMaxSnapshotThreadPoolSize(int allocatedProcessors) { + final ByteSizeValue maxHeapSize = ByteSizeValue.ofBytes(Runtime.getRuntime().maxMemory()); + return getMaxSnapshotThreadPoolSize(allocatedProcessors, maxHeapSize); + } + + static int getMaxSnapshotThreadPoolSize(int allocatedProcessors, final ByteSizeValue maxHeapSize) { // While on larger data nodes, larger snapshot threadpool size improves snapshotting on high latency blob stores, // smaller instances can run into OOM issues and need a smaller snapshot threadpool size. if (maxHeapSize.compareTo(new ByteSizeValue(750, ByteSizeUnit.MB)) < 0) { diff --git a/server/src/test/java/org/elasticsearch/threadpool/ScalingThreadPoolTests.java b/server/src/test/java/org/elasticsearch/threadpool/ScalingThreadPoolTests.java index 07b8a629c81e..a2e42b5a8999 100644 --- a/server/src/test/java/org/elasticsearch/threadpool/ScalingThreadPoolTests.java +++ b/server/src/test/java/org/elasticsearch/threadpool/ScalingThreadPoolTests.java @@ -30,6 +30,7 @@ import java.util.function.BiConsumer; import java.util.function.Function; +import static org.elasticsearch.threadpool.ThreadPool.getMaxSnapshotThreadPoolSize; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; @@ -73,7 +74,9 @@ public void testScalingThreadPoolConfiguration() throws InterruptedException { expectedMax = randomIntBetween(Math.max(1, core), 16); builder.put("thread_pool." + threadPoolName + ".max", expectedMax); } else { - expectedMax = threadPoolName.equals(ThreadPool.Names.SNAPSHOT) ? 10 : maxBasedOnNumberOfProcessors; + expectedMax = threadPoolName.equals(ThreadPool.Names.SNAPSHOT) + ? getMaxSnapshotThreadPoolSize(processors) + : maxBasedOnNumberOfProcessors; } final long keepAlive; diff --git a/server/src/test/java/org/elasticsearch/threadpool/ThreadPoolTests.java b/server/src/test/java/org/elasticsearch/threadpool/ThreadPoolTests.java index 2c01cfd8daa8..f4ad6075ef16 100644 --- a/server/src/test/java/org/elasticsearch/threadpool/ThreadPoolTests.java +++ b/server/src/test/java/org/elasticsearch/threadpool/ThreadPoolTests.java @@ -26,7 +26,7 @@ import static org.elasticsearch.threadpool.ThreadPool.ESTIMATED_TIME_INTERVAL_SETTING; import static org.elasticsearch.threadpool.ThreadPool.LATE_TIME_INTERVAL_WARN_THRESHOLD_SETTING; import static org.elasticsearch.threadpool.ThreadPool.assertCurrentMethodIsNotCalledRecursively; -import static org.elasticsearch.threadpool.ThreadPool.getMaxSnapshotCores; +import static org.elasticsearch.threadpool.ThreadPool.getMaxSnapshotThreadPoolSize; import static org.elasticsearch.threadpool.ThreadPool.halfAllocatedProcessorsMaxFive; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.greaterThan; @@ -335,17 +335,17 @@ public void testForceMergeThreadPoolSize() { public void testGetMaxSnapshotCores() { int allocatedProcessors = randomIntBetween(1, 16); assertThat( - getMaxSnapshotCores(allocatedProcessors, ByteSizeValue.ofMb(400)), + getMaxSnapshotThreadPoolSize(allocatedProcessors, ByteSizeValue.ofMb(400)), equalTo(halfAllocatedProcessorsMaxFive(allocatedProcessors)) ); allocatedProcessors = randomIntBetween(1, 16); assertThat( - getMaxSnapshotCores(allocatedProcessors, ByteSizeValue.ofMb(749)), + getMaxSnapshotThreadPoolSize(allocatedProcessors, ByteSizeValue.ofMb(749)), equalTo(halfAllocatedProcessorsMaxFive(allocatedProcessors)) ); allocatedProcessors = randomIntBetween(1, 16); - assertThat(getMaxSnapshotCores(allocatedProcessors, ByteSizeValue.ofMb(750)), equalTo(10)); + assertThat(getMaxSnapshotThreadPoolSize(allocatedProcessors, ByteSizeValue.ofMb(750)), equalTo(10)); allocatedProcessors = randomIntBetween(1, 16); - assertThat(getMaxSnapshotCores(allocatedProcessors, ByteSizeValue.ofGb(4)), equalTo(10)); + assertThat(getMaxSnapshotThreadPoolSize(allocatedProcessors, ByteSizeValue.ofGb(4)), equalTo(10)); } } From af0db1180507fef90e48c1e56f0f7363d6a17984 Mon Sep 17 00:00:00 2001 From: Bogdan Pintea Date: Mon, 19 Dec 2022 12:16:36 +0100 Subject: [PATCH 302/919] SQL: fix NPE on logging when not tracking total hits (#92425) Fix NPE on logging when not tracking total hits. --- docs/changelog/92425.yaml | 5 +++++ .../elasticsearch/xpack/sql/execution/search/Querier.java | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 docs/changelog/92425.yaml diff --git a/docs/changelog/92425.yaml b/docs/changelog/92425.yaml new file mode 100644 index 000000000000..da6565828a6b --- /dev/null +++ b/docs/changelog/92425.yaml @@ -0,0 +1,5 @@ +pr: 92425 +summary: Fix NPE on logging when not tracking total hits +area: SQL +type: bug +issues: [] diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/Querier.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/Querier.java index 8671328ddf1e..cd8c69004835 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/Querier.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/Querier.java @@ -216,11 +216,12 @@ protected static void logSearchResponse(SearchResponse response, Logger logger) aggsNames.append(aggs.get(i).getName() + (i + 1 == aggs.size() ? "" : ", ")); } + var totalHits = response.getHits().getTotalHits(); + var hits = totalHits != null ? "hits " + totalHits.relation + " " + totalHits.value + ", " : ""; logger.trace( - "Got search response [hits {} {}, {} aggregations: [{}], {} failed shards, {} skipped shards, " + "Got search response [{}{} aggregations: [{}], {} failed shards, {} skipped shards, " + "{} successful shards, {} total shards, took {}, timed out [{}]]", - response.getHits().getTotalHits().relation.toString(), - response.getHits().getTotalHits().value, + hits, aggs.size(), aggsNames, response.getFailedShards(), From 60a22db42e8dc82f28eefa5974e933e2dcdf086c Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Mon, 19 Dec 2022 12:45:45 +0100 Subject: [PATCH 303/919] Improve H3 Vec2d#v2dIntersect method (#92433) --- .../java/org/elasticsearch/h3/FaceIJK.java | 2 +- .../main/java/org/elasticsearch/h3/Vec2d.java | 20 ++++---- .../elasticsearch/h3/CellBoundaryTests.java | 47 ++++++++++++++++++- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/libs/h3/src/main/java/org/elasticsearch/h3/FaceIJK.java b/libs/h3/src/main/java/org/elasticsearch/h3/FaceIJK.java index 24eddd6cf365..0c21655c1973 100644 --- a/libs/h3/src/main/java/org/elasticsearch/h3/FaceIJK.java +++ b/libs/h3/src/main/java/org/elasticsearch/h3/FaceIJK.java @@ -628,7 +628,7 @@ public CellBoundary faceIjkToCellBoundary(final int res, final int start, final adjacent hexagon edge will lie completely on a single icosahedron face, and no additional vertex is required. */ - final boolean isIntersectionAtVertex = orig2d0.equals(inter) || orig2d1.equals(inter); + final boolean isIntersectionAtVertex = orig2d0.numericallyIdentical(inter) || orig2d1.numericallyIdentical(inter); if (isIntersectionAtVertex == false) { final LatLng point = inter.hex2dToGeo(this.face, adjRes, true); boundary.add(point); diff --git a/libs/h3/src/main/java/org/elasticsearch/h3/Vec2d.java b/libs/h3/src/main/java/org/elasticsearch/h3/Vec2d.java index 5aa59b534b96..0ee09604a986 100644 --- a/libs/h3/src/main/java/org/elasticsearch/h3/Vec2d.java +++ b/libs/h3/src/main/java/org/elasticsearch/h3/Vec2d.java @@ -32,6 +32,8 @@ final class Vec2d { /** sin(60') */ private static final double M_SIN60 = Constants.M_SQRT3_2; + private static final double VEC2D_RESOLUTION = 1e-7; + /** * icosahedron face centers in lat/lng radians */ @@ -257,6 +259,10 @@ static CoordIJK hex2dToCoordIJK(double x, double y) { return coordIJK; } + public boolean numericallyIdentical(Vec2d vec2d) { + return Math.abs(vec2d.x - x) < VEC2D_RESOLUTION && Math.abs(vec2d.y - y) < VEC2D_RESOLUTION; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -280,16 +286,14 @@ public int hashCode() { * @param p3 The second endpoint of the second line. */ public static Vec2d v2dIntersect(Vec2d p0, Vec2d p1, Vec2d p2, Vec2d p3) { - double[] s1 = new double[2], s2 = new double[2]; - s1[0] = p1.x - p0.x; - s1[1] = p1.y - p0.y; - s2[0] = p3.x - p2.x; - s2[1] = p3.y - p2.y; + final double s1x = p1.x - p0.x; + final double s1y = p1.y - p0.y; + final double s2x = p3.x - p2.x; + final double s2y = p3.y - p2.y; - float t; - t = (float) ((s2[0] * (p0.y - p2.y) - s2[1] * (p0.x - p2.x)) / (-s2[0] * s1[1] + s1[0] * s2[1])); + final double t = ((s2x * (p0.y - p2.y) - s2y * (p0.x - p2.x)) / (-s2x * s1y + s1x * s2y)); - return new Vec2d(p0.x + (t * s1[0]), p0.y + (t * s1[1])); + return new Vec2d(p0.x + (t * s1x), p0.y + (t * s1y)); } /** diff --git a/libs/h3/src/test/java/org/elasticsearch/h3/CellBoundaryTests.java b/libs/h3/src/test/java/org/elasticsearch/h3/CellBoundaryTests.java index 7101b0ced03b..903e4ed40ec1 100644 --- a/libs/h3/src/test/java/org/elasticsearch/h3/CellBoundaryTests.java +++ b/libs/h3/src/test/java/org/elasticsearch/h3/CellBoundaryTests.java @@ -18,6 +18,8 @@ */ package org.elasticsearch.h3; +import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.tests.geo.GeoTestUtil; import org.elasticsearch.test.ESTestCase; import java.io.BufferedReader; @@ -30,6 +32,9 @@ import java.util.StringTokenizer; import java.util.zip.GZIPInputStream; +import static org.hamcrest.Matchers.either; +import static org.hamcrest.Matchers.equalTo; + public class CellBoundaryTests extends ESTestCase { public void testRes0() throws Exception { @@ -171,8 +176,46 @@ private void processOne(String h3Address, BufferedReader reader) throws IOExcept CellBoundary boundary = H3.h3ToGeoBoundary(h3Address); assert boundary.numPoints() == points.size(); for (int i = 0; i < boundary.numPoints(); i++) { - assertEquals(h3Address, points.get(i)[0], boundary.getLatLon(i).getLatDeg(), 1e-8); - assertEquals(h3Address, points.get(i)[1], boundary.getLatLon(i).getLonDeg(), 1e-8); + assertEquals(h3Address, points.get(i)[0], boundary.getLatLon(i).getLatDeg(), 5e-7); + assertEquals(h3Address, points.get(i)[1], boundary.getLatLon(i).getLonDeg(), 5e-7); + } + } + + public void testNumericEquivalentSharedBoundary() { + // we consider boundaries numerical equivalent if after encoded them using lucene, they resolve to the same number. + long h3 = H3.geoToH3(GeoTestUtil.nextLatitude(), GeoTestUtil.nextLongitude(), randomIntBetween(0, 15)); + CellBoundary boundary = H3.h3ToGeoBoundary(h3); + for (long r : H3.hexRing(h3)) { + int count = 0; + CellBoundary ringBoundary = H3.h3ToGeoBoundary(r); + for (int i = 0; i < boundary.numPoints(); i++) { + LatLng latLng1 = boundary.getLatLon(i % boundary.numPoints()); + LatLng latLng2 = boundary.getLatLon((i + 1) % boundary.numPoints()); + int lon1 = GeoEncodingUtils.encodeLongitude(latLng1.getLonDeg()); + int lat1 = GeoEncodingUtils.encodeLatitude(latLng1.getLatDeg()); + int lon2 = GeoEncodingUtils.encodeLongitude(latLng2.getLonDeg()); + int lat2 = GeoEncodingUtils.encodeLatitude(latLng2.getLatDeg()); + if (isSharedBoundary(lon1, lat1, lon2, lat2, ringBoundary)) { + count++; + } + } + assertThat("For cell " + H3.h3ToString(h3), count, either(equalTo(1)).or(equalTo(2))); + } + } + + private boolean isSharedBoundary(int clon1, int clat1, int clon2, int clat2, CellBoundary boundary) { + for (int i = 0; i < boundary.numPoints(); i++) { + LatLng latLng1 = boundary.getLatLon(i % boundary.numPoints()); + LatLng latLng2 = boundary.getLatLon((i + 1) % boundary.numPoints()); + int lon1 = GeoEncodingUtils.encodeLongitude(latLng1.getLonDeg()); + int lat1 = GeoEncodingUtils.encodeLatitude(latLng1.getLatDeg()); + int lon2 = GeoEncodingUtils.encodeLongitude(latLng2.getLonDeg()); + int lat2 = GeoEncodingUtils.encodeLatitude(latLng2.getLatDeg()); + // edges are in opposite directions. + if (clon1 == lon2 & clat1 == lat2 && clon2 == lon1 && clat2 == lat1) { + return true; + } } + return false; } } From a422d82a073af4239cc1018da475e834b8772e08 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Mon, 19 Dec 2022 12:22:06 +0000 Subject: [PATCH 304/919] [ML] Improve anomaly detection results indexing speed (#92417) The anomaly detection results were being indexed in much smaller batches than they could be, leading to unnecessarily poor indexing performance. - We used to execute a bulk request for every bucket result seen. This is only necessary when interim results are present. When running on historical data there is no need for every bucket result to submit a new bulk request. - Category definitions and model size stats were indexed singly. These are now added to the bulk requests that index other results. However, there is a complication which is that two results with the same key may be seen in quick succession. To avoid keeping the older result rather than the newer one the bulk requests now deduplicate by document ID, such that if two results with the same document ID are added then only the later one actually gets indexed. - When receiving new quantiles, the bulk request is only submitted and the index refreshed if they are to be used for normalization. Many quantiles are superseded before they are used, and these do not need to refresh the index. --- docs/changelog/92417.yaml | 5 + .../ml/integration/AnomalyJobCRUDIT.java | 2 +- .../AutodetectResultProcessorIT.java | 6 +- .../ml/integration/EstablishedMemUsageIT.java | 24 +-- .../ml/integration/JobResultsProviderIT.java | 6 +- .../ml/annotations/AnnotationPersister.java | 14 +- .../job/persistence/JobResultsPersister.java | 186 ++++++++++-------- .../output/AutodetectResultProcessor.java | 91 +++++---- .../JobSnapshotUpgraderResultProcessor.java | 4 +- .../job/process/normalizer/Renormalizer.java | 2 +- .../ShortCircuitingRenormalizer.java | 20 +- .../persistence/JobResultsPersisterTests.java | 7 +- .../AutodetectProcessManagerTests.java | 6 +- .../AutodetectResultProcessorTests.java | 102 +++++----- .../ShortCircuitingRenormalizerTests.java | 4 +- 15 files changed, 258 insertions(+), 221 deletions(-) create mode 100644 docs/changelog/92417.yaml diff --git a/docs/changelog/92417.yaml b/docs/changelog/92417.yaml new file mode 100644 index 000000000000..b766fe0384f1 --- /dev/null +++ b/docs/changelog/92417.yaml @@ -0,0 +1,5 @@ +pr: 92417 +summary: Improve anomaly detection results indexing speed +area: Machine Learning +type: enhancement +issues: [] diff --git a/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/AnomalyJobCRUDIT.java b/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/AnomalyJobCRUDIT.java index 1be8afe96194..4a091891de0a 100644 --- a/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/AnomalyJobCRUDIT.java +++ b/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/AnomalyJobCRUDIT.java @@ -92,7 +92,7 @@ public void testUpdateModelMemoryLimitOnceEstablished() { new ModelSizeStats.Builder(jobId).setTimestamp(new Date()).setLogTime(new Date()).setModelBytes(10000000).build(), () -> false ); - jobResultsPersister.commitResultWrites(jobId); + jobResultsPersister.commitWrites(jobId, JobResultsPersister.CommitType.RESULTS); ElasticsearchStatusException iae = expectThrows( ElasticsearchStatusException.class, diff --git a/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/AutodetectResultProcessorIT.java b/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/AutodetectResultProcessorIT.java index bb5ce746d2ff..63f1b4d5fc7a 100644 --- a/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/AutodetectResultProcessorIT.java +++ b/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/AutodetectResultProcessorIT.java @@ -110,6 +110,8 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.startsWith; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -372,7 +374,7 @@ public void testParseQuantiles_GivenRenormalizationIsEnabled() throws Exception Optional persistedQuantiles = getQuantiles(); assertTrue(persistedQuantiles.isPresent()); assertEquals(quantiles, persistedQuantiles.get()); - verify(renormalizer).renormalize(quantiles); + verify(renormalizer).renormalize(eq(quantiles), any(Runnable.class)); } public void testParseQuantiles_GivenRenormalizationIsDisabled() throws Exception { @@ -389,7 +391,7 @@ public void testParseQuantiles_GivenRenormalizationIsDisabled() throws Exception Optional persistedQuantiles = getQuantiles(); assertTrue(persistedQuantiles.isPresent()); assertEquals(quantiles, persistedQuantiles.get()); - verify(renormalizer, never()).renormalize(quantiles); + verify(renormalizer, never()).renormalize(any(), any()); } public void testDeleteInterimResults() throws Exception { diff --git a/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/EstablishedMemUsageIT.java b/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/EstablishedMemUsageIT.java index 5b297846a642..e09df368ecbd 100644 --- a/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/EstablishedMemUsageIT.java +++ b/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/EstablishedMemUsageIT.java @@ -82,7 +82,7 @@ public void testEstablishedMem_givenNoStatsLongHistory() throws Exception { initClusterAndJob(jobId); createBuckets(jobId, 25); - jobResultsPersister.commitResultWrites(jobId); + jobResultsPersister.commitWrites(jobId, JobResultsPersister.CommitType.RESULTS); assertThat(queryEstablishedMemoryUsage(jobId), equalTo(0L)); } @@ -93,7 +93,7 @@ public void testEstablishedMem_givenNoStatsShortHistory() throws Exception { initClusterAndJob(jobId); createBuckets(jobId, 5); - jobResultsPersister.commitResultWrites(jobId); + jobResultsPersister.commitWrites(jobId, JobResultsPersister.CommitType.RESULTS); assertThat(queryEstablishedMemoryUsage(jobId), equalTo(0L)); } @@ -106,7 +106,7 @@ public void testEstablishedMem_givenHistoryTooShort() throws Exception { createBuckets(jobId, 19); createModelSizeStats(jobId, 1, 19000L); ModelSizeStats latestModelSizeStats = createModelSizeStats(jobId, 10, 20000L); - jobResultsPersister.commitResultWrites(jobId); + jobResultsPersister.commitWrites(jobId, JobResultsPersister.CommitType.RESULTS); assertThat(queryEstablishedMemoryUsage(jobId), equalTo(0L)); assertThat(queryEstablishedMemoryUsage(jobId, 19, latestModelSizeStats), equalTo(0L)); @@ -120,7 +120,7 @@ public void testEstablishedMem_givenHistoryJustEnoughLowVariation() throws Excep createBuckets(jobId, 20); createModelSizeStats(jobId, 1, 19000L); ModelSizeStats latestModelSizeStats = createModelSizeStats(jobId, 10, 20000L); - jobResultsPersister.commitResultWrites(jobId); + jobResultsPersister.commitWrites(jobId, JobResultsPersister.CommitType.RESULTS); assertThat(queryEstablishedMemoryUsage(jobId), equalTo(20000L)); assertThat(queryEstablishedMemoryUsage(jobId, 20, latestModelSizeStats), equalTo(20000L)); @@ -134,7 +134,7 @@ public void testEstablishedMem_givenHistoryJustEnoughAndUninitialized() throws E createBuckets(jobId, 20); createModelSizeStats(jobId, 1, 0L); ModelSizeStats latestModelSizeStats = createModelSizeStats(jobId, 10, 0L); - jobResultsPersister.commitResultWrites(jobId); + jobResultsPersister.commitWrites(jobId, JobResultsPersister.CommitType.RESULTS); assertThat(queryEstablishedMemoryUsage(jobId), equalTo(0L)); assertThat(queryEstablishedMemoryUsage(jobId, 20, latestModelSizeStats), equalTo(0L)); @@ -148,7 +148,7 @@ public void testEstablishedMem_givenHistoryJustEnoughHighVariation() throws Exce createBuckets(jobId, 20); createModelSizeStats(jobId, 1, 1000L); ModelSizeStats latestModelSizeStats = createModelSizeStats(jobId, 10, 20000L); - jobResultsPersister.commitResultWrites(jobId); + jobResultsPersister.commitWrites(jobId, JobResultsPersister.CommitType.RESULTS); assertThat(queryEstablishedMemoryUsage(jobId), equalTo(0L)); assertThat(queryEstablishedMemoryUsage(jobId, 20, latestModelSizeStats), equalTo(0L)); @@ -162,7 +162,7 @@ public void testEstablishedMem_givenLongEstablished() throws Exception { createBuckets(jobId, 25); createModelSizeStats(jobId, 1, 10000L); ModelSizeStats latestModelSizeStats = createModelSizeStats(jobId, 2, 20000L); - jobResultsPersister.commitResultWrites(jobId); + jobResultsPersister.commitWrites(jobId, JobResultsPersister.CommitType.RESULTS); assertThat(queryEstablishedMemoryUsage(jobId), equalTo(20000L)); assertThat(queryEstablishedMemoryUsage(jobId, 25, latestModelSizeStats), equalTo(20000L)); @@ -176,7 +176,7 @@ public void testEstablishedMem_givenOneRecentChange() throws Exception { createBuckets(jobId, 25); createModelSizeStats(jobId, 1, 10000L); ModelSizeStats latestModelSizeStats = createModelSizeStats(jobId, 10, 20000L); - jobResultsPersister.commitResultWrites(jobId); + jobResultsPersister.commitWrites(jobId, JobResultsPersister.CommitType.RESULTS); assertThat(queryEstablishedMemoryUsage(jobId), equalTo(20000L)); assertThat(queryEstablishedMemoryUsage(jobId, 25, latestModelSizeStats), equalTo(20000L)); @@ -189,7 +189,7 @@ public void testEstablishedMem_givenOneRecentChangeOnlyAndUninitialized() throws createBuckets(jobId, 25); ModelSizeStats latestModelSizeStats = createModelSizeStats(jobId, 10, 0L); - jobResultsPersister.commitResultWrites(jobId); + jobResultsPersister.commitWrites(jobId, JobResultsPersister.CommitType.RESULTS); assertThat(queryEstablishedMemoryUsage(jobId), equalTo(0L)); assertThat(queryEstablishedMemoryUsage(jobId, 25, latestModelSizeStats), equalTo(0L)); @@ -202,7 +202,7 @@ public void testEstablishedMem_givenOneRecentChangeOnly() throws Exception { createBuckets(jobId, 25); ModelSizeStats latestModelSizeStats = createModelSizeStats(jobId, 10, 20000L); - jobResultsPersister.commitResultWrites(jobId); + jobResultsPersister.commitWrites(jobId, JobResultsPersister.CommitType.RESULTS); assertThat(queryEstablishedMemoryUsage(jobId), equalTo(20000L)); assertThat(queryEstablishedMemoryUsage(jobId, 25, latestModelSizeStats), equalTo(20000L)); @@ -220,7 +220,7 @@ public void testEstablishedMem_givenHistoricHighVariationRecentLowVariation() th createModelSizeStats(jobId, 19, 9000L); createModelSizeStats(jobId, 30, 19000L); ModelSizeStats latestModelSizeStats = createModelSizeStats(jobId, 35, 20000L); - jobResultsPersister.commitResultWrites(jobId); + jobResultsPersister.commitWrites(jobId, JobResultsPersister.CommitType.RESULTS); assertThat(queryEstablishedMemoryUsage(jobId), equalTo(20000L)); assertThat(queryEstablishedMemoryUsage(jobId, 40, latestModelSizeStats), equalTo(20000L)); @@ -238,7 +238,7 @@ public void testEstablishedMem_givenHistoricLowVariationRecentHighVariation() th createModelSizeStats(jobId, 27, 39000L); createModelSizeStats(jobId, 30, 67000L); ModelSizeStats latestModelSizeStats = createModelSizeStats(jobId, 35, 95000L); - jobResultsPersister.commitResultWrites(jobId); + jobResultsPersister.commitWrites(jobId, JobResultsPersister.CommitType.RESULTS); assertThat(queryEstablishedMemoryUsage(jobId), equalTo(0L)); assertThat(queryEstablishedMemoryUsage(jobId, 40, latestModelSizeStats), equalTo(0L)); diff --git a/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/JobResultsProviderIT.java b/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/JobResultsProviderIT.java index 84677093ae58..a5996f517a35 100644 --- a/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/JobResultsProviderIT.java +++ b/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/JobResultsProviderIT.java @@ -468,7 +468,7 @@ public void testGetDataCountsModelSizeAndTimingStatsWithSomeDocs() throws Except ModelSizeStats storedModelSizeStats = new ModelSizeStats.Builder(job.getId()).setModelBytes(10L).build(); jobResultsPersister.persistModelSizeStats(storedModelSizeStats, () -> false); - jobResultsPersister.commitResultWrites(job.getId()); + jobResultsPersister.commitWrites(job.getId(), JobResultsPersister.CommitType.RESULTS); setOrThrow.get(); assertThat(dataCountsAtomicReference.get().getJobId(), equalTo(job.getId())); @@ -479,7 +479,7 @@ public void testGetDataCountsModelSizeAndTimingStatsWithSomeDocs() throws Except storedTimingStats.updateStats(10); jobResultsPersister.bulkPersisterBuilder(job.getId()).persistTimingStats(storedTimingStats).executeRequest(); - jobResultsPersister.commitResultWrites(job.getId()); + jobResultsPersister.commitWrites(job.getId(), JobResultsPersister.CommitType.RESULTS); setOrThrow.get(); @@ -492,7 +492,7 @@ public void testGetDataCountsModelSizeAndTimingStatsWithSomeDocs() throws Except storedDataCounts.incrementMissingFieldCount(1L); JobDataCountsPersister jobDataCountsPersister = new JobDataCountsPersister(client(), resultsPersisterService, auditor); jobDataCountsPersister.persistDataCounts(job.getId(), storedDataCounts); - jobResultsPersister.commitResultWrites(job.getId()); + jobResultsPersister.commitWrites(job.getId(), JobResultsPersister.CommitType.RESULTS); setOrThrow.get(); assertThat(dataCountsAtomicReference.get(), equalTo(storedDataCounts)); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/annotations/AnnotationPersister.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/annotations/AnnotationPersister.java index 4d45c7a8fa73..18d3c4701243 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/annotations/AnnotationPersister.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/annotations/AnnotationPersister.java @@ -66,22 +66,22 @@ public Tuple persistAnnotation(@Nullable String annotationId } public Builder bulkPersisterBuilder(String jobId) { - return new Builder(jobId); + return new Builder(jobId, () -> true); + } + + public Builder bulkPersisterBuilder(String jobId, Supplier shouldRetry) { + return new Builder(jobId, shouldRetry); } public class Builder { private final String jobId; private BulkRequest bulkRequest = new BulkRequest(AnnotationIndex.WRITE_ALIAS_NAME); - private Supplier shouldRetry = () -> true; + private final Supplier shouldRetry; - private Builder(String jobId) { + private Builder(String jobId, Supplier shouldRetry) { this.jobId = Objects.requireNonNull(jobId); - } - - public Builder shouldRetry(Supplier shouldRetry) { this.shouldRetry = Objects.requireNonNull(shouldRetry); - return this; } public Builder persistAnnotation(Annotation annotation) { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsPersister.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsPersister.java index e65d78749f6b..57b906990df9 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsPersister.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsPersister.java @@ -49,9 +49,14 @@ import org.elasticsearch.xpack.ml.utils.persistence.ResultsPersisterService; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; +import java.util.EnumSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.Supplier; import static org.elasticsearch.core.Strings.format; @@ -82,30 +87,39 @@ public class JobResultsPersister { private final OriginSettingClient client; private final ResultsPersisterService resultsPersisterService; + /** + * The possible types of data that may be committed. + */ + public enum CommitType { + RESULTS, + STATE, + ANNOTATIONS + }; + public JobResultsPersister(OriginSettingClient client, ResultsPersisterService resultsPersisterService) { this.client = client; this.resultsPersisterService = resultsPersisterService; } public Builder bulkPersisterBuilder(String jobId) { - return new Builder(jobId); + return new Builder(jobId, () -> true); + } + + public Builder bulkPersisterBuilder(String jobId, Supplier shouldRetry) { + return new Builder(jobId, shouldRetry); } public class Builder { - private BulkRequest bulkRequest; + private final Map items; private final String jobId; private final String indexName; - private Supplier shouldRetry = () -> true; + private final Supplier shouldRetry; - private Builder(String jobId) { - this.bulkRequest = new BulkRequest(); + private Builder(String jobId, Supplier shouldRetry) { + this.items = new LinkedHashMap<>(); this.jobId = Objects.requireNonNull(jobId); this.indexName = AnomalyDetectorsIndex.resultsWriteAlias(jobId); - } - - public Builder shouldRetry(Supplier shouldRetry) { - this.shouldRetry = Objects.requireNonNull(shouldRetry); - return this; + this.shouldRetry = shouldRetry; } /** @@ -115,7 +129,7 @@ public Builder shouldRetry(Supplier shouldRetry) { * @param bucket The bucket to persist * @return this */ - public Builder persistBucket(Bucket bucket) { + public synchronized Builder persistBucket(Bucket bucket) { // If the supplied bucket has records then create a copy with records // removed, because we never persist nested records in buckets Bucket bucketWithoutRecords = bucket; @@ -132,7 +146,7 @@ public Builder persistBucket(Bucket bucket) { return this; } - private void persistBucketInfluencersStandalone( + private synchronized void persistBucketInfluencersStandalone( @SuppressWarnings("HiddenField") String jobId, List bucketInfluencers ) { @@ -151,7 +165,7 @@ private void persistBucketInfluencersStandalone( * @param timingStats timing stats to persist * @return this */ - public Builder persistTimingStats(TimingStats timingStats) { + public synchronized Builder persistTimingStats(TimingStats timingStats) { indexResult( TimingStats.documentId(timingStats.getJobId()), timingStats, @@ -167,7 +181,7 @@ public Builder persistTimingStats(TimingStats timingStats) { * @param records the records to persist * @return this */ - public Builder persistRecords(List records) { + public synchronized Builder persistRecords(List records) { for (AnomalyRecord record : records) { logger.trace("[{}] ES BULK ACTION: index record to index [{}] with ID [{}]", jobId, indexName, record.getId()); indexResult(record.getId(), record, "record"); @@ -183,7 +197,7 @@ public Builder persistRecords(List records) { * @param influencers the influencers to persist * @return this */ - public Builder persistInfluencers(List influencers) { + public synchronized Builder persistInfluencers(List influencers) { for (Influencer influencer : influencers) { logger.trace("[{}] ES BULK ACTION: index influencer to index [{}] with ID [{}]", jobId, indexName, influencer.getId()); indexResult(influencer.getId(), influencer, "influencer"); @@ -192,13 +206,13 @@ public Builder persistInfluencers(List influencers) { return this; } - public Builder persistModelPlot(ModelPlot modelPlot) { + public synchronized Builder persistModelPlot(ModelPlot modelPlot) { logger.trace("[{}] ES BULK ACTION: index model plot to index [{}] with ID [{}]", jobId, indexName, modelPlot.getId()); indexResult(modelPlot.getId(), modelPlot, "model plot"); return this; } - public Builder persistCategorizerStats(CategorizerStats categorizerStats) { + public synchronized Builder persistCategorizerStats(CategorizerStats categorizerStats) { logger.trace( "[{}] ES BULK ACTION: index categorizer stats to index [{}] with ID [{}]", jobId, @@ -209,20 +223,42 @@ public Builder persistCategorizerStats(CategorizerStats categorizerStats) { return this; } - public Builder persistForecast(Forecast forecast) { + public synchronized Builder persistCategoryDefinition(CategoryDefinition categoryDefinition) { + logger.trace( + "[{}] ES BULK ACTION: index category definition to index [{}] with ID [{}]", + jobId, + indexName, + categoryDefinition.getId() + ); + indexResult(categoryDefinition.getId(), categoryDefinition, "category definition"); + return this; + } + + public synchronized Builder persistModelSizeStats(ModelSizeStats modelSizeStats) { + logger.trace( + "[{}] ES BULK ACTION: index model size stats to index [{}] with ID [{}]", + jobId, + indexName, + modelSizeStats.getId() + ); + indexResult(modelSizeStats.getId(), modelSizeStats, "model size stats"); + return this; + } + + public synchronized Builder persistForecast(Forecast forecast) { logger.trace("[{}] ES BULK ACTION: index forecast to index [{}] with ID [{}]", jobId, indexName, forecast.getId()); indexResult(forecast.getId(), forecast, Forecast.RESULT_TYPE_VALUE); return this; } - public Builder persistForecastRequestStats(ForecastRequestStats forecastRequestStats) { + public synchronized Builder persistForecastRequestStats(ForecastRequestStats forecastRequestStats) { logger.trace( "[{}] ES BULK ACTION: index forecast request stats to index [{}] with ID [{}]", jobId, indexName, forecastRequestStats.getId() ); - indexResult(forecastRequestStats.getId(), forecastRequestStats, Forecast.RESULT_TYPE_VALUE); + indexResult(forecastRequestStats.getId(), forecastRequestStats, "forecast request stats"); return this; } @@ -232,12 +268,12 @@ private void indexResult(String id, ToXContent resultDoc, String resultType) { private void indexResult(String id, ToXContent resultDoc, ToXContent.Params params, String resultType) { try (XContentBuilder content = toXContentBuilder(resultDoc, params)) { - bulkRequest.add(new IndexRequest(indexName).id(id).source(content)); + items.put(id, new IndexRequest(indexName).id(id).source(content)); } catch (IOException e) { logger.error(() -> format("[%s] Error serialising %s", jobId, resultType), e); } - if (bulkRequest.numberOfActions() >= JobRenormalizedResultsPersister.BULK_LIMIT) { + if (items.size() >= JobRenormalizedResultsPersister.BULK_LIMIT) { executeRequest(); } } @@ -245,45 +281,36 @@ private void indexResult(String id, ToXContent resultDoc, ToXContent.Params para /** * Execute the bulk action */ - public void executeRequest() { - if (bulkRequest.numberOfActions() == 0) { + public synchronized void executeRequest() { + if (items.isEmpty()) { return; } - logger.trace("[{}] ES API CALL: bulk request with {} actions", jobId, bulkRequest.numberOfActions()); + logger.trace("[{}] ES API CALL: bulk request with {} actions", jobId, items.size()); resultsPersisterService.bulkIndexWithRetry( - bulkRequest, + buildBulkRequest(), jobId, shouldRetry, retryMessage -> logger.debug("[{}] Bulk indexing of results failed {}", jobId, retryMessage) ); - bulkRequest = new BulkRequest(); + clear(); } - public void clearBulkRequest() { - bulkRequest = new BulkRequest(); + private BulkRequest buildBulkRequest() { + BulkRequest bulkRequest = new BulkRequest(); + for (IndexRequest item : items.values()) { + bulkRequest.add(item); + } + return bulkRequest; } - // for testing - BulkRequest getBulkRequest() { - return bulkRequest; + public synchronized void clear() { + items.clear(); } - } - /** - * Persist the category definition - * - * @param category The category to be persisted - */ - public void persistCategoryDefinition(CategoryDefinition category, Supplier shouldRetry) { - Persistable persistable = new Persistable( - AnomalyDetectorsIndex.resultsWriteAlias(category.getJobId()), - category.getJobId(), - category, - category.getId() - ); - persistable.persist(shouldRetry, true); - // Don't commit as we expect masses of these updates and they're not - // read again by this process + // for testing + synchronized BulkRequest getBulkRequest() { + return buildBulkRequest(); + } } /** @@ -411,52 +438,47 @@ public void deleteInterimResults(String jobId) { * Once all the job data has been written this function will be * called to commit the writes to the datastore. * - * @param jobId The job Id + * @param jobId The job ID. + * @param commitType Which type of data will be committed? */ - public void commitResultWrites(String jobId) { - // We refresh using the read alias in order to ensure all indices will - // be refreshed even if a rollover occurs in between. - String indexName = AnomalyDetectorsIndex.jobResultsAliasedName(jobId); - - // Refresh should wait for Lucene to make the data searchable - logger.trace("[{}] ES API CALL: refresh index {}", jobId, indexName); - RefreshRequest refreshRequest = new RefreshRequest(indexName); - refreshRequest.indicesOptions(IndicesOptions.lenientExpandOpen()); - try (ThreadContext.StoredContext ignore = client.threadPool().getThreadContext().stashWithOrigin(ML_ORIGIN)) { - client.admin().indices().refresh(refreshRequest).actionGet(); - } + public void commitWrites(String jobId, CommitType commitType) { + commitWrites(jobId, EnumSet.of(commitType)); } /** - * Makes annotations searchable as they are considered part of a job's results - * to fulfil the contract that job results are searchable immediately after a - * close or flush. + * Once all the job data has been written this function will be + * called to commit the writes to the datastore. + * + * @param jobId The job ID. + * @param commitTypes Which type(s) of data will be committed? */ - public void commitAnnotationWrites() { - // We refresh using the read alias in order to ensure all indices will - // be refreshed even if a rollover occurs in between. - RefreshRequest refreshRequest = new RefreshRequest(AnnotationIndex.READ_ALIAS_NAME); - refreshRequest.indicesOptions(IndicesOptions.lenientExpandOpen()); - try (ThreadContext.StoredContext ignore = client.threadPool().getThreadContext().stashWithOrigin(ML_ORIGIN)) { - client.admin().indices().refresh(refreshRequest).actionGet(); + public void commitWrites(String jobId, Set commitTypes) { + if (commitTypes.isEmpty()) { + return; + } + List indexNames = new ArrayList<>(); + if (commitTypes.contains(CommitType.RESULTS)) { + // We refresh using the read alias in order to ensure all indices will + // be refreshed even if a rollover occurs in between. + indexNames.add(AnomalyDetectorsIndex.jobResultsAliasedName(jobId)); + } + if (commitTypes.contains(CommitType.STATE)) { + indexNames.add(AnomalyDetectorsIndex.jobStateIndexPattern()); + } + if (commitTypes.contains(CommitType.ANNOTATIONS)) { + // We refresh using the read alias in order to ensure all indices will + // be refreshed even if a rollover occurs in between. + indexNames.add(AnnotationIndex.READ_ALIAS_NAME); } - } - /** - * Once the job state has been written calling this function makes it - * immediately searchable. - * - * @param jobId The job Id - * */ - public void commitStateWrites(String jobId) { - String indexName = AnomalyDetectorsIndex.jobStateIndexPattern(); // Refresh should wait for Lucene to make the data searchable - logger.trace("[{}] ES API CALL: refresh index {}", jobId, indexName); - RefreshRequest refreshRequest = new RefreshRequest(indexName); + logger.trace("[{}] ES API CALL: refresh indices {}", jobId, indexNames); + RefreshRequest refreshRequest = new RefreshRequest(indexNames.toArray(String[]::new)); refreshRequest.indicesOptions(IndicesOptions.lenientExpandOpen()); try (ThreadContext.StoredContext ignore = client.threadPool().getThreadContext().stashWithOrigin(ML_ORIGIN)) { client.admin().indices().refresh(refreshRequest).actionGet(); } + logger.trace("[{}] ES API CALL: finished refresh indices {}", jobId, indexNames); } /** diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/output/AutodetectResultProcessor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/output/AutodetectResultProcessor.java index f240104c285a..ff32f0cc72d6 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/output/AutodetectResultProcessor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/output/AutodetectResultProcessor.java @@ -47,6 +47,7 @@ import java.time.Clock; import java.time.Duration; import java.util.Date; +import java.util.EnumSet; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -68,7 +69,7 @@ *

    * Has methods to register and remove alert observers. * Also has a method to wait for a flush to be complete. - * + *

    * Buckets are the written last after records, influencers etc * when the end of bucket is reached. Therefore results aren't persisted * until the bucket is read, this means that interim results for all @@ -80,7 +81,7 @@ */ public class AutodetectResultProcessor { - private static final Logger LOGGER = LogManager.getLogger(AutodetectResultProcessor.class); + private static final Logger logger = LogManager.getLogger(AutodetectResultProcessor.class); private final Client client; private final AnomalyDetectionAuditor auditor; @@ -157,8 +158,8 @@ public AutodetectResultProcessor( this.process = Objects.requireNonNull(autodetectProcess); this.flushListener = Objects.requireNonNull(flushListener); this.latestModelSizeStats = Objects.requireNonNull(latestModelSizeStats); - this.bulkResultsPersister = persister.bulkPersisterBuilder(jobId).shouldRetry(this::isAlive); - this.bulkAnnotationsPersister = annotationPersister.bulkPersisterBuilder(jobId).shouldRetry(this::isAlive); + this.bulkResultsPersister = persister.bulkPersisterBuilder(jobId, this::isAlive); + this.bulkAnnotationsPersister = annotationPersister.bulkPersisterBuilder(jobId, this::isAlive); this.timingStatsReporter = new TimingStatsReporter(timingStats, bulkResultsPersister); this.clock = Objects.requireNonNull(clock); this.deleteInterimRequired = true; @@ -181,9 +182,9 @@ public void process() { bulkAnnotationsPersister.executeRequest(); } } catch (Exception e) { - LOGGER.warn(() -> "[" + jobId + "] Error persisting autodetect results", e); + logger.warn(() -> "[" + jobId + "] Error persisting autodetect results", e); } - LOGGER.info("[{}] {} buckets parsed from autodetect output", jobId, currentRunBucketCount); + logger.info("[{}] {} buckets parsed from autodetect output", jobId, currentRunBucketCount); } catch (Exception e) { failed = true; @@ -193,14 +194,14 @@ public void process() { // that it would have been better to close jobs before shutting down, // but we now fully expect jobs to move between nodes without doing // all their graceful close activities. - LOGGER.warn("[{}] some results not processed due to the process being killed", jobId); + logger.warn("[{}] some results not processed due to the process being killed", jobId); } else if (process.isProcessAliveAfterWaiting() == false) { // Don't log the stack trace to not shadow the root cause. - LOGGER.warn("[{}] some results not processed due to the termination of autodetect", jobId); + logger.warn("[{}] some results not processed due to the termination of autodetect", jobId); } else { // We should only get here if the iterator throws in which // case parsing the autodetect output has failed. - LOGGER.error(() -> "[" + jobId + "] error parsing autodetect output", e); + logger.error(() -> "[" + jobId + "] error parsing autodetect output", e); } } finally { flushListener.clear(); @@ -218,13 +219,13 @@ private void readResults() { AutodetectResult result = iterator.next(); processResult(result); if (result.getBucket() != null) { - LOGGER.trace("[{}] Bucket number {} parsed from output", jobId, currentRunBucketCount); + logger.trace("[{}] Bucket number {} parsed from output", jobId, currentRunBucketCount); } } catch (Exception e) { if (isAlive() == false) { throw e; } - LOGGER.warn(() -> "[" + jobId + "] Error processing autodetect result", e); + logger.warn(() -> "[" + jobId + "] Error processing autodetect result", e); } } } finally { @@ -250,9 +251,9 @@ public void setVacating(boolean vacating) { void handleOpenForecasts() { try { if (runningForecasts.isEmpty() == false) { - LOGGER.warn("[{}] still had forecasts {} executing. Attempting to set them to failed.", jobId, runningForecasts.keySet()); + logger.warn("[{}] still had forecasts {} executing. Attempting to set them to failed.", jobId, runningForecasts.keySet()); // There may be many docs in the results persistence queue. But we only want to bother updating the running forecasts - bulkResultsPersister.clearBulkRequest(); + bulkResultsPersister.clear(); for (ForecastRequestStats forecastRequestStats : runningForecasts.values()) { ForecastRequestStats failedStats = new ForecastRequestStats(forecastRequestStats); failedStats.setStatus(ForecastRequestStats.ForecastRequestStatus.FAILED); @@ -262,7 +263,7 @@ void handleOpenForecasts() { bulkResultsPersister.executeRequest(); } } catch (Exception ex) { - LOGGER.warn(() -> "[" + jobId + "] failure setting running forecasts to failed.", ex); + logger.warn(() -> "[" + jobId + "] failure setting running forecasts to failed.", ex); } } @@ -276,19 +277,23 @@ void processResult(AutodetectResult result) { if (deleteInterimRequired) { // Delete any existing interim results generated by a Flush command // which have not been replaced or superseded by new results. - LOGGER.trace("[{}] Deleting interim results", jobId); + logger.trace("[{}] Deleting interim results", jobId); persister.deleteInterimResults(jobId); - deleteInterimRequired = false; } if (bucket.isInterim() == false) { timingStatsReporter.reportBucket(bucket); ++currentRunBucketCount; } - // persist after deleting interim results in case the new - // results are also interim - bulkResultsPersister.persistBucket(bucket).executeRequest(); - bulkAnnotationsPersister.executeRequest(); + bulkResultsPersister.persistBucket(bucket); + if (deleteInterimRequired || bucket.isInterim()) { + // Execute the bulk request after deleting interim results in case the new + // results are also interim. Also execute the bulk request after creating new + // interim results, so that they exist before any subsequent deletion. + bulkResultsPersister.executeRequest(); + bulkAnnotationsPersister.executeRequest(); + deleteInterimRequired = false; + } } List records = result.getRecords(); if (records != null && records.isEmpty() == false) { @@ -300,7 +305,7 @@ void processResult(AutodetectResult result) { } CategoryDefinition categoryDefinition = result.getCategoryDefinition(); if (categoryDefinition != null) { - persister.persistCategoryDefinition(categoryDefinition, this::isAlive); + bulkResultsPersister.persistCategoryDefinition(categoryDefinition); } CategorizerStats categorizerStats = result.getCategorizerStats(); if (categorizerStats != null) { @@ -321,7 +326,7 @@ void processResult(AutodetectResult result) { } ForecastRequestStats forecastRequestStats = result.getForecastRequestStats(); if (forecastRequestStats != null) { - LOGGER.trace("Received Forecast Stats [{}]", forecastRequestStats.getId()); + logger.trace("Received Forecast Stats [{}]", forecastRequestStats.getId()); bulkResultsPersister.persistForecastRequestStats(forecastRequestStats); if (forecastRequestStats.getStatus() @@ -364,21 +369,24 @@ void processResult(AutodetectResult result) { } Quantiles quantiles = result.getQuantiles(); if (quantiles != null) { - LOGGER.debug("[{}] Parsed Quantiles with timestamp {}", jobId, quantiles.getTimestamp()); + logger.debug("[{}] Parsed Quantiles with timestamp {}", jobId, quantiles.getTimestamp()); persister.persistQuantiles(quantiles, this::isAlive); - bulkResultsPersister.executeRequest(); // If a node is trying to shut down then don't trigger any further normalizations on the node if (vacating == false && processKilled == false && renormalizer.isEnabled()) { - // We need to make all results written up to these quantiles available for renormalization - persister.commitResultWrites(jobId); - LOGGER.debug("[{}] Quantiles queued for renormalization", jobId); - renormalizer.renormalize(quantiles); + logger.debug("[{}] Quantiles queued for renormalization", jobId); + renormalizer.renormalize(quantiles, () -> { + // We need to make all results written up to these quantiles available for renormalization. + // However, this should be done as close to the point of normalization as possible, as many + // quantiles are superseded before they're used. + bulkResultsPersister.executeRequest(); + persister.commitWrites(jobId, JobResultsPersister.CommitType.RESULTS); + }); } } FlushAcknowledgement flushAcknowledgement = result.getFlushAcknowledgement(); if (flushAcknowledgement != null) { - LOGGER.debug("[{}] Flush acknowledgement parsed from output for ID {}", jobId, flushAcknowledgement.getId()); + logger.debug("[{}] Flush acknowledgement parsed from output for ID {}", jobId, flushAcknowledgement.getId()); // Commit previous writes here, effectively continuing // the flush from the C++ autodetect process right // through to the data store @@ -386,11 +394,12 @@ void processResult(AutodetectResult result) { try { bulkResultsPersister.executeRequest(); bulkAnnotationsPersister.executeRequest(); - persister.commitResultWrites(jobId); - persister.commitAnnotationWrites(); - LOGGER.debug("[{}] Flush acknowledgement sent to listener for ID {}", jobId, flushAcknowledgement.getId()); + persister.commitWrites( + jobId, + EnumSet.of(JobResultsPersister.CommitType.RESULTS, JobResultsPersister.CommitType.ANNOTATIONS) + ); } catch (Exception e) { - LOGGER.error( + logger.error( "[" + jobId + "] failed to bulk persist results and commit writes during flush acknowledgement for ID " @@ -428,7 +437,7 @@ private Annotation createModelSnapshotAnnotation(ModelSnapshot modelSnapshot) { } private void processModelSizeStats(ModelSizeStats modelSizeStats) { - LOGGER.trace( + logger.trace( "[{}] Parsed ModelSizeStats: {} / {} / {} / {} / {} / {}", jobId, modelSizeStats.getModelBytes(), @@ -439,7 +448,7 @@ private void processModelSizeStats(ModelSizeStats modelSizeStats) { modelSizeStats.getMemoryStatus() ); - persister.persistModelSizeStats(modelSizeStats, this::isAlive); + bulkResultsPersister.persistModelSizeStats(modelSizeStats); notifyModelMemoryStatusChange(modelSizeStats); latestModelSizeStats = modelSizeStats; @@ -491,7 +500,7 @@ protected void updateModelSnapshotOnJob(ModelSnapshot modelSnapshot) { updateModelSnapshotSemaphore.acquire(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - LOGGER.info("[{}] Interrupted acquiring update model snapshot semaphore", jobId); + logger.info("[{}] Interrupted acquiring update model snapshot semaphore", jobId); return; } @@ -499,13 +508,13 @@ protected void updateModelSnapshotOnJob(ModelSnapshot modelSnapshot) { @Override public void onResponse(PutJobAction.Response response) { updateModelSnapshotSemaphore.release(); - LOGGER.debug("[{}] Updated job with model snapshot id [{}]", jobId, modelSnapshot.getSnapshotId()); + logger.debug("[{}] Updated job with model snapshot id [{}]", jobId, modelSnapshot.getSnapshotId()); } @Override public void onFailure(Exception e) { updateModelSnapshotSemaphore.release(); - LOGGER.error("[" + jobId + "] Failed to update job with new model snapshot id [" + modelSnapshot.getSnapshotId() + "]", e); + logger.error("[" + jobId + "] Failed to update job with new model snapshot id [" + modelSnapshot.getSnapshotId() + "]", e); } }); } @@ -525,13 +534,11 @@ public void awaitCompletion() throws TimeoutException { // These lines ensure that the "completion" we're awaiting includes making the results searchable waitUntilRenormalizerIsIdle(); - persister.commitResultWrites(jobId); - persister.commitAnnotationWrites(); - persister.commitStateWrites(jobId); + persister.commitWrites(jobId, EnumSet.allOf(JobResultsPersister.CommitType.class)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - LOGGER.info("[{}] Interrupted waiting for results processor to complete", jobId); + logger.info("[{}] Interrupted waiting for results processor to complete", jobId); } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/output/JobSnapshotUpgraderResultProcessor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/output/JobSnapshotUpgraderResultProcessor.java index 12dca226858b..7d5bc1feeaf8 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/output/JobSnapshotUpgraderResultProcessor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/output/JobSnapshotUpgraderResultProcessor.java @@ -69,7 +69,7 @@ public JobSnapshotUpgraderResultProcessor( this.snapshotId = Objects.requireNonNull(snapshotId); this.persister = Objects.requireNonNull(persister); this.process = Objects.requireNonNull(autodetectProcess); - this.bulkResultsPersister = persister.bulkPersisterBuilder(jobId).shouldRetry(this::isAlive); + this.bulkResultsPersister = persister.bulkPersisterBuilder(jobId, this::isAlive); this.flushListener = new FlushListener(); } @@ -249,7 +249,7 @@ public void awaitCompletion() throws TimeoutException { } // These lines ensure that the "completion" we're awaiting includes making the results searchable - persister.commitStateWrites(jobId); + persister.commitWrites(jobId, JobResultsPersister.CommitType.STATE); } catch (InterruptedException e) { Thread.currentThread().interrupt(); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/Renormalizer.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/Renormalizer.java index a6500503ee76..62d3a7d58524 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/Renormalizer.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/Renormalizer.java @@ -20,7 +20,7 @@ public interface Renormalizer { * Update the anomaly score field on all previously persisted buckets * and all contained records */ - void renormalize(Quantiles quantiles); + void renormalize(Quantiles quantiles, Runnable setupStep); /** * Blocks until the renormalizer is idle and no further quantiles updates are pending. diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/ShortCircuitingRenormalizer.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/ShortCircuitingRenormalizer.java index c3932c0334f9..c2ac0055daf6 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/ShortCircuitingRenormalizer.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/ShortCircuitingRenormalizer.java @@ -53,7 +53,7 @@ public boolean isEnabled() { } @Override - public void renormalize(Quantiles quantiles) { + public void renormalize(Quantiles quantiles, Runnable setupStep) { if (isEnabled() == false) { return; } @@ -61,8 +61,13 @@ public void renormalize(Quantiles quantiles) { // Needed to ensure work is not added while the tryFinishWork() method is running synchronized (this) { latestQuantilesHolder = (latestQuantilesHolder == null) - ? new AugmentedQuantiles(quantiles, null, new CountDownLatch(1)) - : new AugmentedQuantiles(quantiles, latestQuantilesHolder.getEvictedTimestamp(), latestQuantilesHolder.getLatch()); + ? new AugmentedQuantiles(quantiles, setupStep, null, new CountDownLatch(1)) + : new AugmentedQuantiles( + quantiles, + setupStep, + latestQuantilesHolder.getEvictedTimestamp(), + latestQuantilesHolder.getLatch() + ); tryStartWork(); } } @@ -153,6 +158,7 @@ private void doRenormalizations() { AugmentedQuantiles latestAugmentedQuantiles = getLatestAugmentedQuantilesAndClear(); assert latestAugmentedQuantiles != null; if (latestAugmentedQuantiles != null) { // TODO: remove this if the assert doesn't trip in CI over the next year or so + latestAugmentedQuantiles.runSetupStep(); Quantiles latestQuantiles = latestAugmentedQuantiles.getQuantiles(); CountDownLatch latch = latestAugmentedQuantiles.getLatch(); try { @@ -181,11 +187,13 @@ private void doRenormalizations() { */ private class AugmentedQuantiles { private final Quantiles quantiles; + private final Runnable setupStep; private final Date earliestEvictedTimestamp; private final CountDownLatch latch; - AugmentedQuantiles(Quantiles quantiles, Date earliestEvictedTimestamp, CountDownLatch latch) { + AugmentedQuantiles(Quantiles quantiles, Runnable setupStep, Date earliestEvictedTimestamp, CountDownLatch latch) { this.quantiles = Objects.requireNonNull(quantiles); + this.setupStep = Objects.requireNonNull(setupStep); this.earliestEvictedTimestamp = earliestEvictedTimestamp; this.latch = Objects.requireNonNull(latch); } @@ -194,6 +202,10 @@ Quantiles getQuantiles() { return quantiles; } + void runSetupStep() { + setupStep.run(); + } + Date getEvictedTimestamp() { return (earliestEvictedTimestamp != null) ? earliestEvictedTimestamp : quantiles.getTimestamp(); } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsPersisterTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsPersisterTests.java index e73ab615045f..3d7f8a10c6ee 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsPersisterTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsPersisterTests.java @@ -74,7 +74,6 @@ public class JobResultsPersisterTests extends ESTestCase { private static final String JOB_ID = "foo"; private Client client; - private OriginSettingClient originSettingClient; private ArgumentCaptor bulkRequestCaptor; private JobResultsPersister persister; @@ -83,7 +82,7 @@ public void setUpTests() { bulkRequestCaptor = ArgumentCaptor.forClass(BulkRequest.class); client = mock(Client.class); doAnswer(withResponse(mock(BulkResponse.class))).when(client).execute(eq(BulkAction.INSTANCE), any(), any()); - originSettingClient = MockOriginSettingClient.mockOriginSettingClient(client, ClientHelper.ML_ORIGIN); + OriginSettingClient originSettingClient = MockOriginSettingClient.mockOriginSettingClient(client, ClientHelper.ML_ORIGIN); persister = new JobResultsPersister(originSettingClient, buildResultsPersisterService(originSettingClient)); } @@ -222,8 +221,8 @@ public void testExecuteRequest_ClearsBulkRequest() { public void testBulkRequestExecutesWhenReachMaxDocs() { JobResultsPersister.Builder bulkBuilder = persister.bulkPersisterBuilder("foo"); - ModelPlot modelPlot = new ModelPlot("foo", new Date(), 123456, 0); for (int i = 0; i <= JobRenormalizedResultsPersister.BULK_LIMIT; i++) { + ModelPlot modelPlot = new ModelPlot("foo", new Date(), 123456, i); bulkBuilder.persistModelPlot(modelPlot); } @@ -282,7 +281,6 @@ public void testPersistTimingStats() { ); } - @SuppressWarnings("unchecked") public void testPersistDatafeedTimingStats() { DatafeedTimingStats timingStats = new DatafeedTimingStats( "foo", @@ -325,7 +323,6 @@ public void testPersistDatafeedTimingStats() { ); } - @SuppressWarnings("unchecked") private void testPersistQuantilesSync(SearchHits searchHits, String expectedIndexOrAlias) { SearchResponse searchResponse = mock(SearchResponse.class); when(searchResponse.status()).thenReturn(RestStatus.OK); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/AutodetectProcessManagerTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/AutodetectProcessManagerTests.java index 35a7d9c422c1..7a5d02be1a26 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/AutodetectProcessManagerTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/AutodetectProcessManagerTests.java @@ -186,13 +186,11 @@ public void setup() throws Exception { jobResultsProvider = mock(JobResultsProvider.class); jobResultsPersister = mock(JobResultsPersister.class); JobResultsPersister.Builder bulkPersisterBuilder = mock(JobResultsPersister.Builder.class); - when(bulkPersisterBuilder.shouldRetry(any())).thenReturn(bulkPersisterBuilder); - when(jobResultsPersister.bulkPersisterBuilder(any())).thenReturn(bulkPersisterBuilder); + when(jobResultsPersister.bulkPersisterBuilder(any(), any())).thenReturn(bulkPersisterBuilder); jobDataCountsPersister = mock(JobDataCountsPersister.class); annotationPersister = mock(AnnotationPersister.class); AnnotationPersister.Builder bulkAnnotationsBuilder = mock(AnnotationPersister.Builder.class); - when(bulkAnnotationsBuilder.shouldRetry(any())).thenReturn(bulkAnnotationsBuilder); - when(annotationPersister.bulkPersisterBuilder(any())).thenReturn(bulkAnnotationsBuilder); + when(annotationPersister.bulkPersisterBuilder(any(), any())).thenReturn(bulkAnnotationsBuilder); autodetectCommunicator = mock(AutodetectCommunicator.class); autodetectFactory = mock(AutodetectProcessFactory.class); normalizerFactory = mock(NormalizerFactory.class); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/output/AutodetectResultProcessorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/output/AutodetectResultProcessorTests.java index cfd10da57de2..d49596a4811f 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/output/AutodetectResultProcessorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/output/AutodetectResultProcessorTests.java @@ -58,6 +58,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.EnumSet; import java.util.Iterator; import java.util.List; import java.util.concurrent.ScheduledThreadPoolExecutor; @@ -84,7 +85,6 @@ public class AutodetectResultProcessorTests extends ESTestCase { private static final long BUCKET_SPAN_MS = 1000; private static final Instant CURRENT_TIME = Instant.ofEpochMilli(2000000000); - private ThreadPool threadPool; private Client client; private AnomalyDetectionAuditor auditor; private Renormalizer renormalizer; @@ -101,19 +101,17 @@ public class AutodetectResultProcessorTests extends ESTestCase { public void setUpMocks() { executor = new Scheduler.SafeScheduledThreadPoolExecutor(1); client = mock(Client.class); - threadPool = mock(ThreadPool.class); + ThreadPool threadPool = mock(ThreadPool.class); when(client.threadPool()).thenReturn(threadPool); when(threadPool.getThreadContext()).thenReturn(new ThreadContext(Settings.EMPTY)); auditor = mock(AnomalyDetectionAuditor.class); renormalizer = mock(Renormalizer.class); persister = mock(JobResultsPersister.class); bulkResultsPersister = mock(JobResultsPersister.Builder.class); - when(bulkResultsPersister.shouldRetry(any())).thenReturn(bulkResultsPersister); - when(persister.bulkPersisterBuilder(eq(JOB_ID))).thenReturn(bulkResultsPersister); + when(persister.bulkPersisterBuilder(eq(JOB_ID), any())).thenReturn(bulkResultsPersister); annotationPersister = mock(AnnotationPersister.class); bulkAnnotationsPersister = mock(AnnotationPersister.Builder.class); - when(bulkAnnotationsPersister.shouldRetry(any())).thenReturn(bulkAnnotationsPersister); - when(annotationPersister.bulkPersisterBuilder(eq(JOB_ID))).thenReturn(bulkAnnotationsPersister); + when(annotationPersister.bulkPersisterBuilder(eq(JOB_ID), any())).thenReturn(bulkAnnotationsPersister); process = mock(AutodetectProcess.class); flushListener = mock(FlushListener.class); processorUnderTest = new AutodetectResultProcessor( @@ -133,7 +131,7 @@ public void setUpMocks() { @After public void cleanup() { - verify(annotationPersister).bulkPersisterBuilder(eq(JOB_ID)); + verify(annotationPersister).bulkPersisterBuilder(eq(JOB_ID), any()); verifyNoMoreInteractions(auditor, renormalizer, persister, annotationPersister); executor.shutdown(); } @@ -147,10 +145,8 @@ public void testProcess() throws Exception { assertThat(processorUnderTest.completionLatch.getCount(), is(equalTo(0L))); verify(renormalizer).waitUntilIdle(); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); - verify(persister).commitResultWrites(JOB_ID); - verify(persister).commitAnnotationWrites(); - verify(persister).commitStateWrites(JOB_ID); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); + verify(persister).commitWrites(JOB_ID, EnumSet.allOf(JobResultsPersister.CommitType.class)); } public void testProcessResult_bucket() { @@ -163,10 +159,10 @@ public void testProcessResult_bucket() { processorUnderTest.setDeleteInterimRequired(false); processorUnderTest.processResult(result); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); verify(bulkResultsPersister).persistTimingStats(any(TimingStats.class)); verify(bulkResultsPersister).persistBucket(bucket); - verify(bulkResultsPersister).executeRequest(); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); + verify(bulkResultsPersister, never()).executeRequest(); verify(persister, never()).deleteInterimResults(JOB_ID); } @@ -184,7 +180,7 @@ public void testProcessResult_bucket_deleteInterimRequired() { verify(bulkResultsPersister).persistTimingStats(any(TimingStats.class)); verify(bulkResultsPersister).persistBucket(bucket); verify(bulkResultsPersister).executeRequest(); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); verify(persister).deleteInterimResults(JOB_ID); } @@ -203,7 +199,7 @@ public void testProcessResult_bucket_isInterim() { verify(bulkResultsPersister, never()).persistTimingStats(any(TimingStats.class)); verify(bulkResultsPersister).persistBucket(bucket); verify(bulkResultsPersister).executeRequest(); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); } public void testProcessResult_records() { @@ -219,7 +215,7 @@ public void testProcessResult_records() { verify(bulkResultsPersister).persistRecords(records); verify(bulkResultsPersister, never()).executeRequest(); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); } public void testProcessResult_influencers() { @@ -235,7 +231,7 @@ public void testProcessResult_influencers() { verify(bulkResultsPersister).persistInfluencers(influencers); verify(bulkResultsPersister, never()).executeRequest(); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); } public void testProcessResult_categoryDefinition() { @@ -247,9 +243,9 @@ public void testProcessResult_categoryDefinition() { processorUnderTest.setDeleteInterimRequired(false); processorUnderTest.processResult(result); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); + verify(bulkResultsPersister).persistCategoryDefinition(eq(categoryDefinition)); verify(bulkResultsPersister, never()).executeRequest(); - verify(persister).persistCategoryDefinition(eq(categoryDefinition), any()); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); } public void testProcessResult_flushAcknowledgement() { @@ -262,17 +258,19 @@ public void testProcessResult_flushAcknowledgement() { processorUnderTest.processResult(result); assertTrue(processorUnderTest.isDeleteInterimRequired()); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); verify(flushListener).acknowledgeFlush(flushAcknowledgement, null); - verify(persister).commitResultWrites(JOB_ID); - verify(persister).commitAnnotationWrites(); + verify(persister).commitWrites( + JOB_ID, + EnumSet.of(JobResultsPersister.CommitType.RESULTS, JobResultsPersister.CommitType.ANNOTATIONS) + ); verify(bulkResultsPersister).executeRequest(); } public void testProcessResult_flushAcknowledgementMustBeProcessedLast() { AutodetectResult result = mock(AutodetectResult.class); FlushAcknowledgement flushAcknowledgement = mock(FlushAcknowledgement.class); - when(flushAcknowledgement.getId()).thenReturn(JOB_ID); + when(flushAcknowledgement.getId()).thenReturn(Integer.valueOf(randomInt(100)).toString()); when(result.getFlushAcknowledgement()).thenReturn(flushAcknowledgement); CategoryDefinition categoryDefinition = mock(CategoryDefinition.class); when(categoryDefinition.getCategoryId()).thenReturn(1L); @@ -283,11 +281,13 @@ public void testProcessResult_flushAcknowledgementMustBeProcessedLast() { assertTrue(processorUnderTest.isDeleteInterimRequired()); InOrder inOrder = inOrder(persister, bulkResultsPersister, flushListener); - inOrder.verify(persister).bulkPersisterBuilder(eq(JOB_ID)); - inOrder.verify(persister).persistCategoryDefinition(eq(categoryDefinition), any()); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); + inOrder.verify(bulkResultsPersister).persistCategoryDefinition(eq(categoryDefinition)); inOrder.verify(bulkResultsPersister).executeRequest(); - inOrder.verify(persister).commitResultWrites(JOB_ID); - inOrder.verify(persister).commitAnnotationWrites(); + verify(persister).commitWrites( + JOB_ID, + EnumSet.of(JobResultsPersister.CommitType.RESULTS, JobResultsPersister.CommitType.ANNOTATIONS) + ); inOrder.verify(flushListener).acknowledgeFlush(flushAcknowledgement, null); } @@ -299,7 +299,7 @@ public void testProcessResult_modelPlot() { processorUnderTest.setDeleteInterimRequired(false); processorUnderTest.processResult(result); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); verify(bulkResultsPersister).persistModelPlot(modelPlot); } @@ -310,7 +310,7 @@ public void testProcessResult_annotation() { processorUnderTest.processResult(result); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); verify(bulkAnnotationsPersister).persistAnnotation(annotation); if (annotation.getEvent() == Annotation.Event.CATEGORIZATION_STATUS_CHANGE) { verify(auditor).warning(eq(JOB_ID), anyString()); @@ -326,8 +326,8 @@ public void testProcessResult_modelSizeStats() { processorUnderTest.processResult(result); assertThat(processorUnderTest.modelSizeStats(), is(equalTo(modelSizeStats))); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); - verify(persister).persistModelSizeStats(eq(modelSizeStats), any()); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); + verify(bulkResultsPersister).persistModelSizeStats(eq(modelSizeStats)); } public void testProcessResult_modelSizeStatsWithMemoryStatusChanges() { @@ -357,8 +357,8 @@ public void testProcessResult_modelSizeStatsWithMemoryStatusChanges() { when(result.getModelSizeStats()).thenReturn(modelSizeStats); processorUnderTest.processResult(result); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); - verify(persister, times(4)).persistModelSizeStats(any(ModelSizeStats.class), any()); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); + verify(bulkResultsPersister, times(4)).persistModelSizeStats(any(ModelSizeStats.class)); // We should have only fired two notifications: one for soft_limit and one for hard_limit verify(auditor).warning(JOB_ID, Messages.getMessage(Messages.JOB_AUDIT_MEMORY_STATUS_SOFT_LIMIT)); verify(auditor).error(JOB_ID, Messages.getMessage(Messages.JOB_AUDIT_MEMORY_STATUS_HARD_LIMIT, "512mb", "1kb")); @@ -381,7 +381,7 @@ public void testProcessResult_categorizationStatusChangeAnnotationCausesNotifica when(result.getAnnotation()).thenReturn(annotation); processorUnderTest.processResult(result); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); verify(bulkAnnotationsPersister).persistAnnotation(annotation); verify(auditor).warning(JOB_ID, "Categorization status changed to 'warn' for partition 'foo' after 0 buckets"); } @@ -419,7 +419,7 @@ public void testProcessResult_modelSnapshot() { new JobUpdate.Builder(JOB_ID).setModelSnapshotId("a_snapshot_id").build() ); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); verify(persister).persistModelSnapshot(eq(modelSnapshot), eq(WriteRequest.RefreshPolicy.IMMEDIATE), any()); verify(bulkAnnotationsPersister).persistAnnotation(ModelSnapshot.annotationDocumentId(modelSnapshot), expectedAnnotation); verify(client).execute(same(UpdateJobAction.INSTANCE), eq(expectedJobUpdateRequest), any()); @@ -434,12 +434,10 @@ public void testProcessResult_quantiles_givenRenormalizationIsEnabled() { processorUnderTest.setDeleteInterimRequired(false); processorUnderTest.processResult(result); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); verify(persister).persistQuantiles(eq(quantiles), any()); - verify(bulkResultsPersister).executeRequest(); - verify(persister).commitResultWrites(JOB_ID); verify(renormalizer).isEnabled(); - verify(renormalizer).renormalize(quantiles); + verify(renormalizer).renormalize(eq(quantiles), any(Runnable.class)); } public void testProcessResult_quantiles_givenRenormalizationIsDisabled() { @@ -451,9 +449,9 @@ public void testProcessResult_quantiles_givenRenormalizationIsDisabled() { processorUnderTest.setDeleteInterimRequired(false); processorUnderTest.processResult(result); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); verify(persister).persistQuantiles(eq(quantiles), any()); - verify(bulkResultsPersister).executeRequest(); + verify(bulkResultsPersister, never()).executeRequest(); verify(renormalizer).isEnabled(); } @@ -466,10 +464,8 @@ public void testAwaitCompletion() throws Exception { assertThat(processorUnderTest.completionLatch.getCount(), is(equalTo(0L))); assertThat(processorUnderTest.updateModelSnapshotSemaphore.availablePermits(), is(equalTo(1))); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); - verify(persister).commitResultWrites(JOB_ID); - verify(persister).commitAnnotationWrites(); - verify(persister).commitStateWrites(JOB_ID); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); + verify(persister).commitWrites(JOB_ID, EnumSet.allOf(JobResultsPersister.CommitType.class)); verify(renormalizer).waitUntilIdle(); } @@ -486,7 +482,7 @@ public void testPersisterThrowingDoesntBlockProcessing() { processorUnderTest.process(); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); verify(persister, times(2)).persistModelSnapshot(any(), eq(WriteRequest.RefreshPolicy.IMMEDIATE), any()); } @@ -507,7 +503,7 @@ public void testParsingErrorSetsFailed() throws Exception { ); assertThat(flushAcknowledgement, is(nullValue())); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); } public void testKill() throws Exception { @@ -520,11 +516,9 @@ public void testKill() throws Exception { assertThat(processorUnderTest.completionLatch.getCount(), is(equalTo(0L))); assertThat(processorUnderTest.updateModelSnapshotSemaphore.availablePermits(), is(equalTo(1))); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); - verify(persister).commitResultWrites(JOB_ID); - verify(persister).commitAnnotationWrites(); - verify(persister).commitStateWrites(JOB_ID); - verify(renormalizer, never()).renormalize(any()); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); + verify(persister).commitWrites(JOB_ID, EnumSet.allOf(JobResultsPersister.CommitType.class)); + verify(renormalizer, never()).renormalize(any(), any()); verify(renormalizer).shutdown(); verify(renormalizer).waitUntilIdle(); verify(flushListener).clear(); @@ -546,7 +540,7 @@ public void testProcessingOpenedForecasts() { verify(bulkResultsPersister, times(2)).persistForecastRequestStats(argument.capture()); verify(bulkResultsPersister, times(1)).executeRequest(); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); verify(persister, never()).deleteInterimResults(JOB_ID); // Get all values is in reverse call order @@ -578,7 +572,7 @@ public void testProcessingForecasts() { verify(bulkResultsPersister, times(2)).persistForecastRequestStats(argument.capture()); verify(bulkResultsPersister, times(1)).executeRequest(); - verify(persister).bulkPersisterBuilder(eq(JOB_ID)); + verify(persister).bulkPersisterBuilder(eq(JOB_ID), any()); verify(persister, never()).deleteInterimResults(JOB_ID); List stats = argument.getAllValues(); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/normalizer/ShortCircuitingRenormalizerTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/normalizer/ShortCircuitingRenormalizerTests.java index 02c4830a6e60..ae34a675d15b 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/normalizer/ShortCircuitingRenormalizerTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/normalizer/ShortCircuitingRenormalizerTests.java @@ -47,12 +47,12 @@ public void testNormalize() throws InterruptedException { // Blast through many sets of quantiles in quick succession, faster than the normalizer can process them for (int i = 1; i < TEST_SIZE / 2; ++i) { Quantiles quantiles = new Quantiles(JOB_ID, new Date(), Integer.toString(i)); - renormalizer.renormalize(quantiles); + renormalizer.renormalize(quantiles, () -> {}); } renormalizer.waitUntilIdle(); for (int i = TEST_SIZE / 2; i <= TEST_SIZE; ++i) { Quantiles quantiles = new Quantiles(JOB_ID, new Date(), Integer.toString(i)); - renormalizer.renormalize(quantiles); + renormalizer.renormalize(quantiles, () -> {}); } renormalizer.waitUntilIdle(); From 56036676cb9b6733a9f01fc2a85d67a8a6b3e40a Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 19 Dec 2022 23:22:24 +1100 Subject: [PATCH 305/919] JWT Realm - documentation update (#92409) * wip * Initial update for the JWT realm doc * Apply suggestions from code review Co-authored-by: amyjtechwriter <61687663+amyjtechwriter@users.noreply.github.com> Co-authored-by: amyjtechwriter <61687663+amyjtechwriter@users.noreply.github.com> --- .../settings/security-settings.asciidoc | 55 ++++++- .../authentication/jwt-realm.asciidoc | 139 +++++++++++++++--- 2 files changed, 170 insertions(+), 24 deletions(-) diff --git a/docs/reference/settings/security-settings.asciidoc b/docs/reference/settings/security-settings.asciidoc index ed1ce8d72f3f..ef142b75e56a 100644 --- a/docs/reference/settings/security-settings.asciidoc +++ b/docs/reference/settings/security-settings.asciidoc @@ -2022,13 +2022,21 @@ In addition to the <> can specify the following settings. // end::jwt-description-tag[] +// tag::jwt-token-type-tag[] +`token_type` {ess-icon}:: +(<>) +The token type, `id_token` or `access_token`, that the JWT realm uses to verify +incoming JWTs. Defaults to `id_token`. +// end::jwt-token-type-tag[] + // tag::jwt-allowed-audiences-tag[] `allowed_audiences` {ess-icon}:: (<>) A list of allowed JWT audiences that {es} should verify. {es} will only consume JWTs that were intended for any of these audiences, as denoted by the `aud` claim in the JWT). Examples of `aud` claim are `https://example.com/client1` -and `other_service,elasticsearch`. +and `other_service,elasticsearch`. When `token_type` is `access_token`, the audiences can +be optionally denoted by a different claim in the JWT if `aud` does not exist. // end::jwt-allowed-audiences-tag[] // tag::jwt-allowed-clock-skew-tag[] @@ -2049,6 +2057,51 @@ should be provided by your JWT Issuer. Examples of `iss` claim are `https://example.com:8443/jwt` and `issuer123`. // end::jwt-allowed-issuer-tag[] +// tag::jwt-allowed-subjects-tag[] +`allowed_subjects` {ess-icon}:: +(<>) +A list of allowed JWT subjects that {es} should verify. {es} will only consume +JWTs that were issued for any of these subjects, as denoted by the `sub` +claim in the JWT. Examples of `sub` claim are `https://example.com/user1` +and `user_1,user2`. +When `token_type` is `access_token`, this setting is mandatory and the subject can be +optionally denoted by a different claim in the JWT if `sub` does not exist. +// end::jwt-allowed-subjects-tag[] + +// tag::jwt-fallback-claims-sub-tag[] +`fallback_claims.sub` {ess-icon}:: +(<>) +The alternative claim to look for the subject information if the `sub` claim +does not exist. It is configurable only when the `token_type` is `access_token`. +The fallback is applied everywhere the `sub` claim is used. +// end::jwt-fallback-claims-sub-tag[] + +// tag::jwt-fallback-claims-aud-tag[] +`fallback_claims.aud` {ess-icon}:: +(<>) +The alternative claim to look for the audiences information if the `aud` claim +does not exist. It is configurable only when the `token_type` is `access_token`. +The fallback is applied everywhere the `aud` claim is used. +// end::jwt-fallback-claims-aud-tag[] + +// tag::jwt-required-claims-tag[] +`required_claims` {ess-icon}:: +(<>) +Additional claims and associated values that {es} should verify. +This is a group setting that takes key/value pairs, where the key is a string +and the value must be either a string or an array of strings. + +For example: + +[source, yaml] +------------------------------------------------------------ +xpack.security.authc.realms.jwt.jwt1: + required_claims: + token_use: "id" + versions: ["1.0", "2.0"] +------------------------------------------------------------ +// end::jwt-required-claims-tag[] + // tag::jwt-allowed-signature-algorithms-tag[] `allowed_signature_algorithms` {ess-icon}:: (<>) diff --git a/x-pack/docs/en/security/authentication/jwt-realm.asciidoc b/x-pack/docs/en/security/authentication/jwt-realm.asciidoc index c3d3f2ab4dfd..de63e22b5c2a 100644 --- a/x-pack/docs/en/security/authentication/jwt-realm.asciidoc +++ b/x-pack/docs/en/security/authentication/jwt-realm.asciidoc @@ -12,35 +12,61 @@ between the _client_ that is connecting to {es}, and the _user_ on whose behalf the request should run. The JWT authenticates the user, and a separate credential authenticates the client. -A common scenario for JWTs is when an existing front-end application uses -OpenID Connect (OIDC) to authenticate and identify a user, and then accesses {es} -on behalf of the authenticated user. - -TIP: If the front-end application does not exist, you can use the -<> instead. +The JWT realm is designed to cater for the following two scenarios: + +1. An application authenticates and identifies a user with an authentication flow, e.g. +OpenID Connect (OIDC), and then accesses {es} on behalf of the authenticated user. +A common scenario for JWTs is when an existing application uses +2. An application itself goes through an authentication flow, e.g. OAuth2 Client Credentials +Flow, and then accesses {es} for itself. + +In both scenarios, the application should present a JWT to {es} that represents either the user (scenario 1) +or itself (scenario 2). {es} categorizes the JWT into two types, ID Token and Access token, +and validates them accordingly. The ID Token type basically follows the OIDC specification. +The access token type is similar to the ID Token type, but with more relaxed rules which makes it +suitable for loosely-defined JWTs, including self-signed ones. +Tokens of both types must contain the following 5 pieces of information. While ID Tokens follow +strict rules on which claim should provide each piece of the information, +access tokens allow certain configurable options. + +[cols="3",frame=all] +|==== +h| 2+^h| Claims +h| Information | ID Token | Access Token + | Issuer | `iss` | `iss` + | Subject | `sub` | Defaults to `sub`, but can fall back to another claim if `sub` does not exist + | Audiences | `aud` | Defaults to `aud`, but can fall back to another claim if `aud` does not exist + | Issue Time | `iat` | `iat` + | Expiration Time | `exp` | `exp` +|==== + +In addition, {es} also validates `nbf` and `auth_time` claims for ID Tokens if these claims are present. +But these claims are ignored for access tokens. + +A single JWT realm can only work with a single token type. The default token +type is `id_token`. To handle both token types, you must configure at least +two JWT realms. [[jwt-realm-oidc]] -==== JWT uses OIDC workflows +==== JWT from OIDC workflows JWT authentication in {es} is derived from OIDC user workflows, where different -tokens can be issued by an OIDC Provider (OP). One possible token is an -_ID token_, which uses the JWT format. If the ID token is presented to a JWT -realm, {es} can use it as a bearer token to authenticate, identify, and authorize an individual -user. +tokens can be issued by an OIDC Provider (OP), including ID Tokens and access +tokens. ID Tokens are well-defined JWT and should be always compatible with +what JWT realm supports. Access tokens can be arbitrary in theory. For them to +be usable with JWT realm, they must at least use the JWT format and satisfy +relevant requirements in the above table. NOTE: Because JWTs are obtained external to {es}, you can define a custom workflow instead of using the OIDC workflow. However, the JWT format must still be JSON Web Signature (JWS). The JWS header and JWS signature are validated using OIDC ID token validation rules. -{es} supports a separate <>, which provides -stronger security guarantees than the JWT realm, and is preferred for any +{es} supports a separate <>. It is preferred for any use case where {es} can act as an OIDC RP. The OIDC realm is the only supported way to enable OIDC authentication in {kib}. -TIP: If JWTs are issued for the front-end application, the application is the realm client and JWT user. -That is not supported by OIDC flows, but it may be supported by bespoke JWT issuers. -In that case, use the client secret and JWT for the client application, and the -`es-security-runas-user` HTTP request header for the different user. See <>. +TIP: The JWT realm is compatible with the <> feature. +See also <>. [[jwt-realm-configuration]] ==== Configure {es} to use a JWT realm @@ -62,6 +88,7 @@ includes the most common settings, which are not intended for every use case: ---- xpack.security.authc.realms.jwt.jwt1: order: 3 + token_type: id_token client_authentication.type: shared_secret allowed_issuer: "https://issuer.example.com/jwt/" allowed_audiences: [ "8fb85eba-979c-496c-8ae2-a57fde3f12d0" ] @@ -75,6 +102,9 @@ Specifies a realm `order` of `3`, which indicates the order in which the configured realm is checked when authenticating a user. Realms are consulted in ascending order, where the realm with the lowest order value is consulted first. +`token_type`:: +Instructs the realm to treat and validate incoming JWTs as ID Tokens (`id_token`). + `client_authentication.type`:: Specifies the client authentication type as `shared_secret`, which means that the client is authenticated using an HTTP request header that must match a @@ -103,6 +133,53 @@ use an absolute path starting with `/app/config/`. `claims.principal`:: The name of the JWT claim that contains the user's principal (username). +The following is an example snippet for configure a JWT realm for handling +access tokens: + +[source,yaml] +---- +xpack.security.authc.realms.jwt.jwt2: + order: 4 + token_type: access_token + client_authentication.type: shared_secret + allowed_issuer: "https://issuer.example.com/jwt/" + allowed_subjects: [ "123456-compute@developer.example.com" ] + allowed_audiences: [ "elasticsearch" ] + required_claims: + token_use: access + version: ["1.0", "2.0"] + allowed_signature_algorithms: [RS256,HS256] + pkc_jwkset_path: "https://idp-42.example.com/.well-known/configuration" + fallback_claims.sub: client_id + fallback_claims.aud: scope + claims.principal: sub +---- + +`token_type`:: +Instructs the realm to treat and validate incoming JWTs as access tokens (`access_token`). + +`allowed_subjects`:: +Specifies a list of JWT subjects that the realm will allow. +These values are typically URLs, UUIDs, or other case-sensitive string values. + +NOTE: This setting is mandatory for when `token_type` is `access_token`. + +`required_claims`:: +Specifies a list of key/value pairs for additional verifications to be performed +against a JWT. The values are either a string or an array of strings. + +`fallback_claims.sub`:: +The name of the JWT claim to extract the subject information if the `sub` claim does not exist. +This setting is only available when `token_type` is `access_token`. +The fallback is applied everywhere the `sub` claim is used. +In the above snippet, it means the `claims.principal` will also fallback to `client_id` +if `sub` does not exist. + +`fallback_claims.aud`:: +The name of the JWT claim to extract the audiences information if the `aud` claim does not exist. +This setting is only available when `token_type` is `access_token`. +The fallback is applied everywhere the `aud` claim is used. + -- . After defining settings, use the @@ -186,11 +263,12 @@ as `HS256`. The algorithm must be in the realm's allow list. [[jwt-validation-payload]] ===== Payload claims -OIDC ID tokens contain several claims, which provide information about the user +Tokens contain several claims, which provide information about the user who is issuing the token, and the token itself. +Depending on the token type, these information can optionally be identified +by different claims. -[[jwt-validation-payload-oidc]] -====== OIDC payload claims +====== JWT payload claims The following claims are validated by a subset of OIDC ID token rules. {es} doesn't validate `nonce` claims, but a custom JWT issuer can add a @@ -205,10 +283,21 @@ creation (`iat`), not before (`nbf`), and expiration times (`exp`). (Required, String) Denotes the issuer that created the ID token. The value must be an exact, case-sensitive match to the value in the `allowed_issuer` setting. +`sub`:: +(Required, String) Indicates the subject that the ID token is created for. +A JWT realm of the `id_token` type by defaults accepts all subjects. +A JWT realm of the `access_token` type can specify a fallback claim that will +be used in place where the `sub` claim does not exist. Such JWT realm +must also specify the `allowed_subjects` setting and the subject value +must be an exact, case-sensitive match to any of the CSV values in the +`allowed_subjects` setting. + `aud`:: (Required, String) Indicates the audiences that the ID token is for, expressed as a comma-separated value (CSV). One of the values must be an exact, case-sensitive match to any of the CSV values in the `allowed_audiences` setting. +A JWT realm of the `access_token` type can specify a fallback claim that will +be used in place where the `aud` claim does not exist. `exp`:: (Required, integer) Expiration time for the ID token, expressed in UTC @@ -221,14 +310,19 @@ seconds since epoch. `nbf`:: (Optional, integer) Indicates the time before which the JWT must not be accepted, expressed as UTC seconds since epoch. +This claim is optional. If it exists, a JWT realm of `id_token` type will verify +it, while a JWT realm of `access_token` will just ignore it. `auth_time`:: (Optional, integer) Time when the user authenticated to the JWT issuer, expressed as UTC seconds since epoch. +This claim is optional. If it exists, a JWT realm of `id_token` type will verify +it, while a JWT realm of `access_token` will just ignore it. + [[jwt-validation-payload-es]] -====== {es} settings for consuming OIDC claims -{es} uses OIDC ID token claims for the following settings. +====== {es} settings for consuming JWT claims +{es} uses JWT claims for the following settings. `principal`:: (Required, String) Contains the user's principal (username). The value is @@ -584,5 +678,4 @@ JWT realm itself. "metadata":{"jwt_claim_email":"user2@something.example.com","jwt_claim_aud":["es01","es02","es03"], "jwt_claim_sub":"user2","jwt_claim_iss":"my-issuer"},"enabled":true,"authentication_realm": {"name":"jwt2","type":"jwt"},"lookup_realm":{"name":"jwt2","type":"jwt"},"authentication_type":"realm"} -% ---- From 8bccf664b0c3d8814c485256a475e6a1857ede5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Slobodan=20Adamovi=C4=87?= Date: Mon, 19 Dec 2022 16:33:02 +0100 Subject: [PATCH 306/919] Pre-authorize child search transport actions (#91886) This PR aims to improve authorization performance of `indices:data/read/search` action by avoiding authorizing the transport child actions on every node. The focus is on index search child actions since they are accessing just a subset of parent's indices. Some optimizations already exist which allow the children of authorized parent actions on the same node to skip authorization (#77221). This PR adds ability to do the same optimization, but for search child actions that are executed on remote nodes in the same cluster. The optimization is realized through "parent" authorization header. The header is set in the thread context when parent action is successfully authorized and removed after it has been used to skip child authorization. It's worth noting that a parent authorization header is removed in two other cases: - before request is sent to a remote cluster - when transport action being sent is not a child of a parent for which authorization exists in thread context --- docs/changelog/91886.yaml | 5 + .../common/util/concurrent/ThreadContext.java | 59 +++- .../util/concurrent/ThreadContextTests.java | 285 +++++++++++++++++- .../xpack/core/security/SecurityContext.java | 36 +++ .../security/authz/AuthorizationEngine.java | 91 +++++- .../authz/ParentActionAuthorizationTests.java | 35 +++ .../security/authz/AuthorizationService.java | 39 ++- .../security/authz/PreAuthorizationUtils.java | 198 ++++++++++++ .../xpack/security/authz/RBACEngine.java | 8 +- .../SecurityServerTransportInterceptor.java | 147 ++++++--- .../xpack/security/SecurityContextTests.java | 85 ++++++ .../authz/AuthorizationServiceTests.java | 9 +- .../authz/PreAuthorizationUtilsTests.java | 158 ++++++++++ .../xpack/security/authz/RBACEngineTests.java | 165 +++++++++- 14 files changed, 1243 insertions(+), 77 deletions(-) create mode 100644 docs/changelog/91886.yaml create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/ParentActionAuthorizationTests.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/PreAuthorizationUtils.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/PreAuthorizationUtilsTests.java diff --git a/docs/changelog/91886.yaml b/docs/changelog/91886.yaml new file mode 100644 index 000000000000..b32ac9e5b5d0 --- /dev/null +++ b/docs/changelog/91886.yaml @@ -0,0 +1,5 @@ +pr: 91886 +summary: Pre-authorize child search transport actions +area: Authorization +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java b/server/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java index 16d11a3cbc3c..ef004bb5d9e5 100644 --- a/server/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java +++ b/server/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java @@ -312,11 +312,29 @@ public StoredContext newStoredContext() { /** * Just like {@link #stashContext()} but no default context is set. Instead, the {@code transientHeadersToClear} argument can be used - * to clear specific transient headers in the new context. All headers (with the possible exception of {@code responseHeaders}) are - * restored by closing the returned {@link StoredContext}. - * + * to clear specific transient headers in the new context and {@code requestHeadersToClear} can be used to clear specific request + * headers. All original headers (without the {@code responseHeaders}) are restored by closing the returned {@link StoredContext}. + */ + public StoredContext newStoredContext(Collection transientHeadersToClear, Collection requestHeadersToClear) { + return newStoredContext(false, transientHeadersToClear, requestHeadersToClear); + } + + /** + * Just like {@link #newStoredContext(Collection, Collection)} but all headers are restored to original, + * except of {@code responseHeaders} which will be preserved from the restore thread. */ - public StoredContext newStoredContext(Collection transientHeadersToClear) { + public StoredContext newStoredContextPreservingResponseHeaders( + Collection transientHeadersToClear, + Collection requestHeadersToClear + ) { + return newStoredContext(true, transientHeadersToClear, requestHeadersToClear); + } + + private StoredContext newStoredContext( + boolean preserveResponseHeaders, + Collection transientHeadersToClear, + Collection requestHeadersToClear + ) { final ThreadContextStruct originalContext = threadLocal.get(); // clear specific transient headers from the current context Map newTransientHeaders = null; @@ -328,18 +346,34 @@ public StoredContext newStoredContext(Collection transientHeadersToClear newTransientHeaders.remove(transientHeaderToClear); } } - // this is the context when this method returns - if (newTransientHeaders != null) { + Map newRequestHeaders = null; + for (String requestHeaderToClear : requestHeadersToClear) { + if (originalContext.requestHeaders.containsKey(requestHeaderToClear)) { + if (newRequestHeaders == null) { + newRequestHeaders = new HashMap<>(originalContext.requestHeaders); + } + newRequestHeaders.remove(requestHeaderToClear); + } + } + if (newTransientHeaders != null || newRequestHeaders != null) { ThreadContextStruct threadContextStruct = new ThreadContextStruct( - originalContext.requestHeaders, + newRequestHeaders != null ? newRequestHeaders : originalContext.requestHeaders, originalContext.responseHeaders, - newTransientHeaders, + newTransientHeaders != null ? newTransientHeaders : originalContext.transientHeaders, originalContext.isSystemContext, originalContext.warningHeadersSize ); threadLocal.set(threadContextStruct); } - return storedOriginalContext(originalContext); + // this is the context when this method returns + final ThreadContextStruct newContext = threadLocal.get(); + return () -> { + if (preserveResponseHeaders && threadLocal.get() != newContext) { + threadLocal.set(originalContext.putResponseHeaders(threadLocal.get().responseHeaders)); + } else { + threadLocal.set(originalContext); + } + }; } /** @@ -510,6 +544,13 @@ public T getTransient(String key) { return (T) threadLocal.get().transientHeaders.get(key); } + /** + * Returns unmodifiable copy of all transient headers. + */ + public Map getTransientHeaders() { + return Collections.unmodifiableMap(threadLocal.get().transientHeaders); + } + /** * Add the {@code value} for the specified {@code key} Any duplicate {@code value} is ignored. * diff --git a/server/src/test/java/org/elasticsearch/common/util/concurrent/ThreadContextTests.java b/server/src/test/java/org/elasticsearch/common/util/concurrent/ThreadContextTests.java index f17962ea7574..717b87799ea5 100644 --- a/server/src/test/java/org/elasticsearch/common/util/concurrent/ThreadContextTests.java +++ b/server/src/test/java/org/elasticsearch/common/util/concurrent/ThreadContextTests.java @@ -33,6 +33,7 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; public class ThreadContextTests extends ESTestCase { @@ -73,7 +74,8 @@ public void testNewContextWithClearedTransients() { // foo is the only existing transient header that is cleared try ( ThreadContext.StoredContext stashed = threadContext.newStoredContext( - randomFrom(List.of("foo", "foo"), List.of("foo"), List.of("foo", "acme")) + randomFrom(List.of("foo", "foo"), List.of("foo"), List.of("foo", "acme")), + List.of() ) ) { // only the requested transient header is cleared @@ -113,7 +115,8 @@ public void testNewContextWithClearedTransients() { // test stashed missing header stays missing try ( ThreadContext.StoredContext stashed = threadContext.newStoredContext( - randomFrom(Arrays.asList("acme", "acme"), Arrays.asList("acme")) + randomFrom(Arrays.asList("acme", "acme"), Arrays.asList("acme")), + List.of() ) ) { assertNull(threadContext.getTransient("acme")); @@ -122,6 +125,284 @@ public void testNewContextWithClearedTransients() { assertNull(threadContext.getTransient("acme")); } + public void testNewContextWithClearedRequestHeaders() { + ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + + final Map requestHeaders = Map.ofEntries( + Map.entry(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)), + Map.entry(Task.X_OPAQUE_ID_HTTP_HEADER, randomAlphaOfLength(10)), + Map.entry(Task.TRACE_ID, randomAlphaOfLength(20)), + Map.entry("_username", "elastic-admin") + ); + threadContext.putHeader(requestHeaders); + + final Map transientHeaders = Map.ofEntries( + Map.entry(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)), + Map.entry("_random", randomAlphaOfLengthBetween(3, 8)), + Map.entry("_map", Map.of("key", new Object())), + Map.entry("_address", "125.124.123.122"), + Map.entry("_object", new Object()), + Map.entry("_number", 42) + ); + transientHeaders.forEach((k, v) -> threadContext.putTransient(k, v)); + + final Map responseHeaders = Map.ofEntries( + Map.entry(randomAlphaOfLengthBetween(3, 6), randomAlphaOfLengthBetween(3, 8)), + Map.entry("_response_message", "All good."), + Map.entry("Warning", "Some warning!") + ); + responseHeaders.forEach((k, v) -> threadContext.addResponseHeader(k, v)); + + // this is missing or null + if (randomBoolean()) { + threadContext.putHeader("_missing_or_null", null); + } + + // mark as system context + boolean setSystemContext = randomBoolean(); + if (setSystemContext) { + threadContext.markAsSystemContext(); + } + + // adding password header here to simplify assertions + threadContext.putHeader("_password", "elastic-password"); + + // password is the only request header that should be cleared + try ( + ThreadContext.StoredContext stashed = threadContext.newStoredContext( + List.of(), + randomFrom(List.of("_password", "_password"), List.of("_password"), List.of("_password", "_missing_or_null")) + ) + ) { + // only the requested header is cleared + assertThat(threadContext.getHeader("_password"), nullValue()); + // system context boolean is preserved + assertThat(threadContext.isSystemContext(), equalTo(setSystemContext)); + // missing header is still missing + assertThat(threadContext.getHeader("_missing_or_null"), nullValue()); + // other headers are preserved + requestHeaders.forEach((k, v) -> assertThat(threadContext.getHeader(k), equalTo(v))); + transientHeaders.forEach((k, v) -> assertThat(threadContext.getTransient(k), equalTo(v))); + responseHeaders.forEach((k, v) -> assertThat(threadContext.getResponseHeaders().get(k).get(0), equalTo(v))); + // warning header count is still equal to 1 + assertThat(threadContext.getResponseHeaders().get("Warning").size(), equalTo(1)); + + // try override stashed header + threadContext.putHeader("_password", "new-password"); + assertThat(threadContext.getHeader("_password"), equalTo("new-password")); + // add new headers + threadContext.addResponseHeader("_new_response_header", randomAlphaOfLengthBetween(3, 8)); + threadContext.putTransient("_new_transient_header", randomAlphaOfLengthBetween(3, 8)); + threadContext.putHeader("_new_request_header", randomAlphaOfLengthBetween(3, 8)); + threadContext.addResponseHeader("Warning", randomAlphaOfLengthBetween(3, 8)); + // warning header is now equal to 2 + assertThat(threadContext.getResponseHeaders().get("Warning").size(), equalTo(2)); + } + + // original "password" header is restored (it is not overridden) + assertThat(threadContext.getHeader("_password"), equalTo("elastic-password")); + // headers added inside the stash are NOT preserved + assertThat(threadContext.getResponseHeaders().get("_new_response_header"), nullValue()); + assertThat(threadContext.getTransient("_new_transient_header"), nullValue()); + assertThat(threadContext.getHeader("_new_request_header"), nullValue()); + // warning header is restored to 1 + assertThat(threadContext.getResponseHeaders().get("Warning").size(), equalTo(1)); + assertThat(threadContext.getResponseHeaders().get("Warning").get(0), equalTo("Some warning!")); + // original headers are restored + requestHeaders.forEach((k, v) -> assertThat(threadContext.getHeader(k), equalTo(v))); + transientHeaders.forEach((k, v) -> assertThat(threadContext.getTransient(k), equalTo(v))); + responseHeaders.forEach((k, v) -> assertThat(threadContext.getResponseHeaders().get(k).get(0), equalTo(v))); + // system context boolean is unchanged + assertThat(threadContext.isSystemContext(), equalTo(setSystemContext)); + + // test stashed missing header stays missing + try ( + ThreadContext.StoredContext stashed = threadContext.newStoredContext( + randomFrom(Arrays.asList("_missing_or_null", "_missing_or_null"), Arrays.asList("_missing_or_null")), + List.of() + ) + ) { + assertThat(threadContext.getHeader("_missing_or_null"), nullValue()); + threadContext.putHeader("_missing_or_null", "not_null"); + } + assertThat(threadContext.getHeader("_missing_or_null"), nullValue()); + } + + public void testNewContextWithoutClearingTransientAndRequestHeaders() { + ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + + final Map requestHeaders = Map.ofEntries( + Map.entry(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)), + Map.entry(Task.X_OPAQUE_ID_HTTP_HEADER, randomAlphaOfLength(10)), + Map.entry(Task.TRACE_ID, randomAlphaOfLength(20)), + Map.entry("_username", "elastic-admin") + ); + threadContext.putHeader(requestHeaders); + + final Map transientHeaders = Map.ofEntries( + Map.entry(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)), + Map.entry("_random", randomAlphaOfLengthBetween(3, 8)), + Map.entry("_map", Map.of("key", new Object())), + Map.entry("_address", "125.124.123.122"), + Map.entry("_object", new Object()), + Map.entry("_number", 42) + ); + transientHeaders.forEach((k, v) -> threadContext.putTransient(k, v)); + + final Map responseHeaders = Map.ofEntries( + Map.entry(randomAlphaOfLengthBetween(3, 6), randomAlphaOfLengthBetween(3, 8)), + Map.entry("_response_message", "All good."), + Map.entry("Warning", "Some warning!") + ); + responseHeaders.forEach((k, v) -> threadContext.addResponseHeader(k, v)); + + // mark as system context + boolean setSystemContext = randomBoolean(); + if (setSystemContext) { + threadContext.markAsSystemContext(); + } + + // test nothing is cleared when empty collections are passed + try (ThreadContext.StoredContext stashed = threadContext.newStoredContext(List.of(), List.of())) { + // system context boolean is preserved + assertThat(threadContext.isSystemContext(), equalTo(setSystemContext)); + // other headers are preserved + assertThat(threadContext.getHeaders().size(), equalTo(requestHeaders.size())); + assertThat(threadContext.getResponseHeaders().size(), equalTo(responseHeaders.size())); + assertThat(threadContext.getTransientHeaders().size(), equalTo(transientHeaders.size())); + requestHeaders.forEach((k, v) -> assertThat(threadContext.getHeader(k), equalTo(v))); + transientHeaders.forEach((k, v) -> assertThat(threadContext.getTransient(k), equalTo(v))); + responseHeaders.forEach((k, v) -> assertThat(threadContext.getResponseHeaders().get(k).get(0), equalTo(v))); + // warning header count is still equal to 1 + assertThat(threadContext.getResponseHeaders().get("Warning").size(), equalTo(1)); + // add new headers + threadContext.addResponseHeader("_new_response_header", randomAlphaOfLengthBetween(3, 8)); + threadContext.putTransient("_new_transient_header", randomAlphaOfLengthBetween(3, 8)); + threadContext.putHeader("_new_request_header", randomAlphaOfLengthBetween(3, 8)); + threadContext.addResponseHeader("Warning", randomAlphaOfLengthBetween(3, 8)); + // warning header is now equal to 2 + assertThat(threadContext.getResponseHeaders().get("Warning").size(), equalTo(2)); + } + + // headers added inside the stash are NOT preserved + assertThat(threadContext.getResponseHeaders().get("_new_response_header"), nullValue()); + assertThat(threadContext.getTransient("_new_transient_header"), nullValue()); + assertThat(threadContext.getHeader("_new_request_header"), nullValue()); + // original headers are unchanged + assertThat(threadContext.getHeaders().size(), equalTo(requestHeaders.size())); + assertThat(threadContext.getResponseHeaders().size(), equalTo(responseHeaders.size())); + assertThat(threadContext.getTransientHeaders().size(), equalTo(transientHeaders.size())); + requestHeaders.forEach((k, v) -> assertThat(threadContext.getHeader(k), equalTo(v))); + transientHeaders.forEach((k, v) -> assertThat(threadContext.getTransient(k), equalTo(v))); + responseHeaders.forEach((k, v) -> assertThat(threadContext.getResponseHeaders().get(k).get(0), equalTo(v))); + // system context boolean is unchanged + assertThat(threadContext.isSystemContext(), equalTo(setSystemContext)); + // warning header is unchanged + assertThat(threadContext.getResponseHeaders().get("Warning").size(), equalTo(1)); + assertThat(threadContext.getResponseHeaders().get("Warning").get(0), equalTo("Some warning!")); + } + + public void testNewContextPreservingResponseHeadersWithClearedTransientAndRequestHeaders() { + ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + + final Map requestHeaders = Map.ofEntries( + Map.entry(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)), + Map.entry(Task.X_OPAQUE_ID_HTTP_HEADER, randomAlphaOfLength(10)), + Map.entry(Task.TRACE_ID, randomAlphaOfLength(20)), + Map.entry("_username", "elastic-admin") + ); + threadContext.putHeader(requestHeaders); + + final Map transientHeaders = Map.ofEntries( + Map.entry(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)), + Map.entry("_random", randomAlphaOfLengthBetween(3, 8)), + Map.entry("_map", Map.of("key", new Object())), + Map.entry("_address", "125.124.123.122"), + Map.entry("_object", new Object()), + Map.entry("_number", 42) + ); + transientHeaders.forEach((k, v) -> threadContext.putTransient(k, v)); + + final Map responseHeaders = Map.ofEntries( + Map.entry(randomAlphaOfLengthBetween(3, 6), randomAlphaOfLengthBetween(3, 8)), + Map.entry("_response_message", "All good."), + Map.entry("Warning", "Some warning!") + ); + responseHeaders.forEach((k, v) -> threadContext.addResponseHeader(k, v)); + + // this is missing or null + if (randomBoolean()) { + threadContext.putHeader("_missing_or_null", null); + threadContext.putTransient("_missing_or_null", null); + } + + // mark as system context + boolean setSystemContext = randomBoolean(); + if (setSystemContext) { + threadContext.markAsSystemContext(); + } + + // adding request and transient headers to be cleared later + threadContext.putHeader("_password", "elastic-password"); + threadContext.putTransient("_transient_to_be_cleared", "original-transient-value"); + + // password is the only request header that should be cleared + try ( + ThreadContext.StoredContext stashed = threadContext.newStoredContextPreservingResponseHeaders( + randomFrom( + List.of("_transient_to_be_cleared"), + List.of("_transient_to_be_cleared", "_transient_to_be_cleared"), + List.of("_transient_to_be_cleared", "_missing_or_null") + ), + randomFrom(List.of("_password", "_password"), List.of("_password"), List.of("_password", "_missing_or_null")) + ) + ) { + // only the requested headers are cleared + assertThat(threadContext.getHeader("_password"), nullValue()); + assertThat(threadContext.getTransient("_transient_to_be_cleared"), nullValue()); + // system context boolean is preserved + assertThat(threadContext.isSystemContext(), equalTo(setSystemContext)); + // missing header is still missing + assertThat(threadContext.getHeader("_missing_or_null"), nullValue()); + assertThat(threadContext.getTransient("_missing_or_null"), nullValue()); + // other headers are preserved + requestHeaders.forEach((k, v) -> assertThat(threadContext.getHeader(k), equalTo(v))); + transientHeaders.forEach((k, v) -> assertThat(threadContext.getTransient(k), equalTo(v))); + responseHeaders.forEach((k, v) -> assertThat(threadContext.getResponseHeaders().get(k).get(0), equalTo(v))); + // warning header count is still equal to 1 + assertThat(threadContext.getResponseHeaders().get("Warning").size(), equalTo(1)); + + // try override stashed headers + threadContext.putHeader("_password", "new-password"); + threadContext.putTransient("_transient_to_be_cleared", "new-transient-value"); + assertThat(threadContext.getHeader("_password"), equalTo("new-password")); + assertThat(threadContext.getTransient("_transient_to_be_cleared"), equalTo("new-transient-value")); + // add new headers + threadContext.addResponseHeader("_new_response_header", "value-which-should-be-preserved"); + threadContext.putTransient("_new_transient_header", randomAlphaOfLengthBetween(3, 8)); + threadContext.putHeader("_new_request_header", randomAlphaOfLengthBetween(3, 8)); + threadContext.addResponseHeader("Warning", "Another warning!"); + // warning header is now equal to 2 + assertThat(threadContext.getResponseHeaders().get("Warning").size(), equalTo(2)); + } + + // originally cleared headers should be restored (and not overridden) + assertThat(threadContext.getHeader("_password"), equalTo("elastic-password")); + assertThat(threadContext.getTransient("_transient_to_be_cleared"), equalTo("original-transient-value")); + requestHeaders.forEach((k, v) -> assertThat(threadContext.getHeader(k), equalTo(v))); + transientHeaders.forEach((k, v) -> assertThat(threadContext.getTransient(k), equalTo(v))); + // headers added inside the stash are NOT preserved + assertThat(threadContext.getTransient("_new_transient_header"), nullValue()); + assertThat(threadContext.getHeader("_new_request_header"), nullValue()); + // except for response headers which should be preserved + assertThat(threadContext.getResponseHeaders().get("_new_response_header").get(0), equalTo("value-which-should-be-preserved")); + assertThat(threadContext.getResponseHeaders().get("Warning").size(), equalTo(2)); + assertThat(threadContext.getResponseHeaders().get("Warning").get(0), equalTo("Some warning!")); + assertThat(threadContext.getResponseHeaders().get("Warning").get(1), equalTo("Another warning!")); + // system context boolean is unchanged + assertThat(threadContext.isSystemContext(), equalTo(setSystemContext)); + } + public void testStashWithOrigin() { final String origin = randomAlphaOfLengthBetween(4, 16); final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityContext.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityContext.java index 744c07984509..06b5922a57c6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityContext.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityContext.java @@ -20,6 +20,7 @@ import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer; import org.elasticsearch.xpack.core.security.authc.support.SecondaryAuthentication; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.ParentActionAuthorization; import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; import org.elasticsearch.xpack.core.security.user.SystemUser; @@ -27,6 +28,7 @@ import java.io.IOException; import java.io.UncheckedIOException; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Consumer; @@ -86,6 +88,24 @@ public AuthorizationEngine.AuthorizationInfo getAuthorizationInfoFromContext() { return Objects.requireNonNull(threadContext.getTransient(AUTHORIZATION_INFO_KEY), "authorization info is missing from context"); } + @Nullable + public ParentActionAuthorization getParentAuthorization() { + try { + return ParentActionAuthorization.readFromThreadContext(threadContext); + } catch (IOException e) { + logger.error("failed to read parent authorization from thread context", e); + throw new UncheckedIOException(e); + } + } + + public void setParentAuthorization(ParentActionAuthorization parentAuthorization) { + try { + parentAuthorization.writeToThreadContext(threadContext); + } catch (IOException e) { + throw new AssertionError("failed to write parent authorization to the thread context", e); + } + } + /** * Returns the "secondary authentication" (see {@link SecondaryAuthentication}) information, * or {@code null} if the current request does not have a secondary authentication context @@ -186,6 +206,22 @@ public void executeAfterRewritingAuthentication(Consumer consumer } } + /** + * Executes consumer in a new thread context after removing {@link ParentActionAuthorization}. + * The original context is provided to the consumer. When this method returns, + * the original context is restored preserving response headers. + */ + public void executeAfterRemovingParentAuthorization(Consumer consumer) { + try ( + ThreadContext.StoredContext original = threadContext.newStoredContextPreservingResponseHeaders( + List.of(), + List.of(ParentActionAuthorization.THREAD_CONTEXT_KEY) + ) + ) { + consumer.accept(original); + } + } + /** * Checks whether the user or API key of the passed in authentication can access the resources owned by the user * or API key of this authentication. The rules are as follows: diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index 553d731c3ab2..f6964fdaf252 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -12,8 +12,11 @@ import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.cluster.metadata.IndexAbstraction; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.core.Nullable; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse; @@ -26,6 +29,7 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -480,17 +484,30 @@ final class RequestInfo { private final String action; @Nullable private final AuthorizationContext originatingAuthorizationContext; + @Nullable + private final ParentActionAuthorization parentAuthorization; public RequestInfo( Authentication authentication, TransportRequest request, String action, - AuthorizationContext originatingContext + AuthorizationContext originatingContext, + ParentActionAuthorization parentAuthorization ) { this.authentication = Objects.requireNonNull(authentication); this.request = Objects.requireNonNull(request); this.action = Objects.requireNonNull(action); this.originatingAuthorizationContext = originatingContext; + this.parentAuthorization = parentAuthorization; + } + + public RequestInfo( + Authentication authentication, + TransportRequest request, + String action, + AuthorizationContext originatingContext + ) { + this(authentication, request, action, originatingContext, null); } public String getAction() { @@ -510,6 +527,11 @@ public AuthorizationContext getOriginatingAuthorizationContext() { return originatingAuthorizationContext; } + @Nullable + public ParentActionAuthorization getParentAuthorization() { + return parentAuthorization; + } + @Override public String toString() { return getClass().getSimpleName() @@ -521,8 +543,10 @@ public String toString() { + "], action=[" + action + ']' - + ", parent=[" + + ", originating=[" + originatingAuthorizationContext + + "], parent=[" + + parentAuthorization + "]}"; } @@ -667,6 +691,69 @@ public IndicesAccessControl getIndicesAccessControl() { } } + /** + * Holds information about authorization of a parent action which is used to pre-authorize its child actions. + * + * @param action the parent action + */ + record ParentActionAuthorization(String action) implements Writeable { + + public static final String THREAD_CONTEXT_KEY = "_xpack_security_parent_action_authz"; + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(action); + } + + /** + * Reads an {@link ParentActionAuthorization} from a {@link StreamInput} + * + * @param in the {@link StreamInput} to read from + * @return {@link ParentActionAuthorization} + * @throws IOException if I/O operation fails + */ + public static ParentActionAuthorization readFrom(StreamInput in) throws IOException { + String action = in.readString(); + return new ParentActionAuthorization(action); + } + + /** + * Read and deserialize parent authorization from thread context. + * + * @param context the thread context to read from + * @return {@link ParentActionAuthorization} or null + * @throws IOException if reading fails due to I/O exception + */ + @Nullable + public static ParentActionAuthorization readFromThreadContext(ThreadContext context) throws IOException { + final String header = context.getHeader(THREAD_CONTEXT_KEY); + if (header == null) { + return null; + } + + byte[] bytes = Base64.getDecoder().decode(header); + StreamInput input = StreamInput.wrap(bytes); + return readFrom(input); + } + + /** + * Writes the authorization to the context. There must not be an existing authorization in the context and if there is an + * {@link IllegalStateException} will be thrown. + */ + public void writeToThreadContext(ThreadContext context) throws IOException { + String header = this.encode(); + assert header != null : "parent authorization object encoded to null"; + context.putHeader(THREAD_CONTEXT_KEY, header); + } + + private String encode() throws IOException { + BytesStreamOutput output = new BytesStreamOutput(); + writeTo(output); + return Base64.getEncoder().encodeToString(BytesReference.toBytes(output.bytes())); + } + + } + @FunctionalInterface interface AsyncSupplier { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/ParentActionAuthorizationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/ParentActionAuthorizationTests.java new file mode 100644 index 000000000000..815c9e6d5400 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/ParentActionAuthorizationTests.java @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.security.authz; + +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.ParentActionAuthorization; + +import java.io.IOException; + +import static org.hamcrest.Matchers.equalTo; + +/** + * Tests for the {@link ParentActionAuthorization} class. + */ +public class ParentActionAuthorizationTests extends ESTestCase { + + public void testSerialization() throws IOException { + ParentActionAuthorization authorization = createRandom(); + final BytesStreamOutput out = new BytesStreamOutput(); + authorization.writeTo(out); + assertThat(ParentActionAuthorization.readFrom(out.bytes().streamInput()), equalTo(authorization)); + } + + private static ParentActionAuthorization createRandom() { + String action = randomAlphaOfLengthBetween(5, 20); + return new ParentActionAuthorization(action); + } + +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java index 7975badddd70..1b93e403cbe8 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java @@ -57,6 +57,7 @@ import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationResult; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.EmptyAuthorizationInfo; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.IndexAuthorizationResult; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.ParentActionAuthorization; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.RequestInfo; import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; @@ -262,6 +263,7 @@ public void authorize( ) { final AuthorizationContext enclosingContext = extractAuthorizationContext(threadContext, action); + final ParentActionAuthorization parentAuthorization = securityContext.getParentAuthorization(); /* authorization fills in certain transient headers, which must be observed in the listener (action handler execution) * as well, but which must not bleed across different action context (eg parent-child action contexts). @@ -270,7 +272,12 @@ public void authorize( * previous parent action that ran under the same thread context (also on the same node). * When the returned {@code StoredContext} is closed, ALL the original headers are restored. */ - try (ThreadContext.StoredContext ignore = threadContext.newStoredContext(ACTION_SCOPE_AUTHORIZATION_KEYS)) { + try ( + ThreadContext.StoredContext ignore = threadContext.newStoredContext( + ACTION_SCOPE_AUTHORIZATION_KEYS, + List.of(ParentActionAuthorization.THREAD_CONTEXT_KEY) + ) + ) { // this does not clear {@code AuthorizationServiceField.ORIGINATING_ACTION_KEY} // prior to doing any authorization lets set the originating action in the thread context // the originating action is the current action if no originating action has yet been set in the current thread context @@ -300,7 +307,13 @@ public void authorize( // this never goes async so no need to wrap the listener authorizeSystemUser(authentication, action, auditId, unwrappedRequest, listener); } else { - final RequestInfo requestInfo = new RequestInfo(authentication, unwrappedRequest, action, enclosingContext); + final RequestInfo requestInfo = new RequestInfo( + authentication, + unwrappedRequest, + action, + enclosingContext, + parentAuthorization + ); final AuthorizationEngine engine = getAuthorizationEngine(authentication); final ActionListener authzInfoListener = wrapPreservingContext(ActionListener.wrap(authorizationInfo -> { threadContext.putTransient(AUTHORIZATION_INFO_KEY, authorizationInfo); @@ -508,10 +521,15 @@ private void handleIndexActionAuthorizationResult( final Metadata metadata, final ActionListener listener ) { + final IndicesAccessControl indicesAccessControl = result.getIndicesAccessControl(); final Authentication authentication = requestInfo.getAuthentication(); final TransportRequest request = requestInfo.getRequest(); final String action = requestInfo.getAction(); - securityContext.putIndicesAccessControl(result.getIndicesAccessControl()); + securityContext.putIndicesAccessControl(indicesAccessControl); + + final AuthorizationContext authzContext = new AuthorizationContext(action, authzInfo, indicesAccessControl); + PreAuthorizationUtils.maybeSkipChildrenActionAuthorization(securityContext, authzContext); + // if we are creating an index we need to authorize potential aliases created at the same time if (IndexPrivilege.CREATE_INDEX_MATCHER.test(action)) { assert (request instanceof CreateIndexRequest) @@ -523,12 +541,7 @@ private void handleIndexActionAuthorizationResult( runRequestInterceptors(requestInfo, authzInfo, authorizationEngine, listener); } else { Set aliases = ((CreateIndexRequest) request).aliases(); - final AuthorizationContext parentContext = new AuthorizationContext( - requestInfo.getAction(), - authzInfo, - result.getIndicesAccessControl() - ); - final RequestInfo aliasesRequestInfo = new RequestInfo(authentication, request, IndicesAliasesAction.NAME, parentContext); + final RequestInfo aliasesRequestInfo = new RequestInfo(authentication, request, IndicesAliasesAction.NAME, authzContext); authzEngine.authorizeIndexAction(aliasesRequestInfo, authzInfo, ril -> { resolvedIndicesAsyncSupplier.getAsync(ActionListener.wrap(resolvedIndices -> { List aliasesAndIndices = new ArrayList<>(resolvedIndices.getLocal()); @@ -556,11 +569,6 @@ private void handleIndexActionAuthorizationResult( // if this is performing multiple actions on the index, then check each of those actions. assert request instanceof BulkShardRequest : "Action " + action + " requires " + BulkShardRequest.class + " but was " + request.getClass(); - final AuthorizationContext authzContext = new AuthorizationContext( - requestInfo.getAction(), - authzInfo, - result.getIndicesAccessControl() - ); authorizeBulkItems( requestInfo, authzContext, @@ -838,7 +846,8 @@ private void authorizeBulkItems( requestInfo.getAuthentication(), requestInfo.getRequest(), bulkItemAction, - bulkAuthzContext + bulkAuthzContext, + requestInfo.getParentAuthorization() ); authzEngine.authorizeIndexAction( bulkItemInfo, diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/PreAuthorizationUtils.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/PreAuthorizationUtils.java new file mode 100644 index 000000000000..67a4d14177fa --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/PreAuthorizationUtils.java @@ -0,0 +1,198 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authz; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.search.SearchAction; +import org.elasticsearch.action.search.SearchTransportService; +import org.elasticsearch.xpack.core.security.SecurityContext; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationContext; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.ParentActionAuthorization; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.RequestInfo; +import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField; +import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; +import org.elasticsearch.xpack.core.security.authz.permission.Role; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +public final class PreAuthorizationUtils { + + private static final Logger logger = LogManager.getLogger(PreAuthorizationUtils.class); + + /** + * This map holds parent-child action relationships for which we can optimize authorization + * and skip authorization for child actions if the parent action is successfully authorized. + * Normally every action would be authorized on a local node on which it's being executed. + * Here we define all child actions for which the authorization can be safely skipped + * on a remote node as they only access a subset of resources. + */ + public static final Map> CHILD_ACTIONS_PRE_AUTHORIZED_BY_PARENT = Map.of( + SearchAction.NAME, + Set.of( + SearchTransportService.FREE_CONTEXT_ACTION_NAME, + SearchTransportService.DFS_ACTION_NAME, + SearchTransportService.QUERY_ACTION_NAME, + SearchTransportService.QUERY_ID_ACTION_NAME, + SearchTransportService.FETCH_ID_ACTION_NAME, + SearchTransportService.QUERY_CAN_MATCH_NAME, + SearchTransportService.QUERY_CAN_MATCH_NODE_NAME + ) + ); + + /** + * This method sets {@link ParentActionAuthorization} as a header in the thread context, + * which will be used for skipping authorization of child actions if the following conditions are met: + * + *

      + *
    • parent action is one of the white listed in {@link #CHILD_ACTIONS_PRE_AUTHORIZED_BY_PARENT}
    • + *
    • FLS and DLS are not configured
    • + *
    • RBACEngine was used to authorize parent request and not a custom authorization engine
    • + *
    + */ + public static void maybeSkipChildrenActionAuthorization( + SecurityContext securityContext, + AuthorizationContext parentAuthorizationContext + ) { + final String parentAction = parentAuthorizationContext.getAction(); + if (CHILD_ACTIONS_PRE_AUTHORIZED_BY_PARENT.containsKey(parentAction) == false) { + // This is not one of the white listed parent actions. + return; + } + + final IndicesAccessControl indicesAccessControl = parentAuthorizationContext.getIndicesAccessControl(); + if (indicesAccessControl == null) { + // This can happen if the parent request was authorized by index name only - e.g. bulk request + // A missing IAC is not an error, but it means we can't safely tie authz of the child action to the parent authz + return; + } + + // Just a sanity check. If we ended up here, the authz should have been granted. + if (indicesAccessControl.isGranted() == false) { + return; + } + + final Role role = RBACEngine.maybeGetRBACEngineRole(parentAuthorizationContext.getAuthorizationInfo()); + if (role == null) { + // If role is null, it means a custom authorization engine is in use, hence we cannot do the optimization here. + return; + } + + // We can't safely pre-authorize actions if DLS or FLS is configured without passing IAC as well with the authorization result. + // For simplicity, we only pre-authorize actions when FLS and DLS are not configured, otherwise we would have to compute and send + // indices access control as well, which we want to avoid with this optimization. + if (role.hasFieldOrDocumentLevelSecurity()) { + return; + } + + final ParentActionAuthorization existingParentAuthorization = securityContext.getParentAuthorization(); + if (existingParentAuthorization != null) { + throw new AssertionError( + "found parent authorization for action [" + + existingParentAuthorization.action() + + "] while attempting to set authorization for new parent action [" + + parentAction + + "]" + ); + } else { + if (logger.isDebugEnabled()) { + logger.debug("adding authorization for parent action [" + parentAction + "] to the thread context"); + } + securityContext.setParentAuthorization(new ParentActionAuthorization(parentAction)); + } + } + + private static boolean shouldPreAuthorizeChildActionOfParent(final String parent, final String child) { + final Set children = CHILD_ACTIONS_PRE_AUTHORIZED_BY_PARENT.get(parent); + return children != null && children.contains(child); + } + + public static boolean shouldRemoveParentAuthorizationFromThreadContext( + Optional remoteClusterAlias, + String childAction, + SecurityContext securityContext + ) { + final ParentActionAuthorization parentAuthorization = securityContext.getParentAuthorization(); + if (parentAuthorization == null) { + // Nothing to remove. + return false; + } + + if (remoteClusterAlias.isPresent()) { + // We never want to send the parent authorization header to remote clusters. + return true; + } + + if (shouldPreAuthorizeChildActionOfParent(parentAuthorization.action(), childAction) == false) { + // We want to remove the parent authorization header if the child action is not one of the white listed. + return true; + } + + return false; + } + + public static boolean shouldPreAuthorizeChildByParentAction(RequestInfo childRequestInfo, AuthorizationInfo childAuthorizationInfo) { + + final ParentActionAuthorization parentAuthorization = childRequestInfo.getParentAuthorization(); + if (parentAuthorization == null) { + return false; + } + + Role role = RBACEngine.maybeGetRBACEngineRole(childAuthorizationInfo); + if (role == null) { + // If role is null, it means a custom authorization engine is in use. + return false; + } + if (role.hasFieldOrDocumentLevelSecurity()) { + // We can't safely pre-authorize actions if DLS or FLS is configured + // without sending IAC as well with authorization result. + return false; + } + + final String parentAction = parentAuthorization.action(); + final String childAction = childRequestInfo.getAction(); + if (shouldPreAuthorizeChildActionOfParent(parentAction, childAction) == false) { + // We only pre-authorize explicitly allowed child actions. + return false; + } + + final IndicesRequest indicesRequest; + if (childRequestInfo.getRequest() instanceof IndicesRequest) { + indicesRequest = (IndicesRequest) childRequestInfo.getRequest(); + } else { + // Can only handle indices request here + return false; + } + + final String[] indices = indicesRequest.indices(); + if (indices == null || indices.length == 0) { + // No indices to check + return false; + } + + if (Arrays.equals(IndicesAndAliasesResolverField.NO_INDICES_OR_ALIASES_ARRAY, indices)) { + // Special placeholder for no indices. + // We probably can short circuit this, but it's safer not to and just fall through to the regular authorization + return false; + } + + if (logger.isDebugEnabled()) { + logger.debug("pre-authorizing child action [" + childAction + "] of parent action [" + parentAction + "]"); + } + return true; + } + + private PreAuthorizationUtils() { + throw new IllegalAccessError(); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index 9ff3942d6f1f..43aef998e284 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -355,8 +355,12 @@ public void authorizeIndexAction( ) ); } - } else if (isChildActionAuthorizedByParent(requestInfo, authorizationInfo)) { + } else if (isChildActionAuthorizedByParentOnLocalNode(requestInfo, authorizationInfo)) { listener.onResponse(new IndexAuthorizationResult(requestInfo.getOriginatingAuthorizationContext().getIndicesAccessControl())); + } else if (PreAuthorizationUtils.shouldPreAuthorizeChildByParentAction(requestInfo, authorizationInfo)) { + // We only pre-authorize child actions if DLS/FLS is not configured, + // hence we can allow here access for all requested indices. + listener.onResponse(new IndexAuthorizationResult(IndicesAccessControl.allowAll())); } else if (allowsRemoteIndices(request) || role.checkIndicesAction(action)) { indicesAsyncSupplier.getAsync(ActionListener.wrap(resolvedIndices -> { assert resolvedIndices.isEmpty() == false @@ -390,7 +394,7 @@ private static boolean allowsRemoteIndices(TransportRequest transportRequest) { return transportRequest instanceof IndicesRequest.Replaceable replaceable && replaceable.allowsRemoteIndices(); } - private static boolean isChildActionAuthorizedByParent(RequestInfo requestInfo, AuthorizationInfo authorizationInfo) { + private static boolean isChildActionAuthorizedByParentOnLocalNode(RequestInfo requestInfo, AuthorizationInfo authorizationInfo) { final AuthorizationContext parent = requestInfo.getOriginatingAuthorizationContext(); if (parent == null) { return false; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java index afb0030d7a27..b073c7e8eeb6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java @@ -19,6 +19,7 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.RemoteConnectionManager; import org.elasticsearch.transport.SendRequestTransportException; import org.elasticsearch.transport.Transport; import org.elasticsearch.transport.TransportChannel; @@ -37,9 +38,12 @@ import org.elasticsearch.xpack.security.authc.AuthenticationService; import org.elasticsearch.xpack.security.authz.AuthorizationService; import org.elasticsearch.xpack.security.authz.AuthorizationUtils; +import org.elasticsearch.xpack.security.authz.PreAuthorizationUtils; import java.util.Collections; import java.util.Map; +import java.util.Optional; +import java.util.function.Function; import static org.elasticsearch.xpack.core.security.SecurityField.setting; @@ -55,6 +59,7 @@ public class SecurityServerTransportInterceptor implements TransportInterceptor private final Settings settings; private final SecurityContext securityContext; private final RemoteClusterAuthorizationResolver remoteClusterAuthorizationResolver; + private final Function> remoteClusterAliasResolver; public SecurityServerTransportInterceptor( Settings settings, @@ -65,6 +70,31 @@ public SecurityServerTransportInterceptor( SecurityContext securityContext, DestructiveOperations destructiveOperations, RemoteClusterAuthorizationResolver remoteClusterAuthorizationResolver + ) { + this( + settings, + threadPool, + authcService, + authzService, + sslService, + securityContext, + destructiveOperations, + remoteClusterAuthorizationResolver, + RemoteConnectionManager::resolveRemoteClusterAlias + ); + } + + SecurityServerTransportInterceptor( + Settings settings, + ThreadPool threadPool, + AuthenticationService authcService, + AuthorizationService authzService, + SSLService sslService, + SecurityContext securityContext, + DestructiveOperations destructiveOperations, + RemoteClusterAuthorizationResolver remoteClusterAuthorizationResolver, + // Inject for simplified testing + Function> remoteClusterAliasResolver ) { this.settings = settings; this.threadPool = threadPool; @@ -74,6 +104,7 @@ public SecurityServerTransportInterceptor( this.securityContext = securityContext; this.profileFilters = initializeProfileFilters(destructiveOperations); this.remoteClusterAuthorizationResolver = remoteClusterAuthorizationResolver; + this.remoteClusterAliasResolver = remoteClusterAliasResolver; } @Override @@ -87,59 +118,84 @@ public void sendRequest( TransportRequestOptions options, TransportResponseHandler handler ) { - // the transport in core normally does this check, BUT since we are serializing to a string header we need to do it - // ourselves otherwise we wind up using a version newer than what we can actually send - final Version minVersion = Version.min(connection.getVersion(), Version.CURRENT); - - // Sometimes a system action gets executed like a internal create index request or update mappings request - // which means that the user is copied over to system actions so we need to change the user - if (AuthorizationUtils.shouldReplaceUserWithSystem(threadPool.getThreadContext(), action)) { - securityContext.executeAsSystemUser( - minVersion, - original -> sendWithUser( - connection, - action, - request, - options, - new ContextRestoreResponseHandler<>(threadPool.getThreadContext().wrapRestorable(original), handler), - sender - ) - ); - } else if (AuthorizationUtils.shouldSetUserBasedOnActionOrigin(threadPool.getThreadContext())) { - AuthorizationUtils.switchUserBasedOnActionOriginAndExecute( - threadPool.getThreadContext(), - securityContext, - minVersion, - (original) -> sendWithUser( + final Optional remoteClusterAlias = remoteClusterAliasResolver.apply(connection); + if (PreAuthorizationUtils.shouldRemoveParentAuthorizationFromThreadContext(remoteClusterAlias, action, securityContext)) { + securityContext.executeAfterRemovingParentAuthorization(original -> { + sendRequestInner( + sender, connection, action, request, options, - new ContextRestoreResponseHandler<>(threadPool.getThreadContext().wrapRestorable(original), handler), - sender - ) - ); - } else if (securityContext.getAuthentication() != null - && securityContext.getAuthentication().getEffectiveSubject().getVersion().equals(minVersion) == false) { - // re-write the authentication since we want the authentication version to match the version of the connection - securityContext.executeAfterRewritingAuthentication( - original -> sendWithUser( - connection, - action, - request, - options, - new ContextRestoreResponseHandler<>(threadPool.getThreadContext().wrapRestorable(original), handler), - sender - ), - minVersion + new ContextRestoreResponseHandler<>(threadPool.getThreadContext().wrapRestorable(original), handler) ); - } else { - sendWithUser(connection, action, request, options, handler, sender); - } + }); + } else { + sendRequestInner(sender, connection, action, request, options, handler); + } } }; } + public void sendRequestInner( + AsyncSender sender, + Transport.Connection connection, + String action, + TransportRequest request, + TransportRequestOptions options, + TransportResponseHandler handler + ) { + // the transport in core normally does this check, BUT since we are serializing to a string header we need to do it + // ourselves otherwise we wind up using a version newer than what we can actually send + final Version minVersion = Version.min(connection.getVersion(), Version.CURRENT); + + // Sometimes a system action gets executed like a internal create index request or update mappings request + // which means that the user is copied over to system actions so we need to change the user + if (AuthorizationUtils.shouldReplaceUserWithSystem(threadPool.getThreadContext(), action)) { + securityContext.executeAsSystemUser( + minVersion, + original -> sendWithUser( + connection, + action, + request, + options, + new ContextRestoreResponseHandler<>(threadPool.getThreadContext().wrapRestorable(original), handler), + sender + ) + ); + } else if (AuthorizationUtils.shouldSetUserBasedOnActionOrigin(threadPool.getThreadContext())) { + AuthorizationUtils.switchUserBasedOnActionOriginAndExecute( + threadPool.getThreadContext(), + securityContext, + minVersion, + (original) -> sendWithUser( + connection, + action, + request, + options, + new ContextRestoreResponseHandler<>(threadPool.getThreadContext().wrapRestorable(original), handler), + sender + ) + ); + } else if (securityContext.getAuthentication() != null + && securityContext.getAuthentication().getEffectiveSubject().getVersion().equals(minVersion) == false) { + // re-write the authentication since we want the authentication version to match the version of the connection + securityContext.executeAfterRewritingAuthentication( + original -> sendWithUser( + connection, + action, + request, + options, + new ContextRestoreResponseHandler<>(threadPool.getThreadContext().wrapRestorable(original), handler), + sender + ), + minVersion + ); + } else { + sendWithUser(connection, action, request, options, handler, sender); + } + } + private void sendWithUser( Transport.Connection connection, String action, @@ -155,6 +211,9 @@ private void sendWithUser( throw new IllegalStateException("there should always be a user when sending a message for action [" + action + "]"); } + assert securityContext.getParentAuthorization() == null || remoteClusterAliasResolver.apply(connection).isPresent() == false + : "parent authorization header should not be set for remote cluster requests"; + try { sender.sendRequest(connection, action, request, options, handler); } catch (Exception e) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityContextTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityContextTests.java index 9109135e469d..023e80469723 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityContextTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityContextTests.java @@ -23,12 +23,17 @@ import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.ParentActionAuthorization; +import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; +import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.AsyncSearchUser; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; import org.elasticsearch.xpack.core.security.user.XPackUser; import org.junit.Before; +import org.mockito.Mockito; import java.io.EOFException; import java.io.IOException; @@ -41,6 +46,8 @@ import static org.elasticsearch.xpack.core.security.authc.Authentication.VERSION_API_KEY_ROLES_AS_BYTES; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; public class SecurityContextTests extends ESTestCase { @@ -249,4 +256,82 @@ public void testExecuteAfterRewritingAuthenticationWillConditionallyRewriteOldAp }); }, VersionUtils.randomVersionBetween(random(), VERSION_API_KEY_ROLES_AS_BYTES, Version.CURRENT)); } + + public void testExecuteAfterRemovingParentAuthorization() { + final Map requestHeaders = Map.ofEntries( + Map.entry(AuthenticationField.PRIVILEGE_CATEGORY_KEY, randomAlphaOfLengthBetween(3, 10)), + Map.entry(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)), + Map.entry(Task.X_OPAQUE_ID_HTTP_HEADER, randomAlphaOfLength(10)), + Map.entry(Task.TRACE_ID, randomAlphaOfLength(20)) + ); + threadContext.putHeader(requestHeaders); + + final Map transientHeaders = Map.ofEntries( + Map.entry(AuthorizationServiceField.AUTHORIZATION_INFO_KEY, Mockito.mock(AuthorizationInfo.class)), + Map.entry( + AuthenticationField.AUTHENTICATION_KEY, + Authentication.newAnonymousAuthentication(new AnonymousUser(Settings.EMPTY), "test-node") + ), + Map.entry(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)), + Map.entry("_some_map", Map.of(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8))), + Map.entry("_remote_address", "125.124.123.122"), + Map.entry(Task.APM_TRACE_CONTEXT, new Object()) + ); + transientHeaders.forEach((k, v) -> threadContext.putTransient(k, v)); + + final Map responseHeaders = Map.ofEntries( + Map.entry(randomAlphaOfLengthBetween(3, 6), randomAlphaOfLengthBetween(3, 8)), + Map.entry("_response_message", "All good."), + Map.entry("Warning", "Some warning!") + ); + responseHeaders.forEach((k, v) -> threadContext.addResponseHeader(k, v)); + + // mark as system context + boolean setSystemContext = randomBoolean(); + if (setSystemContext) { + threadContext.markAsSystemContext(); + } + + final ParentActionAuthorization parentAuthorization = new ParentActionAuthorization("indices:data/read/search"); + securityContext.setParentAuthorization(parentAuthorization); + + securityContext.executeAfterRemovingParentAuthorization(original -> { + // parent authorization header should be removed within execute method + assertThat(securityContext.getParentAuthorization(), nullValue()); + // system context boolean should be preserved + assertThat(threadContext.isSystemContext(), equalTo(setSystemContext)); + // other request and transient headers should be preserved + assertThat(threadContext.getHeaders().size(), equalTo(requestHeaders.size())); + assertThat(threadContext.getResponseHeaders().size(), equalTo(responseHeaders.size())); + assertThat(threadContext.getTransientHeaders().size(), equalTo(transientHeaders.size())); + requestHeaders.forEach((k, v) -> assertThat(threadContext.getHeader(k), equalTo(v))); + transientHeaders.forEach((k, v) -> assertThat(threadContext.getTransient(k), equalTo(v))); + responseHeaders.forEach((k, v) -> assertThat(threadContext.getResponseHeaders().get(k).get(0), equalTo(v))); + // warning header count is still equal to 1 + assertThat(threadContext.getResponseHeaders().get("Warning").size(), equalTo(1)); + // add new headers + threadContext.addResponseHeader("_new_response_header", randomAlphaOfLengthBetween(3, 8)); + threadContext.putTransient("_new_transient_header", randomAlphaOfLengthBetween(3, 8)); + threadContext.putHeader("_new_request_header", randomAlphaOfLengthBetween(3, 8)); + threadContext.addResponseHeader("Warning", randomAlphaOfLengthBetween(3, 8)); + // warning header is now equal to 2 + assertThat(threadContext.getResponseHeaders().get("Warning").size(), equalTo(2)); + }); + + // parent authorization should be restored after execution + assertThat(securityContext.getParentAuthorization(), equalTo(parentAuthorization)); + // system context boolean is unchanged + assertThat(threadContext.isSystemContext(), equalTo(setSystemContext)); + // other request and transient headers should still be there + assertThat(threadContext.getTransientHeaders().size(), equalTo(transientHeaders.size())); + requestHeaders.forEach((k, v) -> assertThat(threadContext.getHeader(k), equalTo(v))); + transientHeaders.forEach((k, v) -> assertThat(threadContext.getTransient(k), equalTo(v))); + responseHeaders.forEach((k, v) -> assertThat(threadContext.getResponseHeaders().get(k).get(0), equalTo(v))); + // newly added transient and request headers should be removed + assertThat(threadContext.getTransient("_new_transient_header"), nullValue()); + assertThat(threadContext.getHeader("_new_request_header"), nullValue()); + // response headers should be preserved and retain newly added ones + assertThat(threadContext.getResponseHeaders().get("_new_response_header"), notNullValue()); + assertThat(threadContext.getResponseHeaders().get("Warning").size(), equalTo(2)); + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index ae9a52a03dfe..7ef078d6ddf8 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -240,6 +240,7 @@ public class AuthorizationServiceTests extends ESTestCase { private OperatorPrivileges.OperatorPrivilegesService operatorPrivilegesService; private boolean shouldFailOperatorPrivilegesCheck = false; private boolean setFakeOriginatingAction = true; + private SecurityContext securityContext; @SuppressWarnings("unchecked") @Before @@ -258,6 +259,7 @@ public void setup() { when(licenseState.isAllowed(Security.AUDITING_FEATURE)).thenReturn(true); auditTrailService = new AuditTrailService(Collections.singletonList(auditTrail), licenseState); threadContext = new ThreadContext(settings); + securityContext = new SecurityContext(settings, threadContext); threadPool = mock(ThreadPool.class); when(threadPool.getThreadContext()).thenReturn(threadContext); final FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(settings); @@ -364,7 +366,6 @@ private void authorize( String someRandomHeader = "test_" + UUIDs.randomBase64UUID(); Object someRandomHeaderValue = mock(Object.class); threadContext.putTransient(someRandomHeader, someRandomHeaderValue); - SecurityContext securityContext = new SecurityContext(Settings.EMPTY, threadContext); // the thread context before authorization could contain any of the transient headers IndicesAccessControl mockAccessControlHeader = threadContext.getTransient(INDICES_PERMISSIONS_KEY); if (mockAccessControlHeader == null && randomBoolean()) { @@ -1168,12 +1169,18 @@ public void testSearchAgainstIndex() { authorize(authentication, SearchAction.NAME, searchRequest, true, () -> { verify(rolesStore).getRoles(Mockito.same(authentication), Mockito.any()); IndicesAccessControl iac = threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY); + // Successful search action authorization should set a parent authorization header. + assertThat(securityContext.getParentAuthorization().action(), equalTo(SearchAction.NAME)); // Within the action handler, execute a child action (the query phase of search) authorize(authentication, SearchTransportService.QUERY_ACTION_NAME, shardRequest, false, () -> { // This child action triggers a second interaction with the role store (which is cached) verify(rolesStore, times(2)).getRoles(Mockito.same(authentication), Mockito.any()); // But it does not create a new IndicesAccessControl assertThat(threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY), sameInstance(iac)); + // The parent authorization header should only be present for direct child actions + // and not be carried over for a child of a child actions. + // Meaning, only query phase action should be pre-authorized in this case and potential sub-actions should not. + assertThat(securityContext.getParentAuthorization(), nullValue()); }); }); verify(auditTrail).accessGranted( diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/PreAuthorizationUtilsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/PreAuthorizationUtilsTests.java new file mode 100644 index 000000000000..866626e7d01f --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/PreAuthorizationUtilsTests.java @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authz; + +import org.elasticsearch.action.search.SearchAction; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.SecurityContext; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationContext; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.ParentActionAuthorization; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.RequestInfo; +import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; +import org.elasticsearch.xpack.core.security.authz.permission.Role; +import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.authz.RBACEngine.RBACAuthorizationInfo; + +import java.util.Optional; + +import static org.elasticsearch.xpack.core.security.test.TestRestrictedIndices.RESTRICTED_INDICES; +import static org.elasticsearch.xpack.security.authz.PreAuthorizationUtils.maybeSkipChildrenActionAuthorization; +import static org.elasticsearch.xpack.security.authz.PreAuthorizationUtils.shouldRemoveParentAuthorizationFromThreadContext; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +/** + * Unit tests for {@link PreAuthorizationUtils}. + */ +public class PreAuthorizationUtilsTests extends ESTestCase { + + public void testMaybeSkipChildrenActionAuthorizationAddsParentAuthorizationHeader() { + String action = SearchAction.NAME; + + Role role = Role.builder(RESTRICTED_INDICES, "test-role").add(IndexPrivilege.READ, "test-*").build(); + + AuthorizationContext parentAuthorizationContext = createAuthorizationContext(action, role, IndicesAccessControl.allowAll()); + SecurityContext securityContext = new SecurityContext(Settings.EMPTY, new ThreadContext(Settings.EMPTY)); + + maybeSkipChildrenActionAuthorization(securityContext, parentAuthorizationContext); + assertThat(securityContext.getParentAuthorization(), notNullValue()); + assertThat(securityContext.getParentAuthorization().action(), equalTo(action)); + } + + public void testMaybeSkipChildrenActionAuthorizationDoesNotAddHeaderForRandomAction() { + String action = "indices:data/" + randomAlphaOfLengthBetween(3, 8); + + Role role = Role.builder(RESTRICTED_INDICES, "test-role").add(IndexPrivilege.READ, "test-*").build(); + + AuthorizationContext parentAuthorizationContext = createAuthorizationContext(action, role, IndicesAccessControl.allowAll()); + SecurityContext securityContext = new SecurityContext(Settings.EMPTY, new ThreadContext(Settings.EMPTY)); + + maybeSkipChildrenActionAuthorization(securityContext, parentAuthorizationContext); + assertThat(securityContext.getParentAuthorization(), nullValue()); + } + + public void testShouldRemoveParentAuthorizationFromThreadContext() { + final String parentAction = SearchAction.NAME; + SecurityContext securityContextWithParentAuthorization = new SecurityContext(Settings.EMPTY, new ThreadContext(Settings.EMPTY)); + securityContextWithParentAuthorization.setParentAuthorization(new ParentActionAuthorization(parentAction)); + + // We should not remove the parent authorization when child action is white-listed + assertThat( + shouldRemoveParentAuthorizationFromThreadContext( + Optional.empty(), + randomWhitelistedChildAction(parentAction), + securityContextWithParentAuthorization + ), + equalTo(false) + ); + + // We should not remove when there is nothing to be removed + assertThat( + shouldRemoveParentAuthorizationFromThreadContext( + Optional.ofNullable(randomBoolean() ? "my_remote_cluster" : null), + randomWhitelistedChildAction(parentAction), + new SecurityContext(Settings.EMPTY, new ThreadContext(Settings.EMPTY)) + ), + equalTo(false) + ); + + // Even-though the child action is white-listed for the parent action, + // we expect to remove parent authorization when targeting remote cluster + assertThat( + shouldRemoveParentAuthorizationFromThreadContext( + Optional.of("my_remote_cluster"), + randomWhitelistedChildAction(parentAction), + securityContextWithParentAuthorization + ), + equalTo(true) + ); + + // The parent authorization should be removed in either case: + // - we are sending a transport request to a remote cluster + // - or the child action is not white-listed for the parent + assertThat( + shouldRemoveParentAuthorizationFromThreadContext( + Optional.ofNullable(randomBoolean() ? "my_remote_cluster" : null), + randomAlphaOfLengthBetween(3, 8), + securityContextWithParentAuthorization + ), + equalTo(true) + ); + } + + public void testShouldPreAuthorizeChildByParentAction() { + final String parentAction = SearchAction.NAME; + final String childAction = randomWhitelistedChildAction(parentAction); + + ParentActionAuthorization parentAuthorization = new ParentActionAuthorization(parentAction); + Authentication authentication = Authentication.newRealmAuthentication( + new User("username1", "role1"), + new RealmRef("realm1", "native", "node1") + ); + RequestInfo requestInfo = new RequestInfo(authentication, new SearchRequest("test-index"), childAction, null, parentAuthorization); + + Role role = Role.builder(RESTRICTED_INDICES, "role1").add(IndexPrivilege.READ, "test-*").build(); + RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); + + assertThat(PreAuthorizationUtils.shouldPreAuthorizeChildByParentAction(requestInfo, authzInfo), equalTo(true)); + } + + public void testShouldPreAuthorizeChildByParentActionWhenParentAndChildAreSame() { + final String parentAction = SearchAction.NAME; + final String childAction = parentAction; + + ParentActionAuthorization parentAuthorization = new ParentActionAuthorization(parentAction); + Authentication authentication = Authentication.newRealmAuthentication( + new User("username1", "role1"), + new RealmRef("realm1", "native", "node1") + ); + RequestInfo requestInfo = new RequestInfo(authentication, new SearchRequest("test-index"), childAction, null, parentAuthorization); + + Role role = Role.builder(RESTRICTED_INDICES, "role1").add(IndexPrivilege.READ, "test-*").build(); + RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); + + assertThat(PreAuthorizationUtils.shouldPreAuthorizeChildByParentAction(requestInfo, authzInfo), equalTo(false)); + } + + private String randomWhitelistedChildAction(String parentAction) { + return randomFrom(PreAuthorizationUtils.CHILD_ACTIONS_PRE_AUTHORIZED_BY_PARENT.get(parentAction)); + } + + private AuthorizationContext createAuthorizationContext(String action, Role role, IndicesAccessControl accessControl) { + RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); + return new AuthorizationContext(action, authzInfo, accessControl); + } + +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index 2b99d6918549..a60abd39441d 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.security.authz; +import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction; import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; @@ -57,11 +58,15 @@ import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings; import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; -import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AsyncSupplier; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizedIndices; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.IndexAuthorizationResult; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.ParentActionAuthorization; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.PrivilegesCheckResult; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.PrivilegesToCheck; +import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.RequestInfo; +import org.elasticsearch.xpack.core.security.authz.ResolvedIndices; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.ApplicationResourcePrivileges; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges; @@ -88,6 +93,7 @@ import org.elasticsearch.xpack.security.authz.RBACEngine.RBACAuthorizationInfo; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import org.hamcrest.Matchers; +import org.junit.Assert; import org.junit.Before; import org.mockito.Mockito; @@ -98,13 +104,18 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; +import java.util.stream.Stream; import static java.util.Collections.emptyMap; +import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_INDEX_VERSION_CREATED; +import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_NUMBER_OF_REPLICAS; +import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_NUMBER_OF_SHARDS; import static org.elasticsearch.common.util.set.Sets.newHashSet; import static org.elasticsearch.test.ActionListenerUtils.anyActionListener; import static org.elasticsearch.xpack.core.security.test.TestRestrictedIndices.RESTRICTED_INDICES; @@ -125,6 +136,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -158,7 +170,7 @@ public void testResolveAuthorizationInfoForEmptyRolesWithAuthentication() { final PlainActionFuture future = new PlainActionFuture<>(); engine.resolveAuthorizationInfo( - new AuthorizationEngine.RequestInfo( + new RequestInfo( AuthenticationTestHelper.builder().build(), mock(TransportRequest.class), randomAlphaOfLengthBetween(20, 30), @@ -1779,6 +1791,155 @@ public void testGetRemoteAccessRoleDescriptorsIntersectionWithoutRemoteIndicesPe assertThat(actual, equalTo(RoleDescriptorsIntersection.EMPTY)); } + public void testChildSearchActionAuthorizationIsSkipped() { + final String[] indices = { "test-index" }; + final Role role = Mockito.spy(Role.builder(RESTRICTED_INDICES, "test-role").add(IndexPrivilege.READ, indices).build()); + + final String action = randomFrom(PreAuthorizationUtils.CHILD_ACTIONS_PRE_AUTHORIZED_BY_PARENT.get(SearchAction.NAME)); + final ParentActionAuthorization parentAuthorization = new ParentActionAuthorization(SearchAction.NAME); + + authorizeIndicesAction(indices, role, action, parentAuthorization, new ActionListener() { + @Override + public void onResponse(IndexAuthorizationResult indexAuthorizationResult) { + assertTrue(indexAuthorizationResult.isGranted()); + // Child authorization should be skipped since we passed parent authorization. + Mockito.verify(role, never()).checkIndicesAction(action); + Mockito.verify(role, never()).authorize(eq(action), any(), any(), any()); + } + + @Override + public void onFailure(Exception e) { + Assert.fail(e.getMessage()); + } + }); + } + + public void testChildSearchActionIsAuthorizedWithoutSkipping() { + final String[] indices = { "test-index" }; + final Role role = Mockito.spy(Role.builder(RESTRICTED_INDICES, "test-role").add(IndexPrivilege.READ, indices).build()); + + final String action = randomFrom(PreAuthorizationUtils.CHILD_ACTIONS_PRE_AUTHORIZED_BY_PARENT.get(SearchAction.NAME)); + final ParentActionAuthorization parentAuthorization = null; + + authorizeIndicesAction(indices, role, action, parentAuthorization, new ActionListener() { + @Override + public void onResponse(IndexAuthorizationResult indexAuthorizationResult) { + assertTrue(indexAuthorizationResult.isGranted()); + // Child action should have been authorized normally since we did not pass parent authorization + Mockito.verify(role, atLeastOnce()).authorize(eq(action), any(), any(), any()); + } + + @Override + public void onFailure(Exception e) { + Assert.fail(e.getMessage()); + } + }); + } + + public void testChildSearchActionAuthorizationIsNotSkippedWhenRoleHasDLS() { + final String[] indices = { "test-index" }; + final BytesArray query = new BytesArray(""" + {"term":{"foo":bar}}"""); + final Role role = Mockito.spy( + Role.builder(RESTRICTED_INDICES, "test-role") + .add( + new FieldPermissions(new FieldPermissionsDefinition(new String[] { "foo" }, new String[0])), + Set.of(query), + IndexPrivilege.READ, + randomBoolean(), + indices + ) + .build() + ); + + final String action = randomFrom(PreAuthorizationUtils.CHILD_ACTIONS_PRE_AUTHORIZED_BY_PARENT.get(SearchAction.NAME)); + final ParentActionAuthorization parentAuthorization = new ParentActionAuthorization(SearchAction.NAME); + + authorizeIndicesAction(indices, role, action, parentAuthorization, new ActionListener() { + @Override + public void onResponse(IndexAuthorizationResult indexAuthorizationResult) { + assertTrue(indexAuthorizationResult.isGranted()); + // Child action authorization should not be skipped, even though the parent authorization was present + Mockito.verify(role, atLeastOnce()).authorize(eq(action), any(), any(), any()); + } + + @Override + public void onFailure(Exception e) { + Assert.fail(e.getMessage()); + } + }); + } + + public void testRandomChildSearchActionAuthorizionIsNotSkipped() { + final String[] indices = { "test-index" }; + final Role role = Mockito.spy(Role.builder(RESTRICTED_INDICES, "test-role").add(IndexPrivilege.READ, indices).build()); + + final String action = SearchAction.NAME + "[" + randomAlphaOfLength(3) + "]"; + final ParentActionAuthorization parentAuthorization = new ParentActionAuthorization(SearchAction.NAME); + + authorizeIndicesAction(indices, role, action, parentAuthorization, new ActionListener() { + @Override + public void onResponse(IndexAuthorizationResult indexAuthorizationResult) { + assertTrue(indexAuthorizationResult.isGranted()); + Mockito.verify(role, atLeastOnce()).authorize(eq(action), any(), any(), any()); + } + + @Override + public void onFailure(Exception e) { + Assert.fail(e.getMessage()); + } + }); + } + + private void authorizeIndicesAction( + final String[] indices, + final Role role, + final String action, + final ParentActionAuthorization parentAuthorization, + final ActionListener listener + ) { + + final RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null); + final ResolvedIndices resolvedIndices = new ResolvedIndices(List.of(indices), List.of()); + final TransportRequest searchRequest = new SearchRequest(indices); + final RequestInfo requestInfo = createRequestInfo(searchRequest, action, parentAuthorization); + final AsyncSupplier indicesAsyncSupplier = s -> s.onResponse(resolvedIndices); + + final Map aliasOrIndexLookup = Stream.of(indices) + .collect( + Collectors.toMap( + i -> i, + v -> new IndexAbstraction.ConcreteIndex( + IndexMetadata.builder(v) + .settings( + Settings.builder() + .put(SETTING_NUMBER_OF_SHARDS, 1) + .put(SETTING_NUMBER_OF_REPLICAS, 0) + .put(SETTING_INDEX_VERSION_CREATED.getKey(), Version.CURRENT) + ) + .build() + ) + ) + ); + + engine.authorizeIndexAction(requestInfo, authzInfo, indicesAsyncSupplier, aliasOrIndexLookup, listener); + } + + private static RequestInfo createRequestInfo(TransportRequest request, String action, ParentActionAuthorization parentAuthorization) { + final Authentication.RealmRef realm = new Authentication.RealmRef( + randomAlphaOfLength(6), + randomAlphaOfLength(4), + "node0" + randomIntBetween(1, 9) + ); + return new RequestInfo( + AuthenticationTestHelper.builder().user(new User(randomAlphaOfLength(8))).realmRef(realm).build(false), + request, + action, + null, + parentAuthorization + ); + } + private GetUserPrivilegesResponse.Indices findIndexPrivilege(Set indices, String name) { return indices.stream().filter(i -> i.getIndices().contains(name)).findFirst().get(); } From 2d2b82bd7c58f60ee18eabd7ae2739a02083a807 Mon Sep 17 00:00:00 2001 From: Joe Gallo Date: Mon, 19 Dec 2022 10:42:43 -0500 Subject: [PATCH 307/919] Add CountDownActionListener (#92308) --- .../support/CountDownActionListener.java | 82 ++++++++ .../cluster/NodeConnectionsService.java | 12 +- .../allocation/DiskThresholdMonitor.java | 4 +- .../blobstore/BlobStoreRepository.java | 9 +- .../snapshots/RestoreService.java | 10 +- .../tasks/TaskCancellationService.java | 15 +- .../transport/RemoteClusterService.java | 6 +- .../support/CountDownActionListenerTests.java | 190 ++++++++++++++++++ .../ShardSnapshotTaskRunnerTests.java | 7 +- .../xpack/slm/SnapshotRetentionTask.java | 8 +- .../store/SearchableSnapshotDirectory.java | 14 +- 11 files changed, 311 insertions(+), 46 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/action/support/CountDownActionListener.java create mode 100644 server/src/test/java/org/elasticsearch/action/support/CountDownActionListenerTests.java diff --git a/server/src/main/java/org/elasticsearch/action/support/CountDownActionListener.java b/server/src/main/java/org/elasticsearch/action/support/CountDownActionListener.java new file mode 100644 index 000000000000..e9da843d34c2 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/action/support/CountDownActionListener.java @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +package org.elasticsearch.action.support; + +import org.elasticsearch.action.ActionListener; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Wraps another listener and adds a counter -- each invocation of this listener will decrement the counter, and when the counter has been + * exhausted the final invocation of this listener will delegate to the wrapped listener. Similar to {@link GroupedActionListener}, but for + * the cases where tracking individual results is not useful. + */ +public final class CountDownActionListener extends ActionListener.Delegating { + + private final AtomicInteger countDown; + private final AtomicReference failure = new AtomicReference<>(); + + /** + * Creates a new listener + * @param groupSize the group size + * @param delegate the delegate listener + */ + public CountDownActionListener(int groupSize, ActionListener delegate) { + super(Objects.requireNonNull(delegate)); + if (groupSize <= 0) { + assert false : "illegal group size [" + groupSize + "]"; + throw new IllegalArgumentException("groupSize must be greater than 0 but was " + groupSize); + } + countDown = new AtomicInteger(groupSize); + } + + /** + * Creates a new listener + * @param groupSize the group size + * @param runnable the runnable + */ + public CountDownActionListener(int groupSize, Runnable runnable) { + this(groupSize, ActionListener.wrap(Objects.requireNonNull(runnable))); + } + + private boolean countDown() { + final var result = countDown.getAndUpdate(current -> Math.max(0, current - 1)); + assert result > 0; + return result == 1; + } + + @Override + public void onResponse(Void element) { + if (countDown()) { + if (failure.get() != null) { + super.onFailure(failure.get()); + } else { + delegate.onResponse(element); + } + } + } + + @Override + public void onFailure(Exception e) { + if (failure.compareAndSet(null, e) == false) { + failure.accumulateAndGet(e, (current, update) -> { + // we have to avoid self-suppression! + if (update != current) { + current.addSuppressed(update); + } + return current; + }); + } + if (countDown()) { + super.onFailure(failure.get()); + } + } + +} diff --git a/server/src/main/java/org/elasticsearch/cluster/NodeConnectionsService.java b/server/src/main/java/org/elasticsearch/cluster/NodeConnectionsService.java index 077771dbcc76..2e67288358c2 100644 --- a/server/src/main/java/org/elasticsearch/cluster/NodeConnectionsService.java +++ b/server/src/main/java/org/elasticsearch/cluster/NodeConnectionsService.java @@ -11,7 +11,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.support.GroupedActionListener; +import org.elasticsearch.action.support.CountDownActionListener; import org.elasticsearch.cluster.coordination.FollowersChecker; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodes; @@ -97,10 +97,7 @@ public void connectToNodes(DiscoveryNodes discoveryNodes, Runnable onCompletion) return; } - final GroupedActionListener listener = new GroupedActionListener<>( - discoveryNodes.getSize(), - ActionListener.wrap(onCompletion) - ); + final CountDownActionListener listener = new CountDownActionListener(discoveryNodes.getSize(), onCompletion); final List runnables = new ArrayList<>(discoveryNodes.getSize()); synchronized (mutex) { @@ -159,10 +156,7 @@ void ensureConnections(Runnable onCompletion) { runnables.add(onCompletion); } else { logger.trace("ensureConnections: {}", targetsByNode); - final GroupedActionListener listener = new GroupedActionListener<>( - connectionTargets.size(), - ActionListener.wrap(onCompletion) - ); + final CountDownActionListener listener = new CountDownActionListener(connectionTargets.size(), onCompletion); for (final ConnectionTarget connectionTarget : connectionTargets) { runnables.add(connectionTarget.connect(listener)); } diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/DiskThresholdMonitor.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/DiskThresholdMonitor.java index 60598d62df01..82413fe3723a 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/DiskThresholdMonitor.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/DiskThresholdMonitor.java @@ -11,7 +11,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.support.GroupedActionListener; +import org.elasticsearch.action.support.CountDownActionListener; import org.elasticsearch.client.internal.Client; import org.elasticsearch.cluster.ClusterInfo; import org.elasticsearch.cluster.ClusterState; @@ -301,7 +301,7 @@ public void onNewInfo(ClusterInfo info) { } } - final ActionListener listener = new GroupedActionListener<>(3, ActionListener.wrap(this::checkFinished)); + final ActionListener listener = new CountDownActionListener(3, this::checkFinished); if (reroute) { logger.debug("rerouting shards: [{}]", explanation); diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java index 3c536328522c..079a21f341e2 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -27,6 +27,7 @@ import org.elasticsearch.action.ActionRunnable; import org.elasticsearch.action.SingleResultDeduplicator; import org.elasticsearch.action.StepListener; +import org.elasticsearch.action.support.CountDownActionListener; import org.elasticsearch.action.support.GroupedActionListener; import org.elasticsearch.action.support.ListenableActionFuture; import org.elasticsearch.action.support.PlainActionFuture; @@ -964,7 +965,7 @@ private void doDeleteShardSnapshots( writeUpdatedRepoDataStep.whenComplete(updatedRepoData -> { listener.onRepositoryDataWritten(updatedRepoData); // Run unreferenced blobs cleanup in parallel to shard-level snapshot deletion - final ActionListener afterCleanupsListener = new GroupedActionListener<>(2, ActionListener.wrap(listener::onDone)); + final ActionListener afterCleanupsListener = new CountDownActionListener(2, listener::onDone); cleanupUnlinkedRootAndIndicesBlobs(snapshotIds, foundIndices, rootBlobs, updatedRepoData, afterCleanupsListener); asyncCleanupUnlinkedShardLevelBlobs( repositoryData, @@ -978,10 +979,10 @@ private void doDeleteShardSnapshots( final RepositoryData updatedRepoData = repositoryData.removeSnapshots(snapshotIds, ShardGenerations.EMPTY); writeIndexGen(updatedRepoData, repositoryStateId, repoMetaVersion, Function.identity(), ActionListener.wrap(newRepoData -> { // Run unreferenced blobs cleanup in parallel to shard-level snapshot deletion - final ActionListener afterCleanupsListener = new GroupedActionListener<>(2, ActionListener.wrap(() -> { + final ActionListener afterCleanupsListener = new CountDownActionListener(2, () -> { listener.onRepositoryDataWritten(newRepoData); listener.onDone(); - })); + }); cleanupUnlinkedRootAndIndicesBlobs(snapshotIds, foundIndices, rootBlobs, newRepoData, afterCleanupsListener); final StepListener> writeMetaAndComputeDeletesStep = new StepListener<>(); writeUpdatedShardMetaDataAndComputeDeletes(snapshotIds, repositoryData, false, writeMetaAndComputeDeletesStep); @@ -1414,7 +1415,7 @@ public void finalizeSnapshot(final FinalizeSnapshotContext finalizeSnapshotConte indexMetaIdentifiers = null; } - final ActionListener allMetaListener = new GroupedActionListener<>(2 + indices.size(), ActionListener.wrap(v -> { + final ActionListener allMetaListener = new CountDownActionListener(2 + indices.size(), ActionListener.wrap(v -> { final String slmPolicy = slmPolicy(snapshotInfo); final SnapshotDetails snapshotDetails = new SnapshotDetails( snapshotInfo.state(), diff --git a/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java b/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java index 408163d4acae..10ecef65c5e6 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java @@ -14,7 +14,7 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.StepListener; import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; -import org.elasticsearch.action.support.GroupedActionListener; +import org.elasticsearch.action.support.CountDownActionListener; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.cluster.ClusterChangedEvent; import org.elasticsearch.cluster.ClusterState; @@ -525,11 +525,11 @@ static void refreshRepositoryUuids(boolean enabled, RepositoriesService reposito "refreshing repository UUIDs for repositories [{}]", repositories.stream().map(repository -> repository.getMetadata().name()).collect(Collectors.joining(",")) ); - final ActionListener groupListener = new GroupedActionListener<>( + final ActionListener countDownListener = new CountDownActionListener( repositories.size(), - new ActionListener>() { + new ActionListener() { @Override - public void onResponse(Collection ignored) { + public void onResponse(Void ignored) { logger.debug("repository UUID refresh completed"); refreshListener.onResponse(null); } @@ -543,7 +543,7 @@ public void onFailure(Exception e) { ).map(repositoryData -> null /* don't collect the RepositoryData */); for (Repository repository : repositories) { - repository.getRepositoryData(groupListener); + repository.getRepositoryData(countDownListener); } } diff --git a/server/src/main/java/org/elasticsearch/tasks/TaskCancellationService.java b/server/src/main/java/org/elasticsearch/tasks/TaskCancellationService.java index 788ae17f2bfb..96e02bfa4f50 100644 --- a/server/src/main/java/org/elasticsearch/tasks/TaskCancellationService.java +++ b/server/src/main/java/org/elasticsearch/tasks/TaskCancellationService.java @@ -17,6 +17,7 @@ import org.elasticsearch.action.ResultDeduplicator; import org.elasticsearch.action.StepListener; import org.elasticsearch.action.support.ChannelActionListener; +import org.elasticsearch.action.support.CountDownActionListener; import org.elasticsearch.action.support.GroupedActionListener; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -99,18 +100,18 @@ void doCancelTaskAndDescendants(CancellableTask task, String reason, boolean wai if (task.shouldCancelChildrenOnCancellation()) { logger.trace("cancelling task [{}] and its descendants", taskId); StepListener completedListener = new StepListener<>(); - GroupedActionListener groupedListener = new GroupedActionListener<>(3, completedListener.map(r -> null)); + CountDownActionListener countDownListener = new CountDownActionListener(3, completedListener); Collection childConnections = taskManager.startBanOnChildTasks(task.getId(), reason, () -> { logger.trace("child tasks of parent [{}] are completed", taskId); - groupedListener.onResponse(null); + countDownListener.onResponse(null); }); taskManager.cancel(task, reason, () -> { logger.trace("task [{}] is cancelled", taskId); - groupedListener.onResponse(null); + countDownListener.onResponse(null); }); StepListener setBanListener = new StepListener<>(); setBanOnChildConnections(reason, waitForCompletion, task, childConnections, setBanListener); - setBanListener.addListener(groupedListener); + setBanListener.addListener(countDownListener); // If we start unbanning when the last child task completed and that child task executed with a specific user, then unban // requests are denied because internal requests can't run with a user. We need to remove bans with the current thread context. final Runnable removeBansRunnable = transportService.getThreadPool() @@ -149,7 +150,7 @@ private void setBanOnChildConnections( } final TaskId taskId = new TaskId(localNodeId(), task.getId()); logger.trace("cancelling child tasks of [{}] on child connections {}", taskId, childConnections); - GroupedActionListener groupedListener = new GroupedActionListener<>(childConnections.size(), listener.map(r -> null)); + CountDownActionListener countDownListener = new CountDownActionListener(childConnections.size(), listener); final BanParentTaskRequest banRequest = BanParentTaskRequest.createSetBanParentTaskRequest(taskId, reason, waitForCompletion); for (Transport.Connection connection : childConnections) { assert TransportService.unwrapConnection(connection) == connection : "Child connection must be unwrapped"; @@ -162,7 +163,7 @@ private void setBanOnChildConnections( @Override public void handleResponse(TransportResponse.Empty response) { logger.trace("sent ban for tasks with the parent [{}] for connection [{}]", taskId, connection); - groupedListener.onResponse(null); + countDownListener.onResponse(null); } @Override @@ -188,7 +189,7 @@ public void handleException(TransportException exp) { ); } - groupedListener.onFailure(exp); + countDownListener.onFailure(exp); } } ); diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java index 696028f9dc5b..bf380df5bbf7 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java @@ -12,7 +12,7 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.OriginalIndices; -import org.elasticsearch.action.support.GroupedActionListener; +import org.elasticsearch.action.support.CountDownActionListener; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.client.internal.Client; @@ -332,14 +332,14 @@ synchronized void updateRemoteCluster(String clusterAlias, Settings newSettings, */ void initializeRemoteClusters() { final TimeValue timeValue = REMOTE_INITIAL_CONNECTION_TIMEOUT_SETTING.get(settings); - final PlainActionFuture> future = new PlainActionFuture<>(); + final PlainActionFuture future = new PlainActionFuture<>(); Set enabledClusters = RemoteClusterAware.getEnabledRemoteClusters(settings); if (enabledClusters.isEmpty()) { return; } - GroupedActionListener listener = new GroupedActionListener<>(enabledClusters.size(), future); + CountDownActionListener listener = new CountDownActionListener(enabledClusters.size(), future); for (String clusterAlias : enabledClusters) { updateRemoteCluster(clusterAlias, settings, listener); } diff --git a/server/src/test/java/org/elasticsearch/action/support/CountDownActionListenerTests.java b/server/src/test/java/org/elasticsearch/action/support/CountDownActionListenerTests.java new file mode 100644 index 000000000000..7655c2fd172f --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/support/CountDownActionListenerTests.java @@ -0,0 +1,190 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +package org.elasticsearch.action.support; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; + +public class CountDownActionListenerTests extends ESTestCase { + + public void testNotifications() throws InterruptedException { + AtomicBoolean called = new AtomicBoolean(false); + ActionListener result = new ActionListener<>() { + @Override + public void onResponse(Void ignored) { + called.set(true); + } + + @Override + public void onFailure(Exception e) { + throw new AssertionError(e); + } + }; + final int groupSize = randomIntBetween(10, 1000); + AtomicInteger count = new AtomicInteger(); + CountDownActionListener listener = new CountDownActionListener(groupSize, result); + int numThreads = randomIntBetween(2, 5); + Thread[] threads = new Thread[numThreads]; + CyclicBarrier barrier = new CyclicBarrier(numThreads); + for (int i = 0; i < numThreads; i++) { + threads[i] = new Thread(() -> { + try { + barrier.await(10, TimeUnit.SECONDS); + } catch (Exception e) { + throw new AssertionError(e); + } + while (count.incrementAndGet() <= groupSize) { + listener.onResponse(null); + } + }); + threads[i].start(); + } + for (Thread t : threads) { + t.join(); + } + assertTrue(called.get()); + } + + public void testFailed() { + AtomicBoolean called = new AtomicBoolean(false); + AtomicReference excRef = new AtomicReference<>(); + + ActionListener result = new ActionListener<>() { + @Override + public void onResponse(Void ignored) { + called.set(true); + } + + @Override + public void onFailure(Exception e) { + excRef.set(e); + } + }; + int size = randomIntBetween(3, 4); + CountDownActionListener listener = new CountDownActionListener(size, result); + listener.onResponse(null); + IOException ioException = new IOException(); + RuntimeException rtException = new RuntimeException(); + listener.onFailure(rtException); + listener.onFailure(ioException); + if (size == 4) { + listener.onResponse(null); + } + assertNotNull(excRef.get()); + assertEquals(rtException, excRef.get()); + assertEquals(1, excRef.get().getSuppressed().length); + assertEquals(ioException, excRef.get().getSuppressed()[0]); + assertFalse(called.get()); + } + + public void testValidation() throws InterruptedException { + AtomicBoolean called = new AtomicBoolean(false); + ActionListener result = new ActionListener<>() { + @Override + public void onResponse(Void ignored) { + called.compareAndSet(false, true); + } + + @Override + public void onFailure(Exception e) { + called.compareAndSet(false, true); + } + }; + + // can't use a groupSize of 0 + expectThrows(AssertionError.class, () -> new CountDownActionListener(0, result)); + + // can't use a null listener or runnable + expectThrows(NullPointerException.class, () -> new CountDownActionListener(1, (ActionListener) null)); + expectThrows(NullPointerException.class, () -> new CountDownActionListener(1, (Runnable) null)); + + final int overage = randomIntBetween(1, 10); + AtomicInteger assertionsTriggered = new AtomicInteger(); + final int groupSize = randomIntBetween(10, 1000); + AtomicInteger count = new AtomicInteger(); + CountDownActionListener listener = new CountDownActionListener(groupSize, result); + int numThreads = randomIntBetween(2, 5); + Thread[] threads = new Thread[numThreads]; + CyclicBarrier barrier = new CyclicBarrier(numThreads); + for (int i = 0; i < numThreads; i++) { + threads[i] = new Thread(() -> { + try { + barrier.await(10, TimeUnit.SECONDS); + } catch (Exception e) { + throw new AssertionError(e); + } + int c; + while ((c = count.incrementAndGet()) <= groupSize + overage) { + try { + if (c % 10 == 1) { // a mix of failures and non-failures + listener.onFailure(new RuntimeException()); + } else { + listener.onResponse(null); + } + } catch (AssertionError e) { + assertionsTriggered.incrementAndGet(); + } + } + }); + threads[i].start(); + } + for (Thread t : threads) { + t.join(); + } + assertTrue(called.get()); + assertEquals(overage, assertionsTriggered.get()); + } + + public void testConcurrentFailures() throws InterruptedException { + AtomicReference finalException = new AtomicReference<>(); + int numGroups = randomIntBetween(10, 100); + CountDownActionListener listener = new CountDownActionListener(numGroups, ActionListener.wrap(r -> {}, finalException::set)); + ExecutorService executorService = Executors.newFixedThreadPool(numGroups); + for (int i = 0; i < numGroups; i++) { + executorService.submit(() -> listener.onFailure(new IOException())); + } + + executorService.shutdown(); + executorService.awaitTermination(10, TimeUnit.SECONDS); + + Exception exception = finalException.get(); + assertNotNull(exception); + assertThat(exception, instanceOf(IOException.class)); + assertEquals(numGroups - 1, exception.getSuppressed().length); + } + + /* + * It can happen that the same exception causes a grouped listener to be notified of the failure multiple times. Since we suppress + * additional exceptions into the first exception, we have to guard against suppressing into the same exception, which could occur if we + * are notified of with the same failure multiple times. This test verifies that the guard against self-suppression remains. + */ + public void testRepeatNotificationForTheSameException() { + final AtomicReference finalException = new AtomicReference<>(); + final CountDownActionListener listener = new CountDownActionListener(2, ActionListener.wrap(r -> {}, finalException::set)); + final Exception e = new Exception(); + // repeat notification for the same exception + listener.onFailure(e); + listener.onFailure(e); + assertThat(finalException.get(), not(nullValue())); + assertThat(finalException.get(), equalTo(e)); + } +} diff --git a/server/src/test/java/org/elasticsearch/repositories/blobstore/ShardSnapshotTaskRunnerTests.java b/server/src/test/java/org/elasticsearch/repositories/blobstore/ShardSnapshotTaskRunnerTests.java index 6cecbd540301..10038993f4c7 100644 --- a/server/src/test/java/org/elasticsearch/repositories/blobstore/ShardSnapshotTaskRunnerTests.java +++ b/server/src/test/java/org/elasticsearch/repositories/blobstore/ShardSnapshotTaskRunnerTests.java @@ -11,7 +11,7 @@ import org.apache.lucene.store.ByteBuffersDirectory; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.support.GroupedActionListener; +import org.elasticsearch.action.support.CountDownActionListener; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.settings.Settings; @@ -72,10 +72,7 @@ public void snapshotShard(SnapshotShardContext context) { finishedShardSnapshots.incrementAndGet(); } else { expectedFileSnapshotTasks.addAndGet(filesToUpload); - ActionListener uploadListener = new GroupedActionListener<>( - filesToUpload, - ActionListener.wrap(finishedShardSnapshots::incrementAndGet) - ); + ActionListener uploadListener = new CountDownActionListener(filesToUpload, finishedShardSnapshots::incrementAndGet); for (int i = 0; i < filesToUpload; i++) { taskRunner.enqueueFileSnapshot(context, ShardSnapshotTaskRunnerTests::dummyFileInfo, uploadListener); } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SnapshotRetentionTask.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SnapshotRetentionTask.java index ad5eacb07b3e..b1f8c11ba9e7 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SnapshotRetentionTask.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/slm/SnapshotRetentionTask.java @@ -10,7 +10,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.support.GroupedActionListener; +import org.elasticsearch.action.support.CountDownActionListener; import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.client.internal.Client; import org.elasticsearch.client.internal.OriginSettingClient; @@ -328,9 +328,9 @@ void deleteSnapshots( long startTime = nowNanoSupplier.getAsLong(); final AtomicInteger deleted = new AtomicInteger(0); final AtomicInteger failed = new AtomicInteger(0); - final GroupedActionListener allDeletesListener = new GroupedActionListener<>( + final CountDownActionListener allDeletesListener = new CountDownActionListener( snapshotsToDelete.size(), - ActionListener.runAfter(listener.map(v -> null), () -> { + ActionListener.runAfter(listener, () -> { TimeValue totalElapsedTime = TimeValue.timeValueNanos(nowNanoSupplier.getAsLong() - startTime); logger.debug("total elapsed time for deletion of [{}] snapshots: {}", deleted, totalElapsedTime); slmStats.deletionTime(totalElapsedTime); @@ -354,7 +354,7 @@ private void deleteSnapshots( ActionListener listener ) { - final ActionListener allDeletesListener = new GroupedActionListener<>(snapshots.size(), listener.map(v -> null)); + final ActionListener allDeletesListener = new CountDownActionListener(snapshots.size(), listener); for (Tuple info : snapshots) { final SnapshotId snapshotId = info.v1(); if (runningDeletions.add(snapshotId) == false) { diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/store/SearchableSnapshotDirectory.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/store/SearchableSnapshotDirectory.java index 3dfe1ed53df2..c0c6fca08b36 100644 --- a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/store/SearchableSnapshotDirectory.java +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/store/SearchableSnapshotDirectory.java @@ -20,7 +20,7 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRunnable; import org.elasticsearch.action.StepListener; -import org.elasticsearch.action.support.GroupedActionListener; +import org.elasticsearch.action.support.CountDownActionListener; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.routing.RecoverySource; import org.elasticsearch.common.blobstore.BlobContainer; @@ -484,9 +484,9 @@ private void prewarmCache(ActionListener listener) { final BlockingQueue, CheckedRunnable>> queue = new LinkedBlockingQueue<>(); final Executor executor = prewarmExecutor(); - final GroupedActionListener completionListener = new GroupedActionListener<>( + final CountDownActionListener completionListener = new CountDownActionListener( snapshot().totalFileCount(), - ActionListener.wrap(voids -> { + ActionListener.wrap(ignored -> { recoveryState.setPreWarmComplete(); listener.onResponse(null); }, listener::onFailure) @@ -510,9 +510,9 @@ private void prewarmCache(ActionListener listener) { assert input instanceof CachedBlobContainerIndexInput : "expected cached index input but got " + input.getClass(); final int numberOfParts = file.numberOfParts(); - final StepListener> fileCompletionListener = new StepListener<>(); - fileCompletionListener.addListener(completionListener.map(voids -> null)); - fileCompletionListener.whenComplete(voids -> { + final StepListener fileCompletionListener = new StepListener<>(); + fileCompletionListener.addListener(completionListener); + fileCompletionListener.whenComplete(ignored -> { logger.debug("{} file [{}] prewarmed", shardId, file.physicalName()); input.close(); }, e -> { @@ -520,7 +520,7 @@ private void prewarmCache(ActionListener listener) { IOUtils.closeWhileHandlingException(input); }); - final GroupedActionListener partsListener = new GroupedActionListener<>(numberOfParts, fileCompletionListener); + final CountDownActionListener partsListener = new CountDownActionListener(numberOfParts, fileCompletionListener); submitted = true; for (int p = 0; p < numberOfParts; p++) { final int part = p; From 4be7743fa3f9684260a2b43bf3bbcb75395f676b Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Mon, 19 Dec 2022 17:02:20 +0100 Subject: [PATCH 308/919] Share common runForDoc impl in AbstractFieldScript (#92450) Each runtime field script class implements its own slighly different variant of the runForDoc method, sometimes called resultsForDoc in case it also returns the results. For simplicity, we can unify runForDoc in the base class and introduce additional getValues methods where missing. This should also simplify unifying error handling down the line introducing the error handling logic directly in runForDoc. --- .../action/PainlessExecuteAction.java | 3 ++- .../fielddata/StringScriptDocValues.java | 3 ++- .../script/AbstractFieldScript.java | 11 ++++++++++ .../script/AbstractLongFieldScript.java | 8 ++----- .../script/BooleanFieldScript.java | 8 ++----- .../script/CompositeFieldScript.java | 10 +++++---- .../script/DoubleFieldScript.java | 8 ++----- .../script/GeoPointFieldScript.java | 8 ++----- .../elasticsearch/script/IpFieldScript.java | 8 ++----- .../script/StringFieldScript.java | 22 +++++++++---------- .../AbstractStringScriptFieldQuery.java | 3 ++- .../index/mapper/StringFieldScriptTests.java | 8 +++---- 12 files changed, 48 insertions(+), 52 deletions(-) diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessExecuteAction.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessExecuteAction.java index 6a3d54e73497..a65d18a762ec 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessExecuteAction.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessExecuteAction.java @@ -663,7 +663,8 @@ static Response innerShardOperation(Request request, ScriptService scriptService context.lookup() ); CompositeFieldScript compositeFieldScript = leafFactory.newInstance(leafReaderContext); - return new Response(compositeFieldScript.runForDoc(0)); + compositeFieldScript.runForDoc(0); + return new Response(compositeFieldScript.getFieldValues()); }, indexService); } else { throw new UnsupportedOperationException("unsupported context [" + scriptContext.name + "]"); diff --git a/server/src/main/java/org/elasticsearch/index/fielddata/StringScriptDocValues.java b/server/src/main/java/org/elasticsearch/index/fielddata/StringScriptDocValues.java index e42659743521..f5e236e9e864 100644 --- a/server/src/main/java/org/elasticsearch/index/fielddata/StringScriptDocValues.java +++ b/server/src/main/java/org/elasticsearch/index/fielddata/StringScriptDocValues.java @@ -21,7 +21,8 @@ public final class StringScriptDocValues extends SortingBinaryDocValues { @Override public boolean advanceExact(int docId) { - List results = script.resultsForDoc(docId); + script.runForDoc(docId); + List results = script.getValues(); count = results.size(); if (count == 0) { return false; diff --git a/server/src/main/java/org/elasticsearch/script/AbstractFieldScript.java b/server/src/main/java/org/elasticsearch/script/AbstractFieldScript.java index c31e5d3db352..cda67064240e 100644 --- a/server/src/main/java/org/elasticsearch/script/AbstractFieldScript.java +++ b/server/src/main/java/org/elasticsearch/script/AbstractFieldScript.java @@ -133,5 +133,16 @@ protected final void checkMaxSize(int currentSize) { } } + protected abstract void prepareExecute(); + + /** + * Execute the script for the provided {@code docId}. + */ + public final void runForDoc(int docId) { + prepareExecute(); + setDocument(docId); + execute(); + } + public abstract void execute(); } diff --git a/server/src/main/java/org/elasticsearch/script/AbstractLongFieldScript.java b/server/src/main/java/org/elasticsearch/script/AbstractLongFieldScript.java index 10cb90607b5a..101598b233cd 100644 --- a/server/src/main/java/org/elasticsearch/script/AbstractLongFieldScript.java +++ b/server/src/main/java/org/elasticsearch/script/AbstractLongFieldScript.java @@ -26,13 +26,9 @@ public AbstractLongFieldScript(String fieldName, Map params, Sea super(fieldName, params, searchLookup, ctx); } - /** - * Execute the script for the provided {@code docId}. - */ - public final void runForDoc(int docId) { + @Override + protected void prepareExecute() { count = 0; - setDocument(docId); - execute(); } /** diff --git a/server/src/main/java/org/elasticsearch/script/BooleanFieldScript.java b/server/src/main/java/org/elasticsearch/script/BooleanFieldScript.java index 5591ff6d796f..5c6e4828471f 100644 --- a/server/src/main/java/org/elasticsearch/script/BooleanFieldScript.java +++ b/server/src/main/java/org/elasticsearch/script/BooleanFieldScript.java @@ -75,14 +75,10 @@ public BooleanFieldScript(String fieldName, Map params, SearchLo super(fieldName, params, searchLookup, ctx); } - /** - * Execute the script for the provided {@code docId}. - */ - public final void runForDoc(int docId) { + @Override + protected void prepareExecute() { trues = 0; falses = 0; - setDocument(docId); - execute(); } public final void runForDoc(int docId, Consumer consumer) { diff --git a/server/src/main/java/org/elasticsearch/script/CompositeFieldScript.java b/server/src/main/java/org/elasticsearch/script/CompositeFieldScript.java index 253d83fd2596..c02968059ae0 100644 --- a/server/src/main/java/org/elasticsearch/script/CompositeFieldScript.java +++ b/server/src/main/java/org/elasticsearch/script/CompositeFieldScript.java @@ -47,17 +47,19 @@ public CompositeFieldScript(String fieldName, Map params, Search */ public final List getValues(String field) { // TODO for now we re-run the script every time a leaf field is accessed, but we could cache the values? - fieldValues.clear(); + prepareExecute(); execute(); List values = fieldValues.get(field); fieldValues.clear(); // don't hold on to values unnecessarily return values; } - public final Map> runForDoc(int doc) { - setDocument(doc); + @Override + protected void prepareExecute() { fieldValues.clear(); - execute(); + } + + public final Map> getFieldValues() { return fieldValues; } diff --git a/server/src/main/java/org/elasticsearch/script/DoubleFieldScript.java b/server/src/main/java/org/elasticsearch/script/DoubleFieldScript.java index f59759e65bdd..d259e9a42d89 100644 --- a/server/src/main/java/org/elasticsearch/script/DoubleFieldScript.java +++ b/server/src/main/java/org/elasticsearch/script/DoubleFieldScript.java @@ -74,13 +74,9 @@ public DoubleFieldScript(String fieldName, Map params, SearchLoo super(fieldName, params, searchLookup, ctx); } - /** - * Execute the script for the provided {@code docId}. - */ - public final void runForDoc(int docId) { + @Override + protected void prepareExecute() { count = 0; - setDocument(docId); - execute(); } /** diff --git a/server/src/main/java/org/elasticsearch/script/GeoPointFieldScript.java b/server/src/main/java/org/elasticsearch/script/GeoPointFieldScript.java index d89cf9d4d069..c4276fc32b24 100644 --- a/server/src/main/java/org/elasticsearch/script/GeoPointFieldScript.java +++ b/server/src/main/java/org/elasticsearch/script/GeoPointFieldScript.java @@ -85,13 +85,9 @@ public GeoPointFieldScript(String fieldName, Map params, SearchL super(fieldName, params, searchLookup, ctx); } - /** - * Execute the script for the provided {@code docId}. - */ - public final void runForDoc(int docId) { + @Override + protected void prepareExecute() { count = 0; - setDocument(docId); - execute(); } /** diff --git a/server/src/main/java/org/elasticsearch/script/IpFieldScript.java b/server/src/main/java/org/elasticsearch/script/IpFieldScript.java index 665eaee24ced..c8cb1f0a2278 100644 --- a/server/src/main/java/org/elasticsearch/script/IpFieldScript.java +++ b/server/src/main/java/org/elasticsearch/script/IpFieldScript.java @@ -95,13 +95,9 @@ public IpFieldScript(String fieldName, Map params, SearchLookup super(fieldName, params, searchLookup, ctx); } - /** - * Execute the script for the provided {@code docId}. - */ - public final void runForDoc(int docId) { + @Override + protected void prepareExecute() { count = 0; - setDocument(docId); - execute(); } public final void runForDoc(int docId, Consumer consumer) { diff --git a/server/src/main/java/org/elasticsearch/script/StringFieldScript.java b/server/src/main/java/org/elasticsearch/script/StringFieldScript.java index 7e366f4b72b1..b996293f1c51 100644 --- a/server/src/main/java/org/elasticsearch/script/StringFieldScript.java +++ b/server/src/main/java/org/elasticsearch/script/StringFieldScript.java @@ -81,22 +81,22 @@ public StringFieldScript(String fieldName, Map params, SearchLoo super(fieldName, params, searchLookup, ctx); } - /** - * Execute the script for the provided {@code docId}. - * - * @return a mutable {@link List} that contains the results of the script - * and will be modified the next time you call {@linkplain #resultsForDoc}. - */ - public final List resultsForDoc(int docId) { + @Override + protected void prepareExecute() { results.clear(); chars = 0; - setDocument(docId); - execute(); - return results; } public final void runForDoc(int docId, Consumer consumer) { - resultsForDoc(docId).forEach(consumer); + runForDoc(docId); + results.forEach(consumer); + } + + /** + * Values from the last time runForDoc(int) was called. This list is mutable and will change with the next call of runForDoc(int). + */ + public List getValues() { + return results; } @Override diff --git a/server/src/main/java/org/elasticsearch/search/runtime/AbstractStringScriptFieldQuery.java b/server/src/main/java/org/elasticsearch/search/runtime/AbstractStringScriptFieldQuery.java index bf2be13aab77..dde67d2143d7 100644 --- a/server/src/main/java/org/elasticsearch/search/runtime/AbstractStringScriptFieldQuery.java +++ b/server/src/main/java/org/elasticsearch/search/runtime/AbstractStringScriptFieldQuery.java @@ -24,7 +24,8 @@ abstract class AbstractStringScriptFieldQuery extends AbstractScriptFieldQuery null, (ft, lookup, fdt) -> null, new SourceLookup.ReaderSourceProvider()) ); StringFieldScript stringFieldScript = leafFactory.newInstance(reader.leaves().get(0)); - List results = stringFieldScript.resultsForDoc(0); - assertEquals(numValues, results.size()); + stringFieldScript.runForDoc(0); + assertEquals(numValues, stringFieldScript.getValues().size()); } } } @@ -162,8 +162,8 @@ public final void testFromSourceDoesNotEnforceCharsLimit() throws IOException { new SearchLookup(field -> null, (ft, lookup, fdt) -> null, new SourceLookup.ReaderSourceProvider()) ); StringFieldScript stringFieldScript = leafFactory.newInstance(reader.leaves().get(0)); - List results = stringFieldScript.resultsForDoc(0); - assertEquals(5, results.size()); + stringFieldScript.runForDoc(0); + assertEquals(5, stringFieldScript.getValues().size()); } } } From 661ea5f74d50189bc4fc185020f863ec1796bb49 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 19 Dec 2022 16:41:26 +0000 Subject: [PATCH 309/919] ActionListener#notifyOnce should release delegate on completion (#92452) There's no need to keep hold of the delegate after completing it, and in some cases this might hold on to excessive heap. With this commit we drop the reference to the delegate when it's complete. Closes #92451 --- .../elasticsearch/action/ActionListener.java | 23 +++++-- .../action/NotifyOnceListener.java | 39 ----------- .../elasticsearch/action/StepListener.java | 17 +++-- .../metadata/MetadataIndexStateService.java | 17 +++-- .../HandshakingTransportAddressConnector.java | 9 ++- .../action/ActionListenerTests.java | 49 ++++++++++++++ .../action/NotifyOnceListenerTests.java | 65 ------------------- 7 files changed, 91 insertions(+), 128 deletions(-) delete mode 100644 server/src/main/java/org/elasticsearch/action/NotifyOnceListener.java delete mode 100644 server/src/test/java/org/elasticsearch/action/NotifyOnceListenerTests.java diff --git a/server/src/main/java/org/elasticsearch/action/ActionListener.java b/server/src/main/java/org/elasticsearch/action/ActionListener.java index 1b918186009c..6a41a8205b78 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionListener.java +++ b/server/src/main/java/org/elasticsearch/action/ActionListener.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -418,15 +419,27 @@ public String toString() { * and {@link #onFailure(Exception)} of the provided listener will be called at most once. */ static ActionListener notifyOnce(ActionListener delegate) { - return new NotifyOnceListener() { + final var delegateRef = new AtomicReference<>(delegate); + return new ActionListener<>() { @Override - protected void innerOnResponse(Response response) { - delegate.onResponse(response); + public void onResponse(Response response) { + final var acquired = delegateRef.getAndSet(null); + if (acquired != null) { + acquired.onResponse(response); + } } @Override - protected void innerOnFailure(Exception e) { - delegate.onFailure(e); + public void onFailure(Exception e) { + final var acquired = delegateRef.getAndSet(null); + if (acquired != null) { + acquired.onFailure(e); + } + } + + @Override + public String toString() { + return "notifyOnce[" + delegate + "]"; } }; } diff --git a/server/src/main/java/org/elasticsearch/action/NotifyOnceListener.java b/server/src/main/java/org/elasticsearch/action/NotifyOnceListener.java deleted file mode 100644 index 582290f2a434..000000000000 --- a/server/src/main/java/org/elasticsearch/action/NotifyOnceListener.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.action; - -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * A listener that ensures that only one of onResponse or onFailure is called. And the method - * the is called is only called once. Subclasses should implement notification logic with - * innerOnResponse and innerOnFailure. - */ -public abstract class NotifyOnceListener implements ActionListener { - - private final AtomicBoolean hasBeenCalled = new AtomicBoolean(false); - - protected abstract void innerOnResponse(Response response); - - protected abstract void innerOnFailure(Exception e); - - @Override - public final void onResponse(Response response) { - if (hasBeenCalled.compareAndSet(false, true)) { - innerOnResponse(response); - } - } - - @Override - public final void onFailure(Exception e) { - if (hasBeenCalled.compareAndSet(false, true)) { - innerOnFailure(e); - } - } -} diff --git a/server/src/main/java/org/elasticsearch/action/StepListener.java b/server/src/main/java/org/elasticsearch/action/StepListener.java index dab36040e3e4..e36b799b9290 100644 --- a/server/src/main/java/org/elasticsearch/action/StepListener.java +++ b/server/src/main/java/org/elasticsearch/action/StepListener.java @@ -14,6 +14,7 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; import java.util.function.Consumer; @@ -40,7 +41,9 @@ * } */ -public final class StepListener extends NotifyOnceListener { +public final class StepListener implements ActionListener { + + private final AtomicBoolean hasBeenCalled = new AtomicBoolean(false); private final ListenableFuture delegate; public StepListener() { @@ -48,13 +51,17 @@ public StepListener() { } @Override - protected void innerOnResponse(Response response) { - delegate.onResponse(response); + public void onResponse(Response response) { + if (hasBeenCalled.compareAndSet(false, true)) { + delegate.onResponse(response); + } } @Override - protected void innerOnFailure(Exception e) { - delegate.onFailure(e); + public void onFailure(Exception e) { + if (hasBeenCalled.compareAndSet(false, true)) { + delegate.onFailure(e); + } } /** diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexStateService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexStateService.java index 592654ac8c12..b77511f7f408 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexStateService.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexStateService.java @@ -14,7 +14,6 @@ import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRunnable; -import org.elasticsearch.action.NotifyOnceListener; import org.elasticsearch.action.admin.indices.close.CloseIndexClusterStateUpdateRequest; import org.elasticsearch.action.admin.indices.close.CloseIndexResponse; import org.elasticsearch.action.admin.indices.close.CloseIndexResponse.IndexResult; @@ -619,9 +618,9 @@ private void waitForShardsReadyForClosing( for (int i = 0; i < indexRoutingTable.size(); i++) { IndexShardRoutingTable shardRoutingTable = indexRoutingTable.shard(i); final int shardId = shardRoutingTable.shardId().id(); - sendVerifyShardBeforeCloseRequest(shardRoutingTable, closingBlock, new NotifyOnceListener<>() { + sendVerifyShardBeforeCloseRequest(shardRoutingTable, closingBlock, ActionListener.notifyOnce(new ActionListener<>() { @Override - public void innerOnResponse(final ReplicationResponse replicationResponse) { + public void onResponse(ReplicationResponse replicationResponse) { ShardResult.Failure[] failures = Arrays.stream(replicationResponse.getShardInfo().getFailures()) .map(f -> new ShardResult.Failure(f.index(), f.shardId(), f.getCause(), f.nodeId())) .toArray(ShardResult.Failure[]::new); @@ -630,7 +629,7 @@ public void innerOnResponse(final ReplicationResponse replicationResponse) { } @Override - public void innerOnFailure(final Exception e) { + public void onFailure(Exception e) { ShardResult.Failure failure = new ShardResult.Failure(index.getName(), shardId, e); results.setOnce(shardId, new ShardResult(shardId, new ShardResult.Failure[] { failure })); processIfFinished(); @@ -641,7 +640,7 @@ private void processIfFinished() { onResponse.accept(new IndexResult(index, results.toArray(new ShardResult[results.length()]))); } } - }); + })); } } @@ -749,9 +748,9 @@ private void waitForShardsReady( for (int i = 0; i < indexRoutingTable.size(); i++) { IndexShardRoutingTable shardRoutingTable = indexRoutingTable.shard(i); final int shardId = shardRoutingTable.shardId().id(); - sendVerifyShardBlockRequest(shardRoutingTable, clusterBlock, new NotifyOnceListener<>() { + sendVerifyShardBlockRequest(shardRoutingTable, clusterBlock, ActionListener.notifyOnce(new ActionListener<>() { @Override - public void innerOnResponse(final ReplicationResponse replicationResponse) { + public void onResponse(ReplicationResponse replicationResponse) { AddBlockShardResult.Failure[] failures = Arrays.stream(replicationResponse.getShardInfo().getFailures()) .map(f -> new AddBlockShardResult.Failure(f.index(), f.shardId(), f.getCause(), f.nodeId())) .toArray(AddBlockShardResult.Failure[]::new); @@ -760,7 +759,7 @@ public void innerOnResponse(final ReplicationResponse replicationResponse) { } @Override - public void innerOnFailure(final Exception e) { + public void onFailure(Exception e) { AddBlockShardResult.Failure failure = new AddBlockShardResult.Failure(index.getName(), shardId, e); results.setOnce(shardId, new AddBlockShardResult(shardId, new AddBlockShardResult.Failure[] { failure })); processIfFinished(); @@ -773,7 +772,7 @@ private void processIfFinished() { onResponse.accept(result); } } - }); + })); } } diff --git a/server/src/main/java/org/elasticsearch/discovery/HandshakingTransportAddressConnector.java b/server/src/main/java/org/elasticsearch/discovery/HandshakingTransportAddressConnector.java index f852a4f584e6..74b1fda553ab 100644 --- a/server/src/main/java/org/elasticsearch/discovery/HandshakingTransportAddressConnector.java +++ b/server/src/main/java/org/elasticsearch/discovery/HandshakingTransportAddressConnector.java @@ -12,7 +12,6 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.NotifyOnceListener; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.Randomness; import org.elasticsearch.common.UUIDs; @@ -95,10 +94,10 @@ public void connectToRemoteMasterNode(TransportAddress transportAddress, ActionL // use NotifyOnceListener to make sure the following line does not result in onFailure being called when // the connection is closed in the onResponse handler - transportService.handshake(connection, probeHandshakeTimeout, new NotifyOnceListener<>() { + transportService.handshake(connection, probeHandshakeTimeout, ActionListener.notifyOnce(new ActionListener<>() { @Override - protected void innerOnResponse(DiscoveryNode remoteNode) { + public void onResponse(DiscoveryNode remoteNode) { try { // success means (amongst other things) that the cluster names match logger.trace("[{}] handshake successful: {}", transportAddress, remoteNode); @@ -166,7 +165,7 @@ public void onFailure(Exception e) { } @Override - protected void innerOnFailure(Exception e) { + public void onFailure(Exception e) { // we opened a connection and successfully performed a low-level handshake, so we were definitely // talking to an Elasticsearch node, but the high-level handshake failed indicating some kind of // mismatched configurations (e.g. cluster name) that the user should address @@ -175,7 +174,7 @@ protected void innerOnFailure(Exception e) { listener.onFailure(e); } - }); + })); }) ); diff --git a/server/src/test/java/org/elasticsearch/action/ActionListenerTests.java b/server/src/test/java/org/elasticsearch/action/ActionListenerTests.java index 84fb08f03adc..a43864a9938c 100644 --- a/server/src/test/java/org/elasticsearch/action/ActionListenerTests.java +++ b/server/src/test/java/org/elasticsearch/action/ActionListenerTests.java @@ -14,7 +14,9 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -209,6 +211,53 @@ public void onFailure(Exception e) { } } + public void testConcurrentNotifyOnce() throws InterruptedException { + final var completed = new AtomicBoolean(); + final var listener = ActionListener.notifyOnce(new ActionListener() { + @Override + public void onResponse(Void o) { + assertTrue(completed.compareAndSet(false, true)); + } + + @Override + public void onFailure(Exception e) { + assertTrue(completed.compareAndSet(false, true)); + } + + @Override + public String toString() { + return "inner-listener"; + } + }); + assertThat(listener.toString(), equalTo("notifyOnce[inner-listener]")); + + final var threads = new Thread[between(1, 10)]; + final var startBarrier = new CyclicBarrier(threads.length); + for (int i = 0; i < threads.length; i++) { + threads[i] = new Thread(() -> { + try { + startBarrier.await(10, TimeUnit.SECONDS); + } catch (Exception e) { + throw new AssertionError(e); + } + if (randomBoolean()) { + listener.onResponse(null); + } else { + listener.onFailure(new RuntimeException("test")); + } + }); + } + + for (Thread thread : threads) { + thread.start(); + } + for (Thread thread : threads) { + thread.join(); + } + + assertTrue(completed.get()); + } + public void testCompleteWith() { PlainActionFuture onResponseListener = new PlainActionFuture<>(); ActionListener.completeWith(onResponseListener, () -> 100); diff --git a/server/src/test/java/org/elasticsearch/action/NotifyOnceListenerTests.java b/server/src/test/java/org/elasticsearch/action/NotifyOnceListenerTests.java deleted file mode 100644 index fa6761b2bf3c..000000000000 --- a/server/src/test/java/org/elasticsearch/action/NotifyOnceListenerTests.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.action; - -import org.elasticsearch.test.ESTestCase; - -import java.util.concurrent.atomic.AtomicReference; - -public class NotifyOnceListenerTests extends ESTestCase { - - public void testWhenSuccessCannotNotifyMultipleTimes() { - AtomicReference response = new AtomicReference<>(); - AtomicReference exception = new AtomicReference<>(); - - NotifyOnceListener listener = new NotifyOnceListener() { - @Override - public void innerOnResponse(String s) { - response.set(s); - } - - @Override - public void innerOnFailure(Exception e) { - exception.set(e); - } - }; - - listener.onResponse("response"); - listener.onResponse("wrong-response"); - listener.onFailure(new RuntimeException()); - - assertNull(exception.get()); - assertEquals("response", response.get()); - } - - public void testWhenErrorCannotNotifyMultipleTimes() { - AtomicReference response = new AtomicReference<>(); - AtomicReference exception = new AtomicReference<>(); - - NotifyOnceListener listener = new NotifyOnceListener() { - @Override - public void innerOnResponse(String s) { - response.set(s); - } - - @Override - public void innerOnFailure(Exception e) { - exception.set(e); - } - }; - - RuntimeException expected = new RuntimeException(); - listener.onFailure(expected); - listener.onFailure(new IllegalArgumentException()); - listener.onResponse("response"); - - assertNull(response.get()); - assertSame(expected, exception.get()); - } -} From 0993c31eb77ced68c73a33280a0669ab18469755 Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Tue, 20 Dec 2022 10:26:53 +0000 Subject: [PATCH 310/919] GA the health API (#92420) This marks the Health API as generally available. --- docs/changelog/92420.yaml | 14 +++++ docs/reference/health/health.asciidoc | 16 +++--- .../http/HealthRestCancellationIT.java | 2 +- .../rest-api-spec/api/_internal.health.json | 51 ------------------ .../resources/rest-api-spec/api/health.json | 53 +++++++++++++++++++ .../rest-api-spec/test/health/10_basic.yml | 2 +- .../rest-api-spec/test/health/30_feature.yml | 8 +-- .../test/health/40_diagnosis.yml | 6 +-- .../health/RestGetHealthAction.java | 2 +- 9 files changed, 83 insertions(+), 71 deletions(-) create mode 100644 docs/changelog/92420.yaml delete mode 100644 rest-api-spec/src/main/resources/rest-api-spec/api/_internal.health.json create mode 100644 rest-api-spec/src/main/resources/rest-api-spec/api/health.json diff --git a/docs/changelog/92420.yaml b/docs/changelog/92420.yaml new file mode 100644 index 000000000000..288c8738d19f --- /dev/null +++ b/docs/changelog/92420.yaml @@ -0,0 +1,14 @@ +pr: 92420 +summary: The Health API is now generally available +area: Health +type: feature +issues: [] +highlight: + title: The Health API is now generally available + body: |- + Elasticsearch introduces a new Health API designed to report the health of + the cluster. The new API provides both a high level overview of the cluster + health, and a very detailed report that can include a precise diagnosis and + a resolution. + notable: true + diff --git a/docs/reference/health/health.asciidoc b/docs/reference/health/health.asciidoc index 579c20a96862..84be05beb4e9 100644 --- a/docs/reference/health/health.asciidoc +++ b/docs/reference/health/health.asciidoc @@ -4,18 +4,14 @@ Health ++++ -An experimental API that returns the health status of an {es} cluster. - -This API is currently experimental for internal use by Elastic software only. - -NOTE: {cloud-only} +An API that returns the health status of an {es} cluster. [[health-api-request]] ==== {api-request-title} -`GET /_internal/_health` + +`GET /_health` + -`GET /_internal/_health/` + +`GET /_health/` + [[health-api-prereqs]] ==== {api-prereq-title} @@ -386,7 +382,7 @@ watermark threshold>>. [source,console] -------------------------------------------------- -GET _internal/_health +GET _health -------------------------------------------------- The API returns a response with all the indicators regardless @@ -394,14 +390,14 @@ of current status. [source,console] -------------------------------------------------- -GET _internal/_health/shards_availability +GET _health/shards_availability -------------------------------------------------- The API returns a response for just the shard availability indicator. [source,console] -------------------------------------------------- -GET _internal/_health?verbose=false +GET _health?verbose=false -------------------------------------------------- The API returns a response with all health indicators but will diff --git a/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/HealthRestCancellationIT.java b/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/HealthRestCancellationIT.java index 9e3b884622d1..2772745e0b57 100644 --- a/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/HealthRestCancellationIT.java +++ b/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/HealthRestCancellationIT.java @@ -47,7 +47,7 @@ protected Collection> nodePlugins() { } public void testHealthRestCancellation() throws Exception { - runTest(new Request(HttpGet.METHOD_NAME, "/_internal/_health")); + runTest(new Request(HttpGet.METHOD_NAME, "/_health")); } private void runTest(Request request) throws Exception { diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/_internal.health.json b/rest-api-spec/src/main/resources/rest-api-spec/api/_internal.health.json deleted file mode 100644 index 615f072a80aa..000000000000 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/_internal.health.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "_internal.health":{ - "documentation":{ - "url": null, - "description":"Returns the health of the cluster." - }, - "stability":"experimental", - "visibility":"private", - "headers":{ - "accept": [ "application/json"] - }, - "url":{ - "paths":[ - { - "path":"/_internal/_health", - "methods":[ - "GET" - ] - }, - { - "path":"/_internal/_health/{feature}", - "methods":[ - "GET" - ], - "parts":{ - "feature":{ - "type":"string", - "description":"A feature of the cluster, as returned by the top-level health API" - } - } - } - ] - }, - "params":{ - "timeout": { - "type": "time", - "description": "Explicit operation timeout" - }, - "verbose": { - "type": "boolean", - "description": "Opt in for more information about the health of the system", - "default": true - }, - "size": { - "type": "int", - "description": "Limit the number of affected resources the health API returns", - "default": 1000 - } - } - } -} diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/health.json b/rest-api-spec/src/main/resources/rest-api-spec/api/health.json new file mode 100644 index 000000000000..9ab7febb68dd --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/health.json @@ -0,0 +1,53 @@ +{ + "health": { + "documentation": { + "url": "https://www.elastic.co/guide/en/elasticsearch/reference/current/health-api.html", + "description": "Returns the health of the cluster." + }, + "stability": "stable", + "visibility": "public", + "headers": { + "accept": [ + "application/json" + ] + }, + "url": { + "paths": [ + { + "path": "/_health", + "methods": [ + "GET" + ] + }, + { + "path": "/_health/{feature}", + "methods": [ + "GET" + ], + "parts": { + "feature": { + "type": "string", + "description": "A feature of the cluster, as returned by the top-level health API" + } + } + } + ] + }, + "params":{ + "timeout":{ + "type":"time", + "description":"Explicit operation timeout" + }, + "verbose":{ + "type":"boolean", + "description":"Opt in for more information about the health of the system", + "default":true + }, + "size": { + "type": "int", + "description": "Limit the number of affected resources the health API returns", + "default": 1000 + } + } + } +} diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/10_basic.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/10_basic.yml index 4f2c7113d877..247c8eb51fa9 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/10_basic.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/10_basic.yml @@ -7,7 +7,7 @@ # reason: "health was only added in 8.2.0, and master_is_stable in 8.4.0" - do: - _internal.health: {} + health: { } - is_true: cluster_name - match: { status: "green" } diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/30_feature.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/30_feature.yml index 575e4c7ad41d..eb11d6ce41a9 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/30_feature.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/30_feature.yml @@ -1,11 +1,11 @@ --- "cluster health test drilling down into a feature": - skip: - version: "- 8.5.99" - reason: "the verbose parameter was only added in 8.6" + version: "- 8.6.99" + reason: "the API path changed in 8.7" - do: - _internal.health: + health: feature: master_is_stable - is_true: cluster_name @@ -15,7 +15,7 @@ - is_true: indicators.master_is_stable.details.recent_masters - do: - _internal.health: + health: feature: master_is_stable verbose: false diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/40_diagnosis.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/40_diagnosis.yml index f475d3b0f1d0..12b38224cbcb 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/40_diagnosis.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/40_diagnosis.yml @@ -1,8 +1,8 @@ --- "Diagnosis": - skip: - version: "- 8.5.99" - reason: "diagnosis was redefined in 8.6.0" + version: "- 8.6.99" + reason: "the API path changed in 8.7" - do: indices.create: @@ -14,7 +14,7 @@ index.routing.allocation.enable: none - do: - _internal.health: + health: feature: shards_availability - is_true: cluster_name diff --git a/server/src/main/java/org/elasticsearch/health/RestGetHealthAction.java b/server/src/main/java/org/elasticsearch/health/RestGetHealthAction.java index 813668f9aa39..a608427dcdee 100644 --- a/server/src/main/java/org/elasticsearch/health/RestGetHealthAction.java +++ b/server/src/main/java/org/elasticsearch/health/RestGetHealthAction.java @@ -33,7 +33,7 @@ public String getName() { @Override public List routes() { - return List.of(new Route(GET, "/_internal/_health"), new Route(GET, "/_internal/_health/{indicator}")); + return List.of(new Route(GET, "/_health"), new Route(GET, "/_health/{indicator}")); } @Override From 26eabc67fddc1a89b14d49ba9268fefe2503792e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Witek?= Date: Tue, 20 Dec 2022 11:59:17 +0100 Subject: [PATCH 311/919] Make transform _stats request cancellable (#92389) --- .../api/transform.get_transform_stats.json | 5 + .../action/GetTransformStatsAction.java | 14 ++- .../GetTransformStatsActionRequestTests.java | 22 ++++- .../test/transform/transforms_stats.yml | 10 ++ .../TransformNoTransformNodeIT.java | 2 +- .../TransformConfigManagerTests.java | 41 ++++---- .../TransportGetTransformStatsAction.java | 93 +++++++++++-------- .../action/TransportStopTransformAction.java | 1 + .../TransportUpgradeTransformsAction.java | 2 +- .../IndexBasedTransformConfigManager.java | 36 +++++-- .../persistence/TransformConfigManager.java | 11 ++- .../rest/action/RestCatTransformAction.java | 2 +- .../action/RestGetTransformStatsAction.java | 10 +- .../InMemoryTransformConfigManager.java | 13 ++- 14 files changed, 186 insertions(+), 76 deletions(-) diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/transform.get_transform_stats.json b/rest-api-spec/src/main/resources/rest-api-spec/api/transform.get_transform_stats.json index f425c41f0997..8139fac7a818 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/transform.get_transform_stats.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/transform.get_transform_stats.json @@ -36,6 +36,11 @@ "required":false, "description":"specifies a max number of transform stats to get, defaults to 100" }, + "timeout":{ + "type":"time", + "required":false, + "description":"Controls the time to wait for the stats" + }, "allow_no_match":{ "type":"boolean", "required":false, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/GetTransformStatsAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/GetTransformStatsAction.java index e961ffb9619c..f2470c3829ca 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/GetTransformStatsAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/GetTransformStatsAction.java @@ -17,7 +17,11 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.tasks.CancellableTask; import org.elasticsearch.tasks.Task; +import org.elasticsearch.tasks.TaskId; import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xpack.core.action.util.PageParams; @@ -29,9 +33,11 @@ import java.io.IOException; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import static org.elasticsearch.action.ValidateActions.addValidationError; +import static org.elasticsearch.core.Strings.format; public class GetTransformStatsAction extends ActionType { @@ -51,7 +57,8 @@ public static class Request extends BaseTasksRequest { // used internally to expand the queried id expression private List expandedIds; - public Request(String id) { + public Request(String id, @Nullable TimeValue timeout) { + setTimeout(timeout); if (Strings.isNullOrEmpty(id) || id.equals("*")) { this.id = Metadata.ALL; } else { @@ -140,6 +147,11 @@ public boolean equals(Object obj) { Request other = (Request) obj; return Objects.equals(id, other.id) && Objects.equals(pageParams, other.pageParams) && allowNoMatch == other.allowNoMatch; } + + @Override + public Task createTask(long id, String type, String action, TaskId parentTaskId, Map headers) { + return new CancellableTask(id, type, action, format("get_transform_stats[%s]", this.id), parentTaskId, headers); + } } public static class Response extends BaseTasksResponse implements ToXContentObject { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/action/GetTransformStatsActionRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/action/GetTransformStatsActionRequestTests.java index 7c320c00f0d2..783c3d48d624 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/action/GetTransformStatsActionRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/action/GetTransformStatsActionRequestTests.java @@ -9,17 +9,37 @@ import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.tasks.CancellableTask; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.tasks.TaskId; import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xpack.core.transform.action.GetTransformStatsAction.Request; +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; + public class GetTransformStatsActionRequestTests extends AbstractWireSerializingTestCase { @Override protected Request createTestInstance() { - return new Request(randomBoolean() ? randomAlphaOfLengthBetween(1, 20) : randomBoolean() ? Metadata.ALL : null); + return new Request( + randomBoolean() ? randomAlphaOfLengthBetween(1, 20) : randomBoolean() ? Metadata.ALL : null, + randomBoolean() ? TimeValue.parseTimeValue(randomTimeValue(), "timeout") : null + ); } @Override protected Writeable.Reader instanceReader() { return Request::new; } + + public void testCreateTask() { + Request request = new Request("some-transform", null); + Task task = request.createTask(123, "type", "action", TaskId.EMPTY_TASK_ID, Map.of()); + assertThat(task, is(instanceOf(CancellableTask.class))); + assertThat(task.getDescription(), is(equalTo("get_transform_stats[some-transform]"))); + } } diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/transform/transforms_stats.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/transform/transforms_stats.yml index e23852572041..c3fe2114ff84 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/transform/transforms_stats.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/transform/transforms_stats.yml @@ -90,6 +90,16 @@ teardown: - lte: { transforms.0.stats.search_total: 1 } - match: { transforms.0.stats.search_failures: 0 } +--- +"Test get transform stats with timeout": + - do: + transform.get_transform_stats: + transform_id: "airline-transform-stats" + timeout: "10s" + - match: { count: 1 } + - match: { transforms.0.id: "airline-transform-stats" } + - match: { transforms.0.state: "/started|indexing|stopping|stopped/" } + --- "Test get transform stats on missing transform": - do: diff --git a/x-pack/plugin/transform/src/internalClusterTest/java/org/elasticsearch/xpack/transform/integration/TransformNoTransformNodeIT.java b/x-pack/plugin/transform/src/internalClusterTest/java/org/elasticsearch/xpack/transform/integration/TransformNoTransformNodeIT.java index a1abfbddaf33..710dcd424e69 100644 --- a/x-pack/plugin/transform/src/internalClusterTest/java/org/elasticsearch/xpack/transform/integration/TransformNoTransformNodeIT.java +++ b/x-pack/plugin/transform/src/internalClusterTest/java/org/elasticsearch/xpack/transform/integration/TransformNoTransformNodeIT.java @@ -41,7 +41,7 @@ protected Settings nodeSettings() { } public void testGetTransformStats() { - GetTransformStatsAction.Request request = new GetTransformStatsAction.Request("_all"); + GetTransformStatsAction.Request request = new GetTransformStatsAction.Request("_all", AcknowledgedRequest.DEFAULT_ACK_TIMEOUT); GetTransformStatsAction.Response response = client().execute(GetTransformStatsAction.INSTANCE, request).actionGet(); assertThat(response.getTransformsStats(), is(empty())); diff --git a/x-pack/plugin/transform/src/internalClusterTest/java/org/elasticsearch/xpack/transform/persistence/TransformConfigManagerTests.java b/x-pack/plugin/transform/src/internalClusterTest/java/org/elasticsearch/xpack/transform/persistence/TransformConfigManagerTests.java index 404dfa51d665..fafb905198d1 100644 --- a/x-pack/plugin/transform/src/internalClusterTest/java/org/elasticsearch/xpack/transform/persistence/TransformConfigManagerTests.java +++ b/x-pack/plugin/transform/src/internalClusterTest/java/org/elasticsearch/xpack/transform/persistence/TransformConfigManagerTests.java @@ -233,7 +233,13 @@ public void testExpandIds() throws Exception { // expand 1 id assertAsync( - listener -> transformConfigManager.expandTransformIds(transformConfig1.getId(), PageParams.defaultParams(), true, listener), + listener -> transformConfigManager.expandTransformIds( + transformConfig1.getId(), + PageParams.defaultParams(), + null, + true, + listener + ), tuple(1L, tuple(singletonList("transform1_expand"), singletonList(transformConfig1))), null, null @@ -244,6 +250,7 @@ public void testExpandIds() throws Exception { listener -> transformConfigManager.expandTransformIds( "transform1_expand,transform2_expand", PageParams.defaultParams(), + null, true, listener ), @@ -257,6 +264,7 @@ public void testExpandIds() throws Exception { listener -> transformConfigManager.expandTransformIds( "transform1*,transform2_expand,transform3_expand", PageParams.defaultParams(), + null, true, listener ), @@ -273,7 +281,7 @@ public void testExpandIds() throws Exception { // expand 3 ids _all assertAsync( - listener -> transformConfigManager.expandTransformIds("_all", PageParams.defaultParams(), true, listener), + listener -> transformConfigManager.expandTransformIds("_all", PageParams.defaultParams(), null, true, listener), tuple( 3L, tuple( @@ -287,7 +295,7 @@ public void testExpandIds() throws Exception { // expand 1 id _all with pagination assertAsync( - listener -> transformConfigManager.expandTransformIds("_all", new PageParams(0, 1), true, listener), + listener -> transformConfigManager.expandTransformIds("_all", new PageParams(0, 1), null, true, listener), tuple(3L, tuple(singletonList("transform1_expand"), singletonList(transformConfig1))), null, null @@ -295,7 +303,7 @@ public void testExpandIds() throws Exception { // expand 2 later ids _all with pagination assertAsync( - listener -> transformConfigManager.expandTransformIds("_all", new PageParams(1, 2), true, listener), + listener -> transformConfigManager.expandTransformIds("_all", new PageParams(1, 2), null, true, listener), tuple(3L, tuple(Arrays.asList("transform2_expand", "transform3_expand"), Arrays.asList(transformConfig2, transformConfig3))), null, null @@ -303,7 +311,7 @@ public void testExpandIds() throws Exception { // expand 1 id explicitly that does not exist assertAsync( - listener -> transformConfigManager.expandTransformIds("unknown,unknown2", new PageParams(1, 2), true, listener), + listener -> transformConfigManager.expandTransformIds("unknown,unknown2", new PageParams(1, 2), null, true, listener), (Tuple, List>>) null, null, e -> { @@ -317,7 +325,7 @@ public void testExpandIds() throws Exception { // expand 1 id implicitly that does not exist assertAsync( - listener -> transformConfigManager.expandTransformIds("unknown*", new PageParams(1, 2), false, listener), + listener -> transformConfigManager.expandTransformIds("unknown*", new PageParams(1, 2), null, false, listener), (Tuple, List>>) null, null, e -> { @@ -348,6 +356,7 @@ public void testExpandIds() throws Exception { listener -> transformConfigManager.expandTransformIds( "transform1_expand,transform2_expand", PageParams.defaultParams(), + null, true, listener ), @@ -368,25 +377,25 @@ public void testGetAllTransformIdsAndGetAllOutdatedTransformIds() throws Excepti TransformConfig transformConfig = TransformConfigTests.randomTransformConfig(id); assertAsync(listener -> transformConfigManager.putTransformConfiguration(transformConfig, listener), true, null, null); } - assertAsync(listener -> transformConfigManager.getAllTransformIds(listener), transformIds, null, null); + assertAsync(listener -> transformConfigManager.getAllTransformIds(null, listener), transformIds, null, null); // test recursive retrieval assertAsync( - listener -> transformConfigManager.expandAllTransformIds(false, 10, listener), + listener -> transformConfigManager.expandAllTransformIds(false, 10, null, listener), tuple(Long.valueOf(numberOfTransformsToGenerate), transformIds), null, null ); assertAsync( - listener -> transformConfigManager.getAllOutdatedTransformIds(listener), + listener -> transformConfigManager.getAllOutdatedTransformIds(null, listener), tuple(Long.valueOf(numberOfTransformsToGenerate), Collections.emptySet()), null, null ); assertAsync( - listener -> transformConfigManager.expandAllTransformIds(true, 10, listener), + listener -> transformConfigManager.expandAllTransformIds(true, 10, null, listener), tuple(Long.valueOf(numberOfTransformsToGenerate), Collections.emptySet()), null, null @@ -410,9 +419,9 @@ public void testGetAllTransformIdsAndGetAllOutdatedTransformIds() throws Excepti client().index(request).actionGet(); } - assertAsync(listener -> transformConfigManager.getAllTransformIds(listener), transformIds, null, null); + assertAsync(listener -> transformConfigManager.getAllTransformIds(null, listener), transformIds, null, null); assertAsync( - listener -> transformConfigManager.getAllOutdatedTransformIds(listener), + listener -> transformConfigManager.getAllOutdatedTransformIds(null, listener), tuple(Long.valueOf(numberOfTransformsToGenerate), Collections.emptySet()), null, null @@ -443,16 +452,16 @@ public void testGetAllTransformIdsAndGetAllOutdatedTransformIds() throws Excepti ); transformIds.add(oldTransformId); - assertAsync(listener -> transformConfigManager.getAllTransformIds(listener), transformIds, null, null); + assertAsync(listener -> transformConfigManager.getAllTransformIds(null, listener), transformIds, null, null); assertAsync( - listener -> transformConfigManager.getAllOutdatedTransformIds(listener), + listener -> transformConfigManager.getAllOutdatedTransformIds(null, listener), tuple(Long.valueOf(numberOfTransformsToGenerate + 1), Collections.singleton(oldTransformId)), null, null ); assertAsync( - listener -> transformConfigManager.expandAllTransformIds(true, 10, listener), + listener -> transformConfigManager.expandAllTransformIds(true, 10, null, listener), tuple(Long.valueOf(numberOfTransformsToGenerate + 1), Collections.singleton(oldTransformId)), null, null @@ -517,7 +526,7 @@ public void testGetStoredDocMultiple() throws InterruptedException { // returned docs will be ordered by id expectedDocs.sort(Comparator.comparing(TransformStoredDoc::getId)); - assertAsync(listener -> transformConfigManager.getTransformStoredDocs(ids, listener), expectedDocs, null, null); + assertAsync(listener -> transformConfigManager.getTransformStoredDocs(ids, null, listener), expectedDocs, null, null); } public void testDeleteOldTransformConfigurations() throws Exception { diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportGetTransformStatsAction.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportGetTransformStatsAction.java index faf910a25cf2..cfa532735d32 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportGetTransformStatsAction.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportGetTransformStatsAction.java @@ -17,6 +17,7 @@ import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.tasks.TransportTasksAction; import org.elasticsearch.client.internal.Client; +import org.elasticsearch.client.internal.ParentTaskAssigningClient; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; @@ -27,6 +28,7 @@ import org.elasticsearch.persistent.PersistentTasksCustomMetadata; import org.elasticsearch.persistent.PersistentTasksCustomMetadata.Assignment; import org.elasticsearch.tasks.Task; +import org.elasticsearch.tasks.TaskId; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.transform.action.GetTransformStatsAction; @@ -138,12 +140,15 @@ protected void taskOperation(Task actionTask, Request request, TransformTask tas @Override protected void doExecute(Task task, Request request, ActionListener finalListener) { + TaskId parentTaskId = new TaskId(clusterService.localNode().getId(), task.getId()); + final ClusterState clusterState = clusterService.state(); TransformNodes.warnIfNoTransformNodes(clusterState); transformConfigManager.expandTransformIds( request.getId(), request.getPageParams(), + request.getTimeout(), request.isAllowNoMatch(), ActionListener.wrap(hitsAndIds -> { boolean hasAnyTransformNode = TransformNodes.hasAnyTransformNode(clusterState.getNodes()); @@ -175,6 +180,7 @@ protected void doExecute(Task task, Request request, ActionListener fi response.getTransformsStats().forEach(dtsasi -> setNodeAttributes(dtsasi, tasksInProgress, clusterState)); } collectStatsForTransformsWithoutTasks( + parentTaskId, request, response, transformNodeAssignments.getWaitingForAssignment(), @@ -246,6 +252,7 @@ static TransformStats deriveStats(TransformTask task, @Nullable TransformCheckpo } private void collectStatsForTransformsWithoutTasks( + TaskId parentTaskId, Request request, Response response, Set transformsWaitingForAssignment, @@ -270,6 +277,7 @@ private void collectStatsForTransformsWithoutTasks( // copy the list as it might be immutable List allStateAndStats = new ArrayList<>(response.getTransformsStats()); addCheckpointingInfoForTransformsWithoutTasks( + parentTaskId, allStateAndStats, statsForTransformsWithoutTasks, transformsWaitingForAssignment, @@ -299,12 +307,16 @@ private void collectStatsForTransformsWithoutTasks( } }); - transformConfigManager.getTransformStoredDocs(transformsWithoutTasks, searchStatsListener); + transformConfigManager.getTransformStoredDocs(transformsWithoutTasks, request.getTimeout(), searchStatsListener); } - private void populateSingleStoppedTransformStat(TransformStoredDoc transform, ActionListener listener) { + private void populateSingleStoppedTransformStat( + TransformStoredDoc transform, + TaskId parentTaskId, + ActionListener listener + ) { transformCheckpointService.getCheckpointingInfo( - client, + new ParentTaskAssigningClient(client, parentTaskId), transform.getId(), transform.getTransformState().getCheckpoint(), transform.getTransformState().getPosition(), @@ -317,6 +329,7 @@ private void populateSingleStoppedTransformStat(TransformStoredDoc transform, Ac } private void addCheckpointingInfoForTransformsWithoutTasks( + TaskId parentTaskId, List allStateAndStats, List statsForTransformsWithoutTasks, Set transformsWaitingForAssignment, @@ -333,42 +346,44 @@ private void addCheckpointingInfoForTransformsWithoutTasks( AtomicInteger numberRemaining = new AtomicInteger(statsForTransformsWithoutTasks.size()); AtomicBoolean isExceptionReported = new AtomicBoolean(false); - statsForTransformsWithoutTasks.forEach(stat -> populateSingleStoppedTransformStat(stat, ActionListener.wrap(checkpointingInfo -> { - synchronized (allStateAndStats) { - if (transformsWaitingForAssignment.contains(stat.getId())) { - Assignment assignment = TransformNodes.getAssignment(stat.getId(), clusterState); - allStateAndStats.add( - new TransformStats( - stat.getId(), - TransformStats.State.WAITING, - assignment.getExplanation(), - null, - stat.getTransformStats(), - checkpointingInfo, - TransformHealthChecker.checkUnassignedTransform(stat.getId(), clusterState) - ) - ); - } else { - allStateAndStats.add( - new TransformStats( - stat.getId(), - TransformStats.State.STOPPED, - null, - null, - stat.getTransformStats(), - checkpointingInfo, - TransformHealth.GREEN - ) - ); + statsForTransformsWithoutTasks.forEach( + stat -> populateSingleStoppedTransformStat(stat, parentTaskId, ActionListener.wrap(checkpointingInfo -> { + synchronized (allStateAndStats) { + if (transformsWaitingForAssignment.contains(stat.getId())) { + Assignment assignment = TransformNodes.getAssignment(stat.getId(), clusterState); + allStateAndStats.add( + new TransformStats( + stat.getId(), + TransformStats.State.WAITING, + assignment.getExplanation(), + null, + stat.getTransformStats(), + checkpointingInfo, + TransformHealthChecker.checkUnassignedTransform(stat.getId(), clusterState) + ) + ); + } else { + allStateAndStats.add( + new TransformStats( + stat.getId(), + TransformStats.State.STOPPED, + null, + null, + stat.getTransformStats(), + checkpointingInfo, + TransformHealth.GREEN + ) + ); + } } - } - if (numberRemaining.decrementAndGet() == 0) { - listener.onResponse(null); - } - }, e -> { - if (isExceptionReported.compareAndSet(false, true)) { - listener.onFailure(e); - } - }))); + if (numberRemaining.decrementAndGet() == 0) { + listener.onResponse(null); + } + }, e -> { + if (isExceptionReported.compareAndSet(false, true)) { + listener.onFailure(e); + } + })) + ); } } diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStopTransformAction.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStopTransformAction.java index 13301381491e..5acb3e541039 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStopTransformAction.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStopTransformAction.java @@ -150,6 +150,7 @@ protected void doExecute(Task task, Request request, ActionListener li transformConfigManager.expandTransformIds( request.getId(), new PageParams(0, 10_000), + request.getTimeout(), request.isAllowNoMatch(), ActionListener.wrap(hitsAndIds -> { validateTaskState(state, hitsAndIds.v2().v1(), request.isForce()); diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportUpgradeTransformsAction.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportUpgradeTransformsAction.java index d4db343a1c3c..e8a6a243a3f6 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportUpgradeTransformsAction.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportUpgradeTransformsAction.java @@ -211,7 +211,7 @@ private void recursiveExpandTransformIdsAndUpgrade( TimeValue timeout, ActionListener> listener ) { - transformConfigManager.getAllOutdatedTransformIds(ActionListener.wrap(totalAndIds -> { + transformConfigManager.getAllOutdatedTransformIds(timeout, ActionListener.wrap(totalAndIds -> { // exit quickly if there is nothing to do if (totalAndIds.v2().isEmpty()) { diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/IndexBasedTransformConfigManager.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/IndexBasedTransformConfigManager.java index 7831405e5c8d..5d7e706447f9 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/IndexBasedTransformConfigManager.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/IndexBasedTransformConfigManager.java @@ -37,6 +37,7 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; +import org.elasticsearch.core.TimeValue; import org.elasticsearch.core.Tuple; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.engine.VersionConflictEngineException; @@ -518,6 +519,7 @@ public void getTransformConfigurationForUpdate( public void expandTransformIds( String transformIdsExpression, PageParams pageParams, + TimeValue timeout, boolean allowNoMatch, ActionListener, List>>> foundConfigsListener ) { @@ -533,6 +535,7 @@ public void expandTransformIds( .setFrom(pageParams.getFrom()) .setTrackTotalHits(true) .setSize(pageParams.getSize()) + .setTimeout(timeout) .setQuery(queryBuilder) .request(); @@ -588,13 +591,18 @@ public void expandTransformIds( } @Override - public void getAllTransformIds(ActionListener> listener) { - expandAllTransformIds(false, MAX_RESULTS_WINDOW, ActionListener.wrap(r -> listener.onResponse(r.v2()), listener::onFailure)); + public void getAllTransformIds(TimeValue timeout, ActionListener> listener) { + expandAllTransformIds( + false, + MAX_RESULTS_WINDOW, + timeout, + ActionListener.wrap(r -> listener.onResponse(r.v2()), listener::onFailure) + ); } @Override - public void getAllOutdatedTransformIds(ActionListener>> listener) { - expandAllTransformIds(true, MAX_RESULTS_WINDOW, listener); + public void getAllOutdatedTransformIds(TimeValue timeout, ActionListener>> listener) { + expandAllTransformIds(true, MAX_RESULTS_WINDOW, timeout, listener); } @Override @@ -781,7 +789,11 @@ public void getTransformStoredDoc( } @Override - public void getTransformStoredDocs(Collection transformIds, ActionListener> listener) { + public void getTransformStoredDocs( + Collection transformIds, + TimeValue timeout, + ActionListener> listener + ) { QueryBuilder builder = QueryBuilders.constantScoreQuery( QueryBuilders.boolQuery() .filter(QueryBuilders.termsQuery(TransformField.ID.getPreferredName(), transformIds)) @@ -797,6 +809,7 @@ public void getTransformStoredDocs(Collection transformIds, ActionListen .setQuery(builder) // the limit for getting stats and transforms is 1000, as long as we do not have 10 indices this works .setSize(Math.min(transformIds.size(), 10_000)) + .setTimeout(timeout) .request(); executeAsyncWithOrigin( @@ -904,13 +917,19 @@ private QueryBuilder buildQueryFromTokenizedIds(String[] idTokens, String resour * * @param filterForOutdated if true, only returns outdated ids (after de-duplication) * @param maxResultWindow the max result window size (exposed for testing) + * @param timeout timeout applied to all the spawned requests * @param listener listener to call containing transform ids */ - void expandAllTransformIds(boolean filterForOutdated, int maxResultWindow, ActionListener>> listener) { + void expandAllTransformIds( + boolean filterForOutdated, + int maxResultWindow, + TimeValue timeout, + ActionListener>> listener + ) { PageParams startPage = new PageParams(0, maxResultWindow); Set collectedIds = new HashSet<>(); - recursiveExpandAllTransformIds(collectedIds, 0, filterForOutdated, maxResultWindow, null, startPage, listener); + recursiveExpandAllTransformIds(collectedIds, 0, filterForOutdated, maxResultWindow, null, startPage, timeout, listener); } private void recursiveExpandAllTransformIds( @@ -920,6 +939,7 @@ private void recursiveExpandAllTransformIds( int maxResultWindow, String lastId, PageParams page, + TimeValue timeout, ActionListener>> listener ) { SearchRequest request = client.prepareSearch( @@ -930,6 +950,7 @@ private void recursiveExpandAllTransformIds( .addSort("_index", SortOrder.DESC) .setFrom(page.getFrom()) .setSize(page.getSize()) + .setTimeout(timeout) .setFetchSource(false) .addDocValueField(TransformField.ID.getPreferredName()) .setQuery( @@ -973,6 +994,7 @@ private void recursiveExpandAllTransformIds( maxResultWindow, idOfLastHit, nextPage, + timeout, listener ); return; diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/TransformConfigManager.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/TransformConfigManager.java index 71e748762190..5ed1a7254268 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/TransformConfigManager.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/persistence/TransformConfigManager.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.transform.persistence; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.core.TimeValue; import org.elasticsearch.core.Tuple; import org.elasticsearch.xpack.core.action.util.PageParams; import org.elasticsearch.xpack.core.transform.TransformField; @@ -146,11 +147,13 @@ void getTransformConfigurationForUpdate( * * @param transformIdsExpression The id expression. Can be _all, *, or comma delimited list of simple regex strings * @param pageParams The paging params + * @param timeout The timeout applied to all the spawned requests * @param foundConfigsListener The listener on signal on success or failure */ void expandTransformIds( String transformIdsExpression, PageParams pageParams, + TimeValue timeout, boolean allowNoMatch, ActionListener, List>>> foundConfigsListener ); @@ -158,16 +161,18 @@ void expandTransformIds( /** * Get all transform ids * + * @param timeout The timeout applied to all the spawned requests * @param listener The listener to call with the collected ids */ - void getAllTransformIds(ActionListener> listener); + void getAllTransformIds(TimeValue timeout, ActionListener> listener); /** * Get all transform ids that aren't using the latest index. * + * @param timeout The timeout applied to all the spawned requests * @param listener The listener to call with total number of transforms and the list of transform ids. */ - void getAllOutdatedTransformIds(ActionListener>> listener); + void getAllOutdatedTransformIds(TimeValue timeout, ActionListener>> listener); /** * This deletes documents corresponding to the transform id (e.g. checkpoints). @@ -198,7 +203,7 @@ void getTransformStoredDoc( ActionListener> resultListener ); - void getTransformStoredDocs(Collection transformIds, ActionListener> listener); + void getTransformStoredDocs(Collection transformIds, TimeValue timeout, ActionListener> listener); void refresh(ActionListener listener); } diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/rest/action/RestCatTransformAction.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/rest/action/RestCatTransformAction.java index 9b5964ac1779..db5f7f9799b4 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/rest/action/RestCatTransformAction.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/rest/action/RestCatTransformAction.java @@ -59,7 +59,7 @@ protected RestChannelConsumer doCatRequest(RestRequest restRequest, NodeClient c GetTransformAction.Request request = new GetTransformAction.Request(id); request.setAllowNoResources(restRequest.paramAsBoolean(ALLOW_NO_MATCH.getPreferredName(), true)); - GetTransformStatsAction.Request statsRequest = new GetTransformStatsAction.Request(id); + GetTransformStatsAction.Request statsRequest = new GetTransformStatsAction.Request(id, null); statsRequest.setAllowNoMatch(restRequest.paramAsBoolean(ALLOW_NO_MATCH.getPreferredName(), true)); if (restRequest.hasParam(PageParams.FROM.getPreferredName()) || restRequest.hasParam(PageParams.SIZE.getPreferredName())) { diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/rest/action/RestGetTransformStatsAction.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/rest/action/RestGetTransformStatsAction.java index f929f886199c..1e263a229922 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/rest/action/RestGetTransformStatsAction.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/rest/action/RestGetTransformStatsAction.java @@ -7,9 +7,13 @@ package org.elasticsearch.xpack.transform.rest.action; +import org.elasticsearch.action.support.master.AcknowledgedRequest; +import org.elasticsearch.client.internal.Client; import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.core.TimeValue; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.action.RestCancellableNodeClient; import org.elasticsearch.rest.action.RestToXContentListener; import org.elasticsearch.xpack.core.action.util.PageParams; import org.elasticsearch.xpack.core.transform.TransformField; @@ -31,9 +35,10 @@ public List routes() { } @Override - protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) { + protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient nodeClient) { String id = restRequest.param(TransformField.ID.getPreferredName()); - GetTransformStatsAction.Request request = new GetTransformStatsAction.Request(id); + TimeValue timeout = restRequest.paramAsTime(TransformField.TIMEOUT.getPreferredName(), AcknowledgedRequest.DEFAULT_ACK_TIMEOUT); + GetTransformStatsAction.Request request = new GetTransformStatsAction.Request(id, timeout); request.setAllowNoMatch(restRequest.paramAsBoolean(ALLOW_NO_MATCH.getPreferredName(), true)); if (restRequest.hasParam(PageParams.FROM.getPreferredName()) || restRequest.hasParam(PageParams.SIZE.getPreferredName())) { request.setPageParams( @@ -43,6 +48,7 @@ protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient ) ); } + Client client = new RestCancellableNodeClient(nodeClient, restRequest.getHttpChannel()); return channel -> client.execute(GetTransformStatsAction.INSTANCE, request, new RestToXContentListener<>(channel)); } diff --git a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/InMemoryTransformConfigManager.java b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/InMemoryTransformConfigManager.java index 104ad7c466de..1b9dedf22a80 100644 --- a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/InMemoryTransformConfigManager.java +++ b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/InMemoryTransformConfigManager.java @@ -10,6 +10,7 @@ import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.regex.Regex; +import org.elasticsearch.core.TimeValue; import org.elasticsearch.core.Tuple; import org.elasticsearch.xpack.core.action.util.PageParams; import org.elasticsearch.xpack.core.transform.TransformMessages; @@ -243,6 +244,7 @@ public void getTransformConfigurationForUpdate( public void expandTransformIds( String transformIdsExpression, PageParams pageParams, + TimeValue timeout, boolean allowNoMatch, ActionListener, List>>> foundConfigsListener ) { @@ -359,7 +361,11 @@ public void getTransformStoredDoc( } @Override - public void getTransformStoredDocs(Collection transformIds, ActionListener> listener) { + public void getTransformStoredDocs( + Collection transformIds, + TimeValue timeout, + ActionListener> listener + ) { List docs = new ArrayList<>(); for (String transformId : transformIds) { TransformStoredDoc storedDoc = transformStoredDocs.get(transformId); @@ -381,17 +387,16 @@ public void refresh(ActionListener listener) { } @Override - public void getAllTransformIds(ActionListener> listener) { + public void getAllTransformIds(TimeValue timeout, ActionListener> listener) { Set allIds = new HashSet<>(configs.keySet()); allIds.addAll(oldConfigs.keySet()); listener.onResponse(allIds); } @Override - public void getAllOutdatedTransformIds(ActionListener>> listener) { + public void getAllOutdatedTransformIds(TimeValue timeout, ActionListener>> listener) { Set outdatedIds = new HashSet<>(oldConfigs.keySet()); outdatedIds.removeAll(configs.keySet()); listener.onResponse(new Tuple<>(Long.valueOf(configs.size() + outdatedIds.size()), outdatedIds)); } - } From a41127ca3fa22bb3f9b95381929f0cfb9e7b33e0 Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Tue, 20 Dec 2022 13:11:28 +0100 Subject: [PATCH 312/919] Check GeohexGrid bounds on geopoint using spherical coordinates (#92460) --- docs/changelog/92460.yaml | 5 ++ .../bucket/geogrid/CellIdSource.java | 2 +- .../bucket/geogrid/GeoHashCellIdSource.java | 4 +- .../GeoGridAggAndQueryConsistencyIT.java | 33 ++++++++++- .../bucket/geogrid/GeoHexCellIdSource.java | 59 +++++++++++++------ .../bucket/geogrid/GeoHexAggregatorTests.java | 24 +++----- 6 files changed, 88 insertions(+), 39 deletions(-) create mode 100644 docs/changelog/92460.yaml diff --git a/docs/changelog/92460.yaml b/docs/changelog/92460.yaml new file mode 100644 index 000000000000..4fc47007ea8a --- /dev/null +++ b/docs/changelog/92460.yaml @@ -0,0 +1,5 @@ +pr: 92460 +summary: Check `GeohexGrid` bounds on geopoint using spherical coordinates +area: Geo +type: bug +issues: [] diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/CellIdSource.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/CellIdSource.java index 54a6a7989f02..8f00e65e9d74 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/CellIdSource.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/CellIdSource.java @@ -99,7 +99,7 @@ public final SortedBinaryDocValues bytesValues(LeafReaderContext ctx) { * * This method maybe faster than having to compute the bounding box for each point grid. * */ - protected boolean validPoint(double lon, double lat) { + protected boolean pointInBounds(double lon, double lat) { if (geoBoundingBox.top() > lat && geoBoundingBox.bottom() < lat) { if (crossesDateline) { return geoBoundingBox.left() < lon || geoBoundingBox.right() > lon; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashCellIdSource.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashCellIdSource.java index dfd83085a675..6cb9af156e40 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashCellIdSource.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoHashCellIdSource.java @@ -42,7 +42,7 @@ protected NumericDocValues boundedCellSingleValue(GeoPointValues values, GeoBoun @Override protected boolean advance(org.elasticsearch.common.geo.GeoPoint target) { final String hash = Geohash.stringEncode(target.getLon(), target.getLat(), precision); - if (validPoint(target.getLon(), target.getLat()) || predicate.validHash(hash)) { + if (pointInBounds(target.getLon(), target.getLat()) || predicate.validHash(hash)) { value = Geohash.longEncode(hash); return true; } @@ -69,7 +69,7 @@ protected SortedNumericDocValues boundedCellMultiValues(MultiGeoPointValues valu @Override protected int advanceValue(org.elasticsearch.common.geo.GeoPoint target, int valuesIdx) { final String hash = Geohash.stringEncode(target.getLon(), target.getLat(), precision); - if (validPoint(target.getLon(), target.getLat()) || predicate.validHash(hash)) { + if (pointInBounds(target.getLon(), target.getLat()) || predicate.validHash(hash)) { values[valuesIdx] = Geohash.longEncode(hash); return valuesIdx + 1; } diff --git a/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoGridAggAndQueryConsistencyIT.java b/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoGridAggAndQueryConsistencyIT.java index 43da13e7aa58..1b497b7ee33e 100644 --- a/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoGridAggAndQueryConsistencyIT.java +++ b/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoGridAggAndQueryConsistencyIT.java @@ -11,7 +11,6 @@ import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.common.geo.GeoBoundingBox; import org.elasticsearch.common.geo.GeoPoint; -import org.elasticsearch.common.geo.GeoUtils; import org.elasticsearch.geo.GeometryTestUtils; import org.elasticsearch.geometry.Geometry; import org.elasticsearch.geometry.Point; @@ -125,13 +124,43 @@ private void doTestGeohexGrid(String fieldType, Supplier randomGeometr } return points; }, - h3 -> new Rectangle(GeoUtils.MIN_LON, GeoUtils.MAX_LON, GeoUtils.MAX_LAT, GeoUtils.MAX_LAT), + this::toGeoHexRectangle, GeoHexGridAggregationBuilder::new, (s1, s2) -> new GeoGridQueryBuilder(s1).setGridId(GeoGridQueryBuilder.Grid.GEOHEX, s2), randomGeometriesSupplier ); } + private Rectangle toGeoHexRectangle(String bucketKey) { + final long h3 = H3.stringToH3(bucketKey); + final CellBoundary boundary = H3.h3ToGeoBoundary(h3); + double minLat = Double.POSITIVE_INFINITY; + double minLon = Double.POSITIVE_INFINITY; + double maxLat = Double.NEGATIVE_INFINITY; + double maxLon = Double.NEGATIVE_INFINITY; + for (int i = 0; i < boundary.numPoints(); i++) { + final double boundaryLat = boundary.getLatLon(i).getLatDeg(); + final double boundaryLon = boundary.getLatLon(i).getLonDeg(); + minLon = Math.min(minLon, boundaryLon); + maxLon = Math.max(maxLon, boundaryLon); + minLat = Math.min(minLat, boundaryLat); + maxLat = Math.max(maxLat, boundaryLat); + } + final int resolution = H3.getResolution(h3); + if (H3.geoToH3(90, 0, resolution) == h3) { + // north pole + return new Rectangle(-180d, 180d, 90, minLat); + } else if (H3.geoToH3(-90, 0, resolution) == h3) { + // south pole + return new Rectangle(-180d, 180d, maxLat, -90); + } else if (maxLon - minLon > 180d) { + // crosses dateline + return new Rectangle(maxLon, minLon, maxLat, minLat); + } else { + return new Rectangle(minLon, maxLon, maxLat, minLat); + } + } + private void doTestGrid( int minPrecision, int maxPrecision, diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoHexCellIdSource.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoHexCellIdSource.java index 06507379be98..07c4f253863a 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoHexCellIdSource.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoHexCellIdSource.java @@ -6,11 +6,17 @@ */ package org.elasticsearch.xpack.spatial.search.aggregations.bucket.geogrid; +import org.apache.lucene.geo.GeoUtils; import org.apache.lucene.index.NumericDocValues; import org.apache.lucene.index.SortedNumericDocValues; +import org.apache.lucene.spatial3d.geom.LatLonBounds; +import org.apache.lucene.spatial3d.geom.Plane; +import org.apache.lucene.spatial3d.geom.PlanetModel; +import org.apache.lucene.spatial3d.geom.SidedPlane; import org.elasticsearch.common.geo.GeoBoundingBox; import org.elasticsearch.h3.CellBoundary; import org.elasticsearch.h3.H3; +import org.elasticsearch.h3.LatLng; import org.elasticsearch.index.fielddata.GeoPointValues; import org.elasticsearch.index.fielddata.MultiGeoPointValues; import org.elasticsearch.search.aggregations.bucket.geogrid.CellIdSource; @@ -44,8 +50,9 @@ protected boolean advance(org.elasticsearch.common.geo.GeoPoint target) { final double lat = target.getLat(); final double lon = target.getLon(); final long hex = H3.geoToH3(lat, lon, precision); - // validPoint is a fast check, validHex is slow - if (validPoint(lon, lat) || predicate.validHex(hex)) { + // pointInBounds is a fast check, validHex is slow + if (pointInBounds(lon, lat) || predicate.validHex(hex)) { + assert predicate.validHex(hex) : H3.h3ToString(hex) + " should be valid but it is not"; value = hex; return true; } @@ -75,7 +82,7 @@ protected int advanceValue(org.elasticsearch.common.geo.GeoPoint target, int val final double lon = target.getLon(); final long hex = H3.geoToH3(lat, lon, precision); // validPoint is a fast check, validHex is slow - if (validPoint(lon, lat) || predicate.validHex(hex)) { + if (pointInBounds(lon, lat) || predicate.validHex(hex)) { values[valuesIdx] = hex; return valuesIdx + 1; } @@ -84,6 +91,24 @@ protected int advanceValue(org.elasticsearch.common.geo.GeoPoint target, int val }; } + // package private for testing + static LatLonBounds getGeoBounds(CellBoundary cellBoundary) { + final LatLonBounds bounds = new LatLonBounds(); + org.apache.lucene.spatial3d.geom.GeoPoint start = getGeoPoint(cellBoundary.getLatLon(cellBoundary.numPoints() - 1)); + for (int i = 0; i < cellBoundary.numPoints(); i++) { + final org.apache.lucene.spatial3d.geom.GeoPoint end = getGeoPoint(cellBoundary.getLatLon(i)); + bounds.addPoint(end); + final Plane plane = new Plane(start, end); + bounds.addPlane(PlanetModel.SPHERE, plane, new SidedPlane(start, plane, end), new SidedPlane(end, start, plane)); + start = end; + } + return bounds; + } + + private static org.apache.lucene.spatial3d.geom.GeoPoint getGeoPoint(LatLng latLng) { + return new org.apache.lucene.spatial3d.geom.GeoPoint(PlanetModel.SPHERE, latLng.getLatRad(), latLng.getLonRad()); + } + private static class GeoHexPredicate { private final boolean crossesDateline; @@ -98,25 +123,25 @@ private static class GeoHexPredicate { } public boolean validHex(long hex) { - CellBoundary boundary = H3.h3ToGeoBoundary(hex); - double minLat = Double.POSITIVE_INFINITY; - double minLon = Double.POSITIVE_INFINITY; - double maxLat = Double.NEGATIVE_INFINITY; - double maxLon = Double.NEGATIVE_INFINITY; - for (int i = 0; i < boundary.numPoints(); i++) { - double boundaryLat = boundary.getLatLon(i).getLatDeg(); - double boundaryLon = boundary.getLatLon(i).getLonDeg(); - minLon = Math.min(minLon, boundaryLon); - maxLon = Math.max(maxLon, boundaryLon); - minLat = Math.min(minLat, boundaryLat); - maxLat = Math.max(maxLat, boundaryLat); + final LatLonBounds bounds = getGeoBounds(H3.h3ToGeoBoundary(hex)); + final double minLat = bounds.checkNoBottomLatitudeBound() ? GeoUtils.MIN_LAT_INCL : Math.toDegrees(bounds.getMinLatitude()); + final double maxLat = bounds.checkNoTopLatitudeBound() ? GeoUtils.MAX_LAT_INCL : Math.toDegrees(bounds.getMaxLatitude()); + final double minLon; + final double maxLon; + if (bounds.checkNoLongitudeBound()) { + assert northPoleHex == hex || southPoleHex == hex; + minLon = GeoUtils.MIN_LON_INCL; + maxLon = GeoUtils.MAX_LON_INCL; + } else { + minLon = Math.toDegrees(bounds.getLeftLongitude()); + maxLon = Math.toDegrees(bounds.getRightLongitude()); } if (northPoleHex == hex) { return minLat < bbox.top(); } else if (southPoleHex == hex) { return maxLat > bbox.bottom(); - } else if (maxLon - minLon > 180) { - return intersects(-180, minLon, minLat, maxLat) || intersects(maxLon, 180, minLat, maxLat); + } else if (maxLon < minLon) { + return intersects(-180, maxLon, minLat, maxLat) || intersects(minLon, 180, minLat, maxLat); } else { return intersects(minLon, maxLon, minLat, maxLat); } diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoHexAggregatorTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoHexAggregatorTests.java index 1427da4daf7d..10bea810d589 100644 --- a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoHexAggregatorTests.java +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/search/aggregations/bucket/geogrid/GeoHexAggregatorTests.java @@ -9,13 +9,13 @@ import org.apache.lucene.document.LatLonDocValuesField; import org.apache.lucene.geo.GeoEncodingUtils; import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.spatial3d.geom.LatLonBounds; import org.elasticsearch.common.geo.GeoBoundingBox; import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.GeoUtils; import org.elasticsearch.geo.GeometryTestUtils; import org.elasticsearch.geometry.Point; import org.elasticsearch.geometry.Rectangle; -import org.elasticsearch.h3.CellBoundary; import org.elasticsearch.h3.H3; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.plugins.SearchPlugin; @@ -95,23 +95,13 @@ protected GeoBoundingBox randomBBox() { @Override protected Rectangle getTile(double lng, double lat, int precision) { - CellBoundary boundary = H3.h3ToGeoBoundary(hashAsString(lng, lat, precision)); - double minLat = Double.POSITIVE_INFINITY; - double minLon = Double.POSITIVE_INFINITY; - double maxLat = Double.NEGATIVE_INFINITY; - double maxLon = Double.NEGATIVE_INFINITY; - for (int i = 0; i < boundary.numPoints(); i++) { - double boundaryLat = boundary.getLatLon(i).getLatDeg(); - double boundaryLon = boundary.getLatLon(i).getLonDeg(); - minLon = Math.min(minLon, boundaryLon); - maxLon = Math.max(maxLon, boundaryLon); - minLat = Math.min(minLat, boundaryLat); - maxLat = Math.max(maxLat, boundaryLat); - } - if (maxLon - minLon > 180) { - return new Rectangle(maxLon, minLon, maxLat, minLat); + final LatLonBounds bounds = GeoHexCellIdSource.getGeoBounds(H3.h3ToGeoBoundary(hashAsString(lng, lat, precision))); + final double minLat = bounds.checkNoBottomLatitudeBound() ? -90d : Math.toDegrees(bounds.getMinLatitude()); + final double maxLat = bounds.checkNoTopLatitudeBound() ? 90d : Math.toDegrees(bounds.getMaxLatitude()); + if (bounds.checkNoLongitudeBound()) { + return new Rectangle(-180d, 180d, maxLat, minLat); } else { - return new Rectangle(minLon, maxLon, maxLat, minLat); + return new Rectangle(Math.toDegrees(bounds.getLeftLongitude()), Math.toDegrees(bounds.getRightLongitude()), maxLat, minLat); } } From 3bad02edec3fde530a099b5b4c8a5fe2721c8f35 Mon Sep 17 00:00:00 2001 From: loupipalien Date: Tue, 20 Dec 2022 20:53:25 +0800 Subject: [PATCH 313/919] Fix numOpenOutputs and modCount in ByteSizeCachingDirectory (#92440) --- docs/changelog/92440.yaml | 6 ++++++ .../index/store/ByteSizeCachingDirectory.java | 9 +++++++-- .../store/ByteSizeCachingDirectoryTests.java | 20 +++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 docs/changelog/92440.yaml diff --git a/docs/changelog/92440.yaml b/docs/changelog/92440.yaml new file mode 100644 index 000000000000..aee9363738b7 --- /dev/null +++ b/docs/changelog/92440.yaml @@ -0,0 +1,6 @@ +pr: 92440 +summary: Fix numOpenOutputs and modCount in ByteSizeCachingDirectory +area: Store +type: bug +issues: + - 92434 diff --git a/server/src/main/java/org/elasticsearch/index/store/ByteSizeCachingDirectory.java b/server/src/main/java/org/elasticsearch/index/store/ByteSizeCachingDirectory.java index a97cd3f2e2d7..932cc80f8f63 100644 --- a/server/src/main/java/org/elasticsearch/index/store/ByteSizeCachingDirectory.java +++ b/server/src/main/java/org/elasticsearch/index/store/ByteSizeCachingDirectory.java @@ -128,6 +128,8 @@ private IndexOutput wrapIndexOutput(IndexOutput out) { numOpenOutputs++; } return new FilterIndexOutput(out.toString(), out) { + private boolean closed; + @Override public void writeBytes(byte[] b, int length) throws IOException { // Don't write to atomicXXX here since it might be called in @@ -168,8 +170,11 @@ public void close() throws IOException { super.close(); } finally { synchronized (ByteSizeCachingDirectory.this) { - numOpenOutputs--; - modCount++; + if (closed == false) { + closed = true; + numOpenOutputs--; + modCount++; + } } } } diff --git a/server/src/test/java/org/elasticsearch/index/store/ByteSizeCachingDirectoryTests.java b/server/src/test/java/org/elasticsearch/index/store/ByteSizeCachingDirectoryTests.java index 332563e3e351..34960e58558b 100644 --- a/server/src/test/java/org/elasticsearch/index/store/ByteSizeCachingDirectoryTests.java +++ b/server/src/test/java/org/elasticsearch/index/store/ByteSizeCachingDirectoryTests.java @@ -87,6 +87,26 @@ public void testBasics() throws IOException { assertEquals(18, countingDir.numFileLengthCalls); assertEquals(15, cachingDir.estimateSizeInBytes()); assertEquals(18, countingDir.numFileLengthCalls); + + // Close more than once + IndexOutput out = cachingDir.createOutput("foo", IOContext.DEFAULT); + try { + out.writeBytes(new byte[5], 5); + + cachingDir.estimateSizeInBytes(); + // +3 because there are 3 files + assertEquals(21, countingDir.numFileLengthCalls); + // An index output is open so no caching + cachingDir.estimateSizeInBytes(); + assertEquals(24, countingDir.numFileLengthCalls); + } finally { + out.close(); + assertEquals(20, cachingDir.estimateSizeInBytes()); + assertEquals(27, countingDir.numFileLengthCalls); + } + out.close(); + assertEquals(20, cachingDir.estimateSizeInBytes()); + assertEquals(27, countingDir.numFileLengthCalls); } } From 7b17e1b5dc9f31f3fefc5cbc880bcb709acdd4b9 Mon Sep 17 00:00:00 2001 From: QY Date: Tue, 20 Dec 2022 21:01:11 +0800 Subject: [PATCH 314/919] [DOCS] Remove outdated note in `Date field type` (#92408) Negative epoch timestamps are supported in 8.2.0 by pr #80208 --- docs/reference/mapping/types/date.asciidoc | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/reference/mapping/types/date.asciidoc b/docs/reference/mapping/types/date.asciidoc index e8c21e8f3dc4..4d69ddfd517c 100644 --- a/docs/reference/mapping/types/date.asciidoc +++ b/docs/reference/mapping/types/date.asciidoc @@ -10,9 +10,6 @@ JSON doesn't have a date data type, so dates in Elasticsearch can either be: * a number representing _milliseconds-since-the-epoch_. * a number representing _seconds-since-the-epoch_ (<>). -NOTE: Values for _milliseconds-since-the-epoch_ must be non-negative. Use a -formatted date to represent dates before 1970. - Internally, dates are converted to UTC (if the time-zone is specified) and stored as a long number representing milliseconds-since-the-epoch. From f581f0b5d2ca103f235ebc8817804065acdefbf3 Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Tue, 20 Dec 2022 14:02:33 +0100 Subject: [PATCH 315/919] Speed h3 library by using FastMath implementation for trigonometric functions (#91839) --- docs/changelog/91839.yaml | 5 + libs/h3/NOTICE.txt | 30 + .../java/org/elasticsearch/h3/FastMath.java | 1055 +++++++++++++++++ .../java/org/elasticsearch/h3/LatLng.java | 6 +- .../main/java/org/elasticsearch/h3/Vec2d.java | 24 +- .../main/java/org/elasticsearch/h3/Vec3d.java | 16 +- .../org/elasticsearch/h3/FastMathTests.java | 128 ++ .../h3/ParentChildNavigationTests.java | 4 +- 8 files changed, 1243 insertions(+), 25 deletions(-) create mode 100644 docs/changelog/91839.yaml create mode 100644 libs/h3/src/main/java/org/elasticsearch/h3/FastMath.java create mode 100644 libs/h3/src/test/java/org/elasticsearch/h3/FastMathTests.java diff --git a/docs/changelog/91839.yaml b/docs/changelog/91839.yaml new file mode 100644 index 000000000000..205319b11023 --- /dev/null +++ b/docs/changelog/91839.yaml @@ -0,0 +1,5 @@ +pr: 91839 +summary: Speed h3 library by using `FastMath` implementation for trigonometric functions +area: Geo +type: enhancement +issues: [] diff --git a/libs/h3/NOTICE.txt b/libs/h3/NOTICE.txt index a8e5e1c94c87..7074527ca3fb 100644 --- a/libs/h3/NOTICE.txt +++ b/libs/h3/NOTICE.txt @@ -19,3 +19,33 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +-- +This project contains code sourced from https://github.com/jeffhain/jafama which is licensed under the Apache 2.0 License. + +Copyright 2012 Jeff Hain + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +============================================================================= +Notice of fdlibm package this program is partially derived from: + +Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved. + +Developed at SunSoft, a Sun Microsystems, Inc. business. +Permission to use, copy, modify, and distribute this +software is freely granted, provided that this notice +is preserved. +============================================================================= + + diff --git a/libs/h3/src/main/java/org/elasticsearch/h3/FastMath.java b/libs/h3/src/main/java/org/elasticsearch/h3/FastMath.java new file mode 100644 index 000000000000..61d767901ae0 --- /dev/null +++ b/libs/h3/src/main/java/org/elasticsearch/h3/FastMath.java @@ -0,0 +1,1055 @@ +/* + * @notice + * Copyright 2012 Jeff Hain + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ============================================================================= + * Notice of fdlibm package this program is partially derived from: + * + * Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved. + * + * Developed at SunSoft, a Sun Microsystems, Inc. business. + * Permission to use, copy, modify, and distribute this + * software is freely granted, provided that this notice + * is preserved. + * ============================================================================= + * + * This code sourced from: + * https://github.com/jeffhain/jafama/blob/d7d2a7659e96e148d827acc24cf385b872cda365/src/main/java/net/jafama/FastMath.java + */ + +package org.elasticsearch.h3; + +/** + * This file is forked from https://github.com/jeffhain/jafama. In particular, it forks the following file: + * https://github.com/jeffhain/jafama/blob/master/src/main/java/net/jafama/FastMath.java + * + * It modifies the original implementation by removing not needed methods leaving the following trigonometric function: + *
      + *
    • {@link #cos(double)}
    • + *
    • {@link #sin(double)}
    • + *
    • {@link #tan(double)}
    • + *
    • {@link #acos(double)}
    • + *
    • {@link #asin(double)}
    • + *
    • {@link #atan(double)}
    • + *
    • {@link #atan2(double, double)}
    • + *
    + */ +final class FastMath { + + /* + * For trigonometric functions, use of look-up tables and Taylor-Lagrange formula + * with 4 derivatives (more take longer to compute and don't add much accuracy, + * less require larger tables (which use more memory, take more time to initialize, + * and are slower to access (at least on the machine they were developed on))). + * + * For angles reduction of cos/sin/tan functions: + * - for small values, instead of reducing angles, and then computing the best index + * for look-up tables, we compute this index right away, and use it for reduction, + * - for large values, treatments derived from fdlibm package are used, as done in + * java.lang.Math. They are faster but still "slow", so if you work with + * large numbers and need speed over accuracy for them, you might want to use + * normalizeXXXFast treatments before your function, or modify cos/sin/tan + * so that they call the fast normalization treatments instead of the accurate ones. + * NB: If an angle is huge (like PI*1e20), in double precision format its last digits + * are zeros, which most likely is not the case for the intended value, and doing + * an accurate reduction on a very inaccurate value is most likely pointless. + * But it gives some sort of coherence that could be needed in some cases. + * + * Multiplication on double appears to be about as fast (or not much slower) than call + * to [], and regrouping some doubles in a private class, to use + * index only once, does not seem to speed things up, so: + * - for uniformly tabulated values, to retrieve the parameter corresponding to + * an index, we recompute it rather than using an array to store it, + * - for cos/sin, we recompute derivatives divided by (multiplied by inverse of) + * factorial each time, rather than storing them in arrays. + * + * Lengths of look-up tables are usually of the form 2^n+1, for their values to be + * of the form ( * k/2^n, k in 0 .. 2^n), so that particular values + * (PI/2, etc.) are "exactly" computed, as well as for other reasons. + * + * Most math treatments I could find on the web, including "fast" ones, + * usually take care of special cases (NaN, etc.) at the beginning, and + * then deal with the general case, which adds a useless overhead for the + * general (and common) case. In this class, special cases are only dealt + * with when needed, and if the general case does not already handle them. + */ + + // -------------------------------------------------------------------------- + // GENERAL CONSTANTS + // -------------------------------------------------------------------------- + + private static final double ONE_DIV_F2 = 1 / 2.0; + private static final double ONE_DIV_F3 = 1 / 6.0; + private static final double ONE_DIV_F4 = 1 / 24.0; + + private static final double TWO_POW_24 = Double.longBitsToDouble(0x4170000000000000L); + private static final double TWO_POW_N24 = Double.longBitsToDouble(0x3E70000000000000L); + + private static final double TWO_POW_66 = Double.longBitsToDouble(0x4410000000000000L); + + private static final int MIN_DOUBLE_EXPONENT = -1074; + private static final int MAX_DOUBLE_EXPONENT = 1023; + + // -------------------------------------------------------------------------- + // CONSTANTS FOR NORMALIZATIONS + // -------------------------------------------------------------------------- + + /* + * Table of constants for 1/(2*PI), 282 Hex digits (enough for normalizing doubles). + * 1/(2*PI) approximation = sum of ONE_OVER_TWOPI_TAB[i]*2^(-24*(i+1)). + */ + private static final double[] ONE_OVER_TWOPI_TAB = { + 0x28BE60, + 0xDB9391, + 0x054A7F, + 0x09D5F4, + 0x7D4D37, + 0x7036D8, + 0xA5664F, + 0x10E410, + 0x7F9458, + 0xEAF7AE, + 0xF1586D, + 0xC91B8E, + 0x909374, + 0xB80192, + 0x4BBA82, + 0x746487, + 0x3F877A, + 0xC72C4A, + 0x69CFBA, + 0x208D7D, + 0x4BAED1, + 0x213A67, + 0x1C09AD, + 0x17DF90, + 0x4E6475, + 0x8E60D4, + 0xCE7D27, + 0x2117E2, + 0xEF7E4A, + 0x0EC7FE, + 0x25FFF7, + 0x816603, + 0xFBCBC4, + 0x62D682, + 0x9B47DB, + 0x4D9FB3, + 0xC9F2C2, + 0x6DD3D1, + 0x8FD9A7, + 0x97FA8B, + 0x5D49EE, + 0xB1FAF9, + 0x7C5ECF, + 0x41CE7D, + 0xE294A4, + 0xBA9AFE, + 0xD7EC47 }; + + /* + * Constants for 2*PI. Only the 23 most significant bits of each mantissa are used. + * 2*PI approximation = sum of TWOPI_TAB. + */ + private static final double TWOPI_TAB0 = Double.longBitsToDouble(0x401921FB40000000L); + private static final double TWOPI_TAB1 = Double.longBitsToDouble(0x3E94442D00000000L); + private static final double TWOPI_TAB2 = Double.longBitsToDouble(0x3D18469880000000L); + private static final double TWOPI_TAB3 = Double.longBitsToDouble(0x3B98CC5160000000L); + private static final double TWOPI_TAB4 = Double.longBitsToDouble(0x3A101B8380000000L); + + private static final double INVPIO2 = Double.longBitsToDouble(0x3FE45F306DC9C883L); // 6.36619772367581382433e-01 53 bits of 2/pi + private static final double PIO2_HI = Double.longBitsToDouble(0x3FF921FB54400000L); // 1.57079632673412561417e+00 first 33 bits of pi/2 + private static final double PIO2_LO = Double.longBitsToDouble(0x3DD0B4611A626331L); // 6.07710050650619224932e-11 pi/2 - PIO2_HI + private static final double INVTWOPI = INVPIO2 / 4; + private static final double TWOPI_HI = 4 * PIO2_HI; + private static final double TWOPI_LO = 4 * PIO2_LO; + + // fdlibm uses 2^19*PI/2 here, but we normalize with % 2*PI instead of % PI/2, + // and we can bear some more error. + private static final double NORMALIZE_ANGLE_MAX_MEDIUM_DOUBLE = StrictMath.pow(2, 20) * (2 * Math.PI); + + // -------------------------------------------------------------------------- + // CONSTANTS AND TABLES FOR COS, SIN + // -------------------------------------------------------------------------- + + private static final int SIN_COS_TABS_SIZE = (1 << getTabSizePower(11)) + 1; + private static final double SIN_COS_DELTA_HI = TWOPI_HI / (SIN_COS_TABS_SIZE - 1); + private static final double SIN_COS_DELTA_LO = TWOPI_LO / (SIN_COS_TABS_SIZE - 1); + private static final double SIN_COS_INDEXER = 1 / (SIN_COS_DELTA_HI + SIN_COS_DELTA_LO); + private static final double[] sinTab = new double[SIN_COS_TABS_SIZE]; + private static final double[] cosTab = new double[SIN_COS_TABS_SIZE]; + + // Max abs value for fast modulo, above which we use regular angle normalization. + // This value must be < (Integer.MAX_VALUE / SIN_COS_INDEXER), to stay in range of int type. + // The higher it is, the higher the error, but also the faster it is for lower values. + // If you set it to ((Integer.MAX_VALUE / SIN_COS_INDEXER) * 0.99), worse accuracy on double range is about 1e-10. + private static final double SIN_COS_MAX_VALUE_FOR_INT_MODULO = ((Integer.MAX_VALUE >> 9) / SIN_COS_INDEXER) * 0.99; + + // -------------------------------------------------------------------------- + // CONSTANTS AND TABLES FOR TAN + // -------------------------------------------------------------------------- + + // We use the following formula: + // 1) tan(-x) = -tan(x) + // 2) tan(x) = 1/tan(PI/2-x) + // ---> we only have to compute tan(x) on [0,A] with PI/4<=A= 45deg, and supposed to be >= 51.4deg, as fdlibm code is not + // supposed to work with values inferior to that (51.4deg is about + // (PI/2-Double.longBitsToDouble(0x3FE5942800000000L))). + private static final double TAN_MAX_VALUE_FOR_TABS = Math.toRadians(77.0); + + private static final int TAN_TABS_SIZE = (int) ((TAN_MAX_VALUE_FOR_TABS / (Math.PI / 2)) * (TAN_VIRTUAL_TABS_SIZE - 1)) + 1; + private static final double TAN_DELTA_HI = PIO2_HI / (TAN_VIRTUAL_TABS_SIZE - 1); + private static final double TAN_DELTA_LO = PIO2_LO / (TAN_VIRTUAL_TABS_SIZE - 1); + private static final double TAN_INDEXER = 1 / (TAN_DELTA_HI + TAN_DELTA_LO); + private static final double[] tanTab = new double[TAN_TABS_SIZE]; + private static final double[] tanDer1DivF1Tab = new double[TAN_TABS_SIZE]; + private static final double[] tanDer2DivF2Tab = new double[TAN_TABS_SIZE]; + private static final double[] tanDer3DivF3Tab = new double[TAN_TABS_SIZE]; + private static final double[] tanDer4DivF4Tab = new double[TAN_TABS_SIZE]; + + // Max abs value for fast modulo, above which we use regular angle normalization. + // This value must be < (Integer.MAX_VALUE / TAN_INDEXER), to stay in range of int type. + // The higher it is, the higher the error, but also the faster it is for lower values. + private static final double TAN_MAX_VALUE_FOR_INT_MODULO = (((Integer.MAX_VALUE >> 9) / TAN_INDEXER) * 0.99); + + // -------------------------------------------------------------------------- + // CONSTANTS AND TABLES FOR ACOS, ASIN + // -------------------------------------------------------------------------- + + // We use the following formula: + // 1) acos(x) = PI/2 - asin(x) + // 2) asin(-x) = -asin(x) + // ---> we only have to compute asin(x) on [0,1]. + // For values not close to +-1, we use look-up tables; + // for values near +-1, we use code derived from fdlibm. + + // Supposed to be >= sin(77.2deg), as fdlibm code is supposed to work with values > 0.975, + // but seems to work well enough as long as value >= sin(25deg). + private static final double ASIN_MAX_VALUE_FOR_TABS = StrictMath.sin(Math.toRadians(73.0)); + + private static final int ASIN_TABS_SIZE = (1 << getTabSizePower(13)) + 1; + private static final double ASIN_DELTA = ASIN_MAX_VALUE_FOR_TABS / (ASIN_TABS_SIZE - 1); + private static final double ASIN_INDEXER = 1 / ASIN_DELTA; + private static final double[] asinTab = new double[ASIN_TABS_SIZE]; + private static final double[] asinDer1DivF1Tab = new double[ASIN_TABS_SIZE]; + private static final double[] asinDer2DivF2Tab = new double[ASIN_TABS_SIZE]; + private static final double[] asinDer3DivF3Tab = new double[ASIN_TABS_SIZE]; + private static final double[] asinDer4DivF4Tab = new double[ASIN_TABS_SIZE]; + + private static final double ASIN_MAX_VALUE_FOR_POWTABS = StrictMath.sin(Math.toRadians(88.6)); + private static final int ASIN_POWTABS_POWER = 84; + + private static final double ASIN_POWTABS_ONE_DIV_MAX_VALUE = 1 / ASIN_MAX_VALUE_FOR_POWTABS; + private static final int ASIN_POWTABS_SIZE = (1 << getTabSizePower(12)) + 1; + private static final int ASIN_POWTABS_SIZE_MINUS_ONE = ASIN_POWTABS_SIZE - 1; + private static final double[] asinParamPowTab = new double[ASIN_POWTABS_SIZE]; + private static final double[] asinPowTab = new double[ASIN_POWTABS_SIZE]; + private static final double[] asinDer1DivF1PowTab = new double[ASIN_POWTABS_SIZE]; + private static final double[] asinDer2DivF2PowTab = new double[ASIN_POWTABS_SIZE]; + private static final double[] asinDer3DivF3PowTab = new double[ASIN_POWTABS_SIZE]; + private static final double[] asinDer4DivF4PowTab = new double[ASIN_POWTABS_SIZE]; + + private static final double ASIN_PIO2_HI = Double.longBitsToDouble(0x3FF921FB54442D18L); // 1.57079632679489655800e+00 + private static final double ASIN_PIO2_LO = Double.longBitsToDouble(0x3C91A62633145C07L); // 6.12323399573676603587e-17 + private static final double ASIN_PS0 = Double.longBitsToDouble(0x3fc5555555555555L); // 1.66666666666666657415e-01 + private static final double ASIN_PS1 = Double.longBitsToDouble(0xbfd4d61203eb6f7dL); // -3.25565818622400915405e-01 + private static final double ASIN_PS2 = Double.longBitsToDouble(0x3fc9c1550e884455L); // 2.01212532134862925881e-01 + private static final double ASIN_PS3 = Double.longBitsToDouble(0xbfa48228b5688f3bL); // -4.00555345006794114027e-02 + private static final double ASIN_PS4 = Double.longBitsToDouble(0x3f49efe07501b288L); // 7.91534994289814532176e-04 + private static final double ASIN_PS5 = Double.longBitsToDouble(0x3f023de10dfdf709L); // 3.47933107596021167570e-05 + private static final double ASIN_QS1 = Double.longBitsToDouble(0xc0033a271c8a2d4bL); // -2.40339491173441421878e+00 + private static final double ASIN_QS2 = Double.longBitsToDouble(0x40002ae59c598ac8L); // 2.02094576023350569471e+00 + private static final double ASIN_QS3 = Double.longBitsToDouble(0xbfe6066c1b8d0159L); // -6.88283971605453293030e-01 + private static final double ASIN_QS4 = Double.longBitsToDouble(0x3fb3b8c5b12e9282L); // 7.70381505559019352791e-02 + + // -------------------------------------------------------------------------- + // CONSTANTS AND TABLES FOR ATAN + // -------------------------------------------------------------------------- + + // We use the formula atan(-x) = -atan(x) + // ---> we only have to compute atan(x) on [0,+infinity[. + // For values corresponding to angles not close to +-PI/2, we use look-up tables; + // for values corresponding to angles near +-PI/2, we use code derived from fdlibm. + + // Supposed to be >= tan(67.7deg), as fdlibm code is supposed to work with values > 2.4375. + private static final double ATAN_MAX_VALUE_FOR_TABS = StrictMath.tan(Math.toRadians(74.0)); + + private static final int ATAN_TABS_SIZE = (1 << getTabSizePower(12)) + 1; + private static final double ATAN_DELTA = ATAN_MAX_VALUE_FOR_TABS / (ATAN_TABS_SIZE - 1); + private static final double ATAN_INDEXER = 1 / ATAN_DELTA; + private static final double[] atanTab = new double[ATAN_TABS_SIZE]; + private static final double[] atanDer1DivF1Tab = new double[ATAN_TABS_SIZE]; + private static final double[] atanDer2DivF2Tab = new double[ATAN_TABS_SIZE]; + private static final double[] atanDer3DivF3Tab = new double[ATAN_TABS_SIZE]; + private static final double[] atanDer4DivF4Tab = new double[ATAN_TABS_SIZE]; + + private static final double ATAN_HI3 = Double.longBitsToDouble(0x3ff921fb54442d18L); // 1.57079632679489655800e+00 atan(inf)hi + private static final double ATAN_LO3 = Double.longBitsToDouble(0x3c91a62633145c07L); // 6.12323399573676603587e-17 atan(inf)lo + private static final double ATAN_AT0 = Double.longBitsToDouble(0x3fd555555555550dL); // 3.33333333333329318027e-01 + private static final double ATAN_AT1 = Double.longBitsToDouble(0xbfc999999998ebc4L); // -1.99999999998764832476e-01 + private static final double ATAN_AT2 = Double.longBitsToDouble(0x3fc24924920083ffL); // 1.42857142725034663711e-01 + private static final double ATAN_AT3 = Double.longBitsToDouble(0xbfbc71c6fe231671L); // -1.11111104054623557880e-01 + private static final double ATAN_AT4 = Double.longBitsToDouble(0x3fb745cdc54c206eL); // 9.09088713343650656196e-02 + private static final double ATAN_AT5 = Double.longBitsToDouble(0xbfb3b0f2af749a6dL); // -7.69187620504482999495e-02 + private static final double ATAN_AT6 = Double.longBitsToDouble(0x3fb10d66a0d03d51L); // 6.66107313738753120669e-02 + private static final double ATAN_AT7 = Double.longBitsToDouble(0xbfadde2d52defd9aL); // -5.83357013379057348645e-02 + private static final double ATAN_AT8 = Double.longBitsToDouble(0x3fa97b4b24760debL); // 4.97687799461593236017e-02 + private static final double ATAN_AT9 = Double.longBitsToDouble(0xbfa2b4442c6a6c2fL); // -3.65315727442169155270e-02 + private static final double ATAN_AT10 = Double.longBitsToDouble(0x3f90ad3ae322da11L); // 1.62858201153657823623e-02 + + // -------------------------------------------------------------------------- + // TABLE FOR POWERS OF TWO + // -------------------------------------------------------------------------- + + private static final double[] twoPowTab = new double[(MAX_DOUBLE_EXPONENT - MIN_DOUBLE_EXPONENT) + 1]; + + // -------------------------------------------------------------------------- + // PUBLIC TREATMENTS + // -------------------------------------------------------------------------- + + /** + * @param angle Angle in radians. + * @return Angle cosine. + */ + public static double cos(double angle) { + angle = Math.abs(angle); + if (angle > SIN_COS_MAX_VALUE_FOR_INT_MODULO) { + // Faster than using normalizeZeroTwoPi. + angle = remainderTwoPi(angle); + if (angle < 0.0) { + angle += 2 * Math.PI; + } + } + // index: possibly outside tables range. + int index = (int) (angle * SIN_COS_INDEXER + 0.5); + double delta = (angle - index * SIN_COS_DELTA_HI) - index * SIN_COS_DELTA_LO; + // Making sure index is within tables range. + // Last value of each table is the same than first, so we ignore it (tabs size minus one) for modulo. + index &= (SIN_COS_TABS_SIZE - 2); // index % (SIN_COS_TABS_SIZE-1) + double indexCos = cosTab[index]; + double indexSin = sinTab[index]; + return indexCos + delta * (-indexSin + delta * (-indexCos * ONE_DIV_F2 + delta * (indexSin * ONE_DIV_F3 + delta * indexCos + * ONE_DIV_F4))); + } + + /** + * @param angle Angle in radians. + * @return Angle sine. + */ + public static double sin(double angle) { + boolean negateResult; + if (angle < 0.0) { + angle = -angle; + negateResult = true; + } else { + negateResult = false; + } + if (angle > SIN_COS_MAX_VALUE_FOR_INT_MODULO) { + // Faster than using normalizeZeroTwoPi. + angle = remainderTwoPi(angle); + if (angle < 0.0) { + angle += 2 * Math.PI; + } + } + int index = (int) (angle * SIN_COS_INDEXER + 0.5); + double delta = (angle - index * SIN_COS_DELTA_HI) - index * SIN_COS_DELTA_LO; + index &= (SIN_COS_TABS_SIZE - 2); // index % (SIN_COS_TABS_SIZE-1) + double indexSin = sinTab[index]; + double indexCos = cosTab[index]; + double result = indexSin + delta * (indexCos + delta * (-indexSin * ONE_DIV_F2 + delta * (-indexCos * ONE_DIV_F3 + delta * indexSin + * ONE_DIV_F4))); + return negateResult ? -result : result; + } + + /** + * @param angle Angle in radians. + * @return Angle tangent. + */ + public static double tan(double angle) { + if (Math.abs(angle) > TAN_MAX_VALUE_FOR_INT_MODULO) { + // Faster than using normalizeMinusHalfPiHalfPi. + angle = remainderTwoPi(angle); + if (angle < -Math.PI / 2) { + angle += Math.PI; + } else if (angle > Math.PI / 2) { + angle -= Math.PI; + } + } + boolean negateResult; + if (angle < 0.0) { + angle = -angle; + negateResult = true; + } else { + negateResult = false; + } + int index = (int) (angle * TAN_INDEXER + 0.5); + double delta = (angle - index * TAN_DELTA_HI) - index * TAN_DELTA_LO; + // index modulo PI, i.e. 2*(virtual tab size minus one). + index &= (2 * (TAN_VIRTUAL_TABS_SIZE - 1) - 1); // index % (2*(TAN_VIRTUAL_TABS_SIZE-1)) + // Here, index is in [0,2*(TAN_VIRTUAL_TABS_SIZE-1)-1], i.e. indicates an angle in [0,PI[. + if (index > (TAN_VIRTUAL_TABS_SIZE - 1)) { + index = (2 * (TAN_VIRTUAL_TABS_SIZE - 1)) - index; + delta = -delta; + negateResult = negateResult == false; + } + double result; + if (index < TAN_TABS_SIZE) { + result = tanTab[index] + delta * (tanDer1DivF1Tab[index] + delta * (tanDer2DivF2Tab[index] + delta * (tanDer3DivF3Tab[index] + + delta * tanDer4DivF4Tab[index]))); + } else { // angle in ]TAN_MAX_VALUE_FOR_TABS,TAN_MAX_VALUE_FOR_INT_MODULO], or angle is NaN + // Using tan(angle) == 1/tan(PI/2-angle) formula: changing angle (index and delta), and inverting. + index = (TAN_VIRTUAL_TABS_SIZE - 1) - index; + result = 1 / (tanTab[index] - delta * (tanDer1DivF1Tab[index] - delta * (tanDer2DivF2Tab[index] - delta + * (tanDer3DivF3Tab[index] - delta * tanDer4DivF4Tab[index])))); + } + return negateResult ? -result : result; + } + + /** + * @param value Value in [-1,1]. + * @return Value arccosine, in radians, in [0,PI]. + */ + public static double acos(double value) { + return Math.PI / 2 - FastMath.asin(value); + } + + /** + * @param value Value in [-1,1]. + * @return Value arcsine, in radians, in [-PI/2,PI/2]. + */ + public static double asin(double value) { + boolean negateResult; + if (value < 0.0) { + value = -value; + negateResult = true; + } else { + negateResult = false; + } + if (value <= ASIN_MAX_VALUE_FOR_TABS) { + int index = (int) (value * ASIN_INDEXER + 0.5); + double delta = value - index * ASIN_DELTA; + double result = asinTab[index] + delta * (asinDer1DivF1Tab[index] + delta * (asinDer2DivF2Tab[index] + delta + * (asinDer3DivF3Tab[index] + delta * asinDer4DivF4Tab[index]))); + return negateResult ? -result : result; + } else if (value <= ASIN_MAX_VALUE_FOR_POWTABS) { + int index = (int) (FastMath.powFast(value * ASIN_POWTABS_ONE_DIV_MAX_VALUE, ASIN_POWTABS_POWER) * ASIN_POWTABS_SIZE_MINUS_ONE + + 0.5); + double delta = value - asinParamPowTab[index]; + double result = asinPowTab[index] + delta * (asinDer1DivF1PowTab[index] + delta * (asinDer2DivF2PowTab[index] + delta + * (asinDer3DivF3PowTab[index] + delta * asinDer4DivF4PowTab[index]))); + return negateResult ? -result : result; + } else { // value > ASIN_MAX_VALUE_FOR_TABS, or value is NaN + // This part is derived from fdlibm. + if (value < 1.0) { + double t = (1.0 - value) * 0.5; + double p = t * (ASIN_PS0 + t * (ASIN_PS1 + t * (ASIN_PS2 + t * (ASIN_PS3 + t * (ASIN_PS4 + t * ASIN_PS5))))); + double q = 1.0 + t * (ASIN_QS1 + t * (ASIN_QS2 + t * (ASIN_QS3 + t * ASIN_QS4))); + double s = Math.sqrt(t); + double z = s + s * (p / q); + double result = ASIN_PIO2_HI - ((z + z) - ASIN_PIO2_LO); + return negateResult ? -result : result; + } else { // value >= 1.0, or value is NaN + if (value == 1.0) { + return negateResult ? -Math.PI / 2 : Math.PI / 2; + } else { + return Double.NaN; + } + } + } + } + + /** + * @param value A double value. + * @return Value arctangent, in radians, in [-PI/2,PI/2]. + */ + public static double atan(double value) { + boolean negateResult; + if (value < 0.0) { + value = -value; + negateResult = true; + } else { + negateResult = false; + } + if (value == 1.0) { + // We want "exact" result for 1.0. + return negateResult ? -Math.PI / 4 : Math.PI / 4; + } else if (value <= ATAN_MAX_VALUE_FOR_TABS) { + int index = (int) (value * ATAN_INDEXER + 0.5); + double delta = value - index * ATAN_DELTA; + double result = atanTab[index] + delta * (atanDer1DivF1Tab[index] + delta * (atanDer2DivF2Tab[index] + delta + * (atanDer3DivF3Tab[index] + delta * atanDer4DivF4Tab[index]))); + return negateResult ? -result : result; + } else { // value > ATAN_MAX_VALUE_FOR_TABS, or value is NaN + // This part is derived from fdlibm. + if (value < TWO_POW_66) { + double x = -1 / value; + double x2 = x * x; + double x4 = x2 * x2; + double s1 = x2 * (ATAN_AT0 + x4 * (ATAN_AT2 + x4 * (ATAN_AT4 + x4 * (ATAN_AT6 + x4 * (ATAN_AT8 + x4 * ATAN_AT10))))); + double s2 = x4 * (ATAN_AT1 + x4 * (ATAN_AT3 + x4 * (ATAN_AT5 + x4 * (ATAN_AT7 + x4 * ATAN_AT9)))); + double result = ATAN_HI3 - ((x * (s1 + s2) - ATAN_LO3) - x); + return negateResult ? -result : result; + } else { // value >= 2^66, or value is NaN + if (Double.isNaN(value)) { + return Double.NaN; + } else { + return negateResult ? -Math.PI / 2 : Math.PI / 2; + } + } + } + } + + /** + * For special values for which multiple conventions could be adopted, behaves like Math.atan2(double,double). + * + * @param y Coordinate on y axis. + * @param x Coordinate on x axis. + * @return Angle from x axis positive side to (x,y) position, in radians, in [-PI,PI]. + * Angle measure is positive when going from x axis to y axis (positive sides). + */ + public static double atan2(double y, double x) { + if (x > 0.0) { + if (y == 0.0) { + return (1 / y == Double.NEGATIVE_INFINITY) ? -0.0 : 0.0; + } + if (x == Double.POSITIVE_INFINITY) { + if (y == Double.POSITIVE_INFINITY) { + return Math.PI / 4; + } else if (y == Double.NEGATIVE_INFINITY) { + return -Math.PI / 4; + } else if (y > 0.0) { + return 0.0; + } else if (y < 0.0) { + return -0.0; + } else { + return Double.NaN; + } + } else { + return FastMath.atan(y / x); + } + } else if (x < 0.0) { + if (y == 0.0) { + return (1 / y == Double.NEGATIVE_INFINITY) ? -Math.PI : Math.PI; + } + if (x == Double.NEGATIVE_INFINITY) { + if (y == Double.POSITIVE_INFINITY) { + return 3 * Math.PI / 4; + } else if (y == Double.NEGATIVE_INFINITY) { + return -3 * Math.PI / 4; + } else if (y > 0.0) { + return Math.PI; + } else if (y < 0.0) { + return -Math.PI; + } else { + return Double.NaN; + } + } else if (y > 0.0) { + return Math.PI / 2 + FastMath.atan(-x / y); + } else if (y < 0.0) { + return -Math.PI / 2 - FastMath.atan(x / y); + } else { + return Double.NaN; + } + } else if (x == 0.0) { + if (y == 0.0) { + if (1 / x == Double.NEGATIVE_INFINITY) { + return (1 / y == Double.NEGATIVE_INFINITY) ? -Math.PI : Math.PI; + } else { + return (1 / y == Double.NEGATIVE_INFINITY) ? -0.0 : 0.0; + } + } + if (y > 0.0) { + return Math.PI / 2; + } else if (y < 0.0) { + return -Math.PI / 2; + } else { + return Double.NaN; + } + } else { + return Double.NaN; + } + } + + /** + * This treatment is somehow accurate for low values of |power|, + * and for |power*getExponent(value)| < 1023 or so (to stay away + * from double extreme magnitudes (large and small)). + * + * @param value A double value. + * @param power A power. + * @return value^power. + */ + private static double powFast(double value, int power) { + if (power > 5) { // Most common case first. + double oddRemains = 1.0; + do { + // Test if power is odd. + if ((power & 1) != 0) { + oddRemains *= value; + } + value *= value; + power >>= 1; // power = power / 2 + } while (power > 5); + // Here, power is in [3,5]: faster to finish outside the loop. + if (power == 3) { + return oddRemains * value * value * value; + } else { + double v2 = value * value; + if (power == 4) { + return oddRemains * v2 * v2; + } else { // power == 5 + return oddRemains * v2 * v2 * value; + } + } + } else if (power >= 0) { // power in [0,5] + if (power < 3) { // power in [0,2] + if (power == 2) { // Most common case first. + return value * value; + } else if (power != 0) { // faster than == 1 + return value; + } else { // power == 0 + return 1.0; + } + } else { // power in [3,5] + if (power == 3) { + return value * value * value; + } else { // power in [4,5] + double v2 = value * value; + if (power == 4) { + return v2 * v2; + } else { // power == 5 + return v2 * v2 * value; + } + } + } + } else { // power < 0 + // Opposite of Integer.MIN_VALUE does not exist as int. + if (power == Integer.MIN_VALUE) { + // Integer.MAX_VALUE = -(power+1) + return 1.0 / (FastMath.powFast(value, Integer.MAX_VALUE) * value); + } else { + return 1.0 / FastMath.powFast(value, -power); + } + } + } + + // -------------------------------------------------------------------------- + // PRIVATE TREATMENTS + // -------------------------------------------------------------------------- + + /** + * FastMath is non-instantiable. + */ + private FastMath() {} + + /** + * Use look-up tables size power through this method, + * to make sure is it small in case java.lang.Math + * is directly used. + */ + private static int getTabSizePower(int tabSizePower) { + return tabSizePower; + } + + /** + * Remainder using an accurate definition of PI. + * Derived from a fdlibm treatment called __ieee754_rem_pio2. + * + * This method can return values slightly (like one ULP or so) outside [-Math.PI,Math.PI] range. + * + * @param angle Angle in radians. + * @return Remainder of (angle % (2*PI)), which is in [-PI,PI] range. + */ + private static double remainderTwoPi(double angle) { + boolean negateResult; + if (angle < 0.0) { + negateResult = true; + angle = -angle; + } else { + negateResult = false; + } + if (angle <= NORMALIZE_ANGLE_MAX_MEDIUM_DOUBLE) { + double fn = (double) (int) (angle * INVTWOPI + 0.5); + double result = (angle - fn * TWOPI_HI) - fn * TWOPI_LO; + return negateResult ? -result : result; + } else if (angle < Double.POSITIVE_INFINITY) { + // Reworking exponent to have a value < 2^24. + long lx = Double.doubleToRawLongBits(angle); + long exp = ((lx >> 52) & 0x7FF) - 1046; + double z = Double.longBitsToDouble(lx - (exp << 52)); + + double x0 = (double) ((int) z); + z = (z - x0) * TWO_POW_24; + double x1 = (double) ((int) z); + double x2 = (z - x1) * TWO_POW_24; + + double result = subRemainderTwoPi(x0, x1, x2, (int) exp, (x2 == 0) ? 2 : 3); + return negateResult ? -result : result; + } else { // angle is +infinity or NaN + return Double.NaN; + } + } + + /** + * Remainder using an accurate definition of PI. + * Derived from a fdlibm treatment called __kernel_rem_pio2. + * + * @param x0 Most significant part of the value, as an integer < 2^24, in double precision format. Must be >= 0. + * @param x1 Following significant part of the value, as an integer < 2^24, in double precision format. + * @param x2 Least significant part of the value, as an integer < 2^24, in double precision format. + * @param e0 Exponent of x0 (value is (2^e0)*(x0+(2^-24)*(x1+(2^-24)*x2))). Must be ≥ -20. + * @param nx Number of significant parts to take into account. Must be 2 or 3. + * @return Remainder of (value % (2*PI)), which is in [-PI,PI] range. + */ + private static double subRemainderTwoPi(double x0, double x1, double x2, int e0, int nx) { + int ih; + double z, fw; + double f0, f1, f2, f3, f4, f5, f6 = 0.0, f7; + double q0, q1, q2, q3, q4, q5; + int iq0, iq1, iq2, iq3, iq4; + + final int jx = nx - 1; // jx in [1,2] (nx in [2,3]) + // Could use a table to avoid division, but the gain isn't worth it most likely... + final int jv = (e0 - 3) / 24; // We do not handle the case (e0-3 < -23). + int q = e0 - ((jv << 4) + (jv << 3)) - 24; // e0-24*(jv+1) + + final int j = jv + 4; + if (jx == 1) { + f5 = (j >= 0) ? ONE_OVER_TWOPI_TAB[j] : 0.0; + f4 = (j >= 1) ? ONE_OVER_TWOPI_TAB[j - 1] : 0.0; + f3 = (j >= 2) ? ONE_OVER_TWOPI_TAB[j - 2] : 0.0; + f2 = (j >= 3) ? ONE_OVER_TWOPI_TAB[j - 3] : 0.0; + f1 = (j >= 4) ? ONE_OVER_TWOPI_TAB[j - 4] : 0.0; + f0 = (j >= 5) ? ONE_OVER_TWOPI_TAB[j - 5] : 0.0; + + q0 = x0 * f1 + x1 * f0; + q1 = x0 * f2 + x1 * f1; + q2 = x0 * f3 + x1 * f2; + q3 = x0 * f4 + x1 * f3; + q4 = x0 * f5 + x1 * f4; + } else { // jx == 2 + f6 = (j >= 0) ? ONE_OVER_TWOPI_TAB[j] : 0.0; + f5 = (j >= 1) ? ONE_OVER_TWOPI_TAB[j - 1] : 0.0; + f4 = (j >= 2) ? ONE_OVER_TWOPI_TAB[j - 2] : 0.0; + f3 = (j >= 3) ? ONE_OVER_TWOPI_TAB[j - 3] : 0.0; + f2 = (j >= 4) ? ONE_OVER_TWOPI_TAB[j - 4] : 0.0; + f1 = (j >= 5) ? ONE_OVER_TWOPI_TAB[j - 5] : 0.0; + f0 = (j >= 6) ? ONE_OVER_TWOPI_TAB[j - 6] : 0.0; + + q0 = x0 * f2 + x1 * f1 + x2 * f0; + q1 = x0 * f3 + x1 * f2 + x2 * f1; + q2 = x0 * f4 + x1 * f3 + x2 * f2; + q3 = x0 * f5 + x1 * f4 + x2 * f3; + q4 = x0 * f6 + x1 * f5 + x2 * f4; + } + + z = q4; + fw = (double) ((int) (TWO_POW_N24 * z)); + iq0 = (int) (z - TWO_POW_24 * fw); + z = q3 + fw; + fw = (double) ((int) (TWO_POW_N24 * z)); + iq1 = (int) (z - TWO_POW_24 * fw); + z = q2 + fw; + fw = (double) ((int) (TWO_POW_N24 * z)); + iq2 = (int) (z - TWO_POW_24 * fw); + z = q1 + fw; + fw = (double) ((int) (TWO_POW_N24 * z)); + iq3 = (int) (z - TWO_POW_24 * fw); + z = q0 + fw; + + // Here, q is in [-25,2] range or so, so we can use the table right away. + double twoPowQ = twoPowTab[q - MIN_DOUBLE_EXPONENT]; + + z = (z * twoPowQ) % 8.0; + z -= (double) ((int) z); + if (q > 0) { + iq3 &= 0xFFFFFF >> q; + ih = iq3 >> (23 - q); + } else if (q == 0) { + ih = iq3 >> 23; + } else if (z >= 0.5) { + ih = 2; + } else { + ih = 0; + } + if (ih > 0) { + int carry; + if (iq0 != 0) { + carry = 1; + iq0 = 0x1000000 - iq0; + iq1 = 0x0FFFFFF - iq1; + iq2 = 0x0FFFFFF - iq2; + iq3 = 0x0FFFFFF - iq3; + } else { + if (iq1 != 0) { + carry = 1; + iq1 = 0x1000000 - iq1; + iq2 = 0x0FFFFFF - iq2; + iq3 = 0x0FFFFFF - iq3; + } else { + if (iq2 != 0) { + carry = 1; + iq2 = 0x1000000 - iq2; + iq3 = 0x0FFFFFF - iq3; + } else { + if (iq3 != 0) { + carry = 1; + iq3 = 0x1000000 - iq3; + } else { + carry = 0; + } + } + } + } + if (q > 0) { + switch (q) { + case 1 -> iq3 &= 0x7FFFFF; + case 2 -> iq3 &= 0x3FFFFF; + } + } + if (ih == 2) { + z = 1.0 - z; + if (carry != 0) { + z -= twoPowQ; + } + } + } + + if (z == 0.0) { + if (jx == 1) { + f6 = ONE_OVER_TWOPI_TAB[jv + 5]; + q5 = x0 * f6 + x1 * f5; + } else { // jx == 2 + f7 = ONE_OVER_TWOPI_TAB[jv + 5]; + q5 = x0 * f7 + x1 * f6 + x2 * f5; + } + + z = q5; + fw = (double) ((int) (TWO_POW_N24 * z)); + iq0 = (int) (z - TWO_POW_24 * fw); + z = q4 + fw; + fw = (double) ((int) (TWO_POW_N24 * z)); + iq1 = (int) (z - TWO_POW_24 * fw); + z = q3 + fw; + fw = (double) ((int) (TWO_POW_N24 * z)); + iq2 = (int) (z - TWO_POW_24 * fw); + z = q2 + fw; + fw = (double) ((int) (TWO_POW_N24 * z)); + iq3 = (int) (z - TWO_POW_24 * fw); + z = q1 + fw; + fw = (double) ((int) (TWO_POW_N24 * z)); + iq4 = (int) (z - TWO_POW_24 * fw); + z = q0 + fw; + + z = (z * twoPowQ) % 8.0; + z -= (double) ((int) z); + if (q > 0) { + // some parentheses for Eclipse formatter's weaknesses with bits shifts + iq4 &= (0xFFFFFF >> q); + ih = (iq4 >> (23 - q)); + } else if (q == 0) { + ih = iq4 >> 23; + } else if (z >= 0.5) { + ih = 2; + } else { + ih = 0; + } + if (ih > 0) { + if (iq0 != 0) { + iq0 = 0x1000000 - iq0; + iq1 = 0x0FFFFFF - iq1; + iq2 = 0x0FFFFFF - iq2; + iq3 = 0x0FFFFFF - iq3; + iq4 = 0x0FFFFFF - iq4; + } else { + if (iq1 != 0) { + iq1 = 0x1000000 - iq1; + iq2 = 0x0FFFFFF - iq2; + iq3 = 0x0FFFFFF - iq3; + iq4 = 0x0FFFFFF - iq4; + } else { + if (iq2 != 0) { + iq2 = 0x1000000 - iq2; + iq3 = 0x0FFFFFF - iq3; + iq4 = 0x0FFFFFF - iq4; + } else { + if (iq3 != 0) { + iq3 = 0x1000000 - iq3; + iq4 = 0x0FFFFFF - iq4; + } else { + if (iq4 != 0) { + iq4 = 0x1000000 - iq4; + } + } + } + } + } + if (q > 0) { + switch (q) { + case 1 -> iq4 &= 0x7FFFFF; + case 2 -> iq4 &= 0x3FFFFF; + } + } + } + fw = twoPowQ * TWO_POW_N24; // q -= 24, so initializing fw with ((2^q)*(2^-24)=2^(q-24)) + } else { + // Here, q is in [-25,-2] range or so, so we could use twoPow's table right away with + // iq4 = (int)(z*twoPowTab[-q-TWO_POW_TAB_MIN_POW]); + // but tests show using division is faster... + iq4 = (int) (z / twoPowQ); + fw = twoPowQ; + } + + q4 = fw * (double) iq4; + fw *= TWO_POW_N24; + q3 = fw * (double) iq3; + fw *= TWO_POW_N24; + q2 = fw * (double) iq2; + fw *= TWO_POW_N24; + q1 = fw * (double) iq1; + fw *= TWO_POW_N24; + q0 = fw * (double) iq0; + fw *= TWO_POW_N24; + + fw = TWOPI_TAB0 * q4; + fw += TWOPI_TAB0 * q3 + TWOPI_TAB1 * q4; + fw += TWOPI_TAB0 * q2 + TWOPI_TAB1 * q3 + TWOPI_TAB2 * q4; + fw += TWOPI_TAB0 * q1 + TWOPI_TAB1 * q2 + TWOPI_TAB2 * q3 + TWOPI_TAB3 * q4; + fw += TWOPI_TAB0 * q0 + TWOPI_TAB1 * q1 + TWOPI_TAB2 * q2 + TWOPI_TAB3 * q3 + TWOPI_TAB4 * q4; + + return (ih == 0) ? fw : -fw; + } + + // -------------------------------------------------------------------------- + // STATIC INITIALIZATIONS + // -------------------------------------------------------------------------- + + /** + * Initializes look-up tables. + * + * Might use some FastMath methods in there, not to spend + * an hour in it, but must take care not to use methods + * which look-up tables have not yet been initialized, + * or that are not accurate enough. + */ + static { + + // sin and cos + + final int SIN_COS_PI_INDEX = (SIN_COS_TABS_SIZE - 1) / 2; + final int SIN_COS_PI_MUL_2_INDEX = 2 * SIN_COS_PI_INDEX; + final int SIN_COS_PI_MUL_0_5_INDEX = SIN_COS_PI_INDEX / 2; + final int SIN_COS_PI_MUL_1_5_INDEX = 3 * SIN_COS_PI_INDEX / 2; + for (int i = 0; i < SIN_COS_TABS_SIZE; i++) { + // angle: in [0,2*PI]. + double angle = i * SIN_COS_DELTA_HI + i * SIN_COS_DELTA_LO; + double sinAngle = StrictMath.sin(angle); + double cosAngle = StrictMath.cos(angle); + // For indexes corresponding to null cosine or sine, we make sure the value is zero + // and not an epsilon. This allows for a much better accuracy for results close to zero. + if (i == SIN_COS_PI_INDEX) { + sinAngle = 0.0; + } else if (i == SIN_COS_PI_MUL_2_INDEX) { + sinAngle = 0.0; + } else if (i == SIN_COS_PI_MUL_0_5_INDEX) { + cosAngle = 0.0; + } else if (i == SIN_COS_PI_MUL_1_5_INDEX) { + cosAngle = 0.0; + } + sinTab[i] = sinAngle; + cosTab[i] = cosAngle; + } + + // tan + + for (int i = 0; i < TAN_TABS_SIZE; i++) { + // angle: in [0,TAN_MAX_VALUE_FOR_TABS]. + double angle = i * TAN_DELTA_HI + i * TAN_DELTA_LO; + tanTab[i] = StrictMath.tan(angle); + double cosAngle = StrictMath.cos(angle); + double sinAngle = StrictMath.sin(angle); + double cosAngleInv = 1 / cosAngle; + double cosAngleInv2 = cosAngleInv * cosAngleInv; + double cosAngleInv3 = cosAngleInv2 * cosAngleInv; + double cosAngleInv4 = cosAngleInv2 * cosAngleInv2; + double cosAngleInv5 = cosAngleInv3 * cosAngleInv2; + tanDer1DivF1Tab[i] = cosAngleInv2; + tanDer2DivF2Tab[i] = ((2 * sinAngle) * cosAngleInv3) * ONE_DIV_F2; + tanDer3DivF3Tab[i] = ((2 * (1 + 2 * sinAngle * sinAngle)) * cosAngleInv4) * ONE_DIV_F3; + tanDer4DivF4Tab[i] = ((8 * sinAngle * (2 + sinAngle * sinAngle)) * cosAngleInv5) * ONE_DIV_F4; + } + + // asin + + for (int i = 0; i < ASIN_TABS_SIZE; i++) { + // x: in [0,ASIN_MAX_VALUE_FOR_TABS]. + double x = i * ASIN_DELTA; + asinTab[i] = StrictMath.asin(x); + double oneMinusXSqInv = 1.0 / (1 - x * x); + double oneMinusXSqInv0_5 = StrictMath.sqrt(oneMinusXSqInv); + double oneMinusXSqInv1_5 = oneMinusXSqInv0_5 * oneMinusXSqInv; + double oneMinusXSqInv2_5 = oneMinusXSqInv1_5 * oneMinusXSqInv; + double oneMinusXSqInv3_5 = oneMinusXSqInv2_5 * oneMinusXSqInv; + asinDer1DivF1Tab[i] = oneMinusXSqInv0_5; + asinDer2DivF2Tab[i] = (x * oneMinusXSqInv1_5) * ONE_DIV_F2; + asinDer3DivF3Tab[i] = ((1 + 2 * x * x) * oneMinusXSqInv2_5) * ONE_DIV_F3; + asinDer4DivF4Tab[i] = ((5 + 2 * x * (2 + x * (5 - 2 * x))) * oneMinusXSqInv3_5) * ONE_DIV_F4; + } + + for (int i = 0; i < ASIN_POWTABS_SIZE; i++) { + // x: in [0,ASIN_MAX_VALUE_FOR_POWTABS]. + double x = StrictMath.pow(i * (1.0 / ASIN_POWTABS_SIZE_MINUS_ONE), 1.0 / ASIN_POWTABS_POWER) * ASIN_MAX_VALUE_FOR_POWTABS; + asinParamPowTab[i] = x; + asinPowTab[i] = StrictMath.asin(x); + double oneMinusXSqInv = 1.0 / (1 - x * x); + double oneMinusXSqInv0_5 = StrictMath.sqrt(oneMinusXSqInv); + double oneMinusXSqInv1_5 = oneMinusXSqInv0_5 * oneMinusXSqInv; + double oneMinusXSqInv2_5 = oneMinusXSqInv1_5 * oneMinusXSqInv; + double oneMinusXSqInv3_5 = oneMinusXSqInv2_5 * oneMinusXSqInv; + asinDer1DivF1PowTab[i] = oneMinusXSqInv0_5; + asinDer2DivF2PowTab[i] = (x * oneMinusXSqInv1_5) * ONE_DIV_F2; + asinDer3DivF3PowTab[i] = ((1 + 2 * x * x) * oneMinusXSqInv2_5) * ONE_DIV_F3; + asinDer4DivF4PowTab[i] = ((5 + 2 * x * (2 + x * (5 - 2 * x))) * oneMinusXSqInv3_5) * ONE_DIV_F4; + } + + // atan + + for (int i = 0; i < ATAN_TABS_SIZE; i++) { + // x: in [0,ATAN_MAX_VALUE_FOR_TABS]. + double x = i * ATAN_DELTA; + double onePlusXSqInv = 1.0 / (1 + x * x); + double onePlusXSqInv2 = onePlusXSqInv * onePlusXSqInv; + double onePlusXSqInv3 = onePlusXSqInv2 * onePlusXSqInv; + double onePlusXSqInv4 = onePlusXSqInv2 * onePlusXSqInv2; + atanTab[i] = StrictMath.atan(x); + atanDer1DivF1Tab[i] = onePlusXSqInv; + atanDer2DivF2Tab[i] = (-2 * x * onePlusXSqInv2) * ONE_DIV_F2; + atanDer3DivF3Tab[i] = ((-2 + 6 * x * x) * onePlusXSqInv3) * ONE_DIV_F3; + atanDer4DivF4Tab[i] = ((24 * x * (1 - x * x)) * onePlusXSqInv4) * ONE_DIV_F4; + } + + // twoPow + + for (int i = MIN_DOUBLE_EXPONENT; i <= MAX_DOUBLE_EXPONENT; i++) { + twoPowTab[i - MIN_DOUBLE_EXPONENT] = StrictMath.pow(2.0, i); + } + } +} diff --git a/libs/h3/src/main/java/org/elasticsearch/h3/LatLng.java b/libs/h3/src/main/java/org/elasticsearch/h3/LatLng.java index 638498da72a1..5371ab2af539 100644 --- a/libs/h3/src/main/java/org/elasticsearch/h3/LatLng.java +++ b/libs/h3/src/main/java/org/elasticsearch/h3/LatLng.java @@ -62,9 +62,9 @@ public double getLonDeg() { * @return The azimuth in radians. */ double geoAzimuthRads(double lat, double lon) { - return Math.atan2( - Math.cos(lat) * Math.sin(lon - this.lon), - Math.cos(this.lat) * Math.sin(lat) - Math.sin(this.lat) * Math.cos(lat) * Math.cos(lon - this.lon) + return FastMath.atan2( + FastMath.cos(lat) * FastMath.sin(lon - this.lon), + FastMath.cos(this.lat) * FastMath.sin(lat) - FastMath.sin(this.lat) * FastMath.cos(lat) * FastMath.cos(lon - this.lon) ); } } diff --git a/libs/h3/src/main/java/org/elasticsearch/h3/Vec2d.java b/libs/h3/src/main/java/org/elasticsearch/h3/Vec2d.java index 0ee09604a986..5dc7fd91a0fb 100644 --- a/libs/h3/src/main/java/org/elasticsearch/h3/Vec2d.java +++ b/libs/h3/src/main/java/org/elasticsearch/h3/Vec2d.java @@ -126,7 +126,7 @@ public LatLng hex2dToGeo(int face, int res, boolean substrate) { return faceCenterGeo[face]; } - double theta = Math.atan2(y, x); + double theta = FastMath.atan2(y, x); // scale for current resolution length u for (int i = 0; i < res; i++) { @@ -144,7 +144,7 @@ public LatLng hex2dToGeo(int face, int res, boolean substrate) { r *= Constants.RES0_U_GNOMONIC; // perform inverse gnomonic scaling of r - r = Math.atan(r); + r = FastMath.atan(r); // adjust theta for Class III // if a substrate grid, then it's already been adjusted for Class III @@ -358,18 +358,18 @@ private static LatLng geoAzDistanceRads(LatLng p1, double az, double distance) { lon = constrainLng(p1.getLonRad()); } } else { // not due north or south - final double sinDistance = Math.sin(distance); - final double cosDistance = Math.cos(distance); - final double sinP1Lat = Math.sin(p1.getLatRad()); - final double cosP1Lat = Math.cos(p1.getLatRad()); - sinlat = sinP1Lat * cosDistance + cosP1Lat * sinDistance * Math.cos(az); + final double sinDistance = FastMath.sin(distance); + final double cosDistance = FastMath.cos(distance); + final double sinP1Lat = FastMath.sin(p1.getLatRad()); + final double cosP1Lat = FastMath.cos(p1.getLatRad()); + sinlat = sinP1Lat * cosDistance + cosP1Lat * sinDistance * FastMath.cos(az); if (sinlat > 1.0) { sinlat = 1.0; } if (sinlat < -1.0) { sinlat = -1.0; } - lat = Math.asin(sinlat); + lat = FastMath.asin(sinlat); if (Math.abs(lat - M_PI_2) < Constants.EPSILON) // north pole { lat = M_PI_2; @@ -379,9 +379,9 @@ private static LatLng geoAzDistanceRads(LatLng p1, double az, double distance) { lat = -M_PI_2; lon = 0.0; } else { - final double cosLat = Math.cos(lat); - sinlng = Math.sin(az) * sinDistance / cosLat; - coslng = (cosDistance - sinP1Lat * Math.sin(lat)) / cosP1Lat / cosLat; + final double cosLat = FastMath.cos(lat); + sinlng = FastMath.sin(az) * sinDistance / cosLat; + coslng = (cosDistance - sinP1Lat * FastMath.sin(lat)) / cosP1Lat / cosLat; if (sinlng > 1.0) { sinlng = 1.0; } @@ -394,7 +394,7 @@ private static LatLng geoAzDistanceRads(LatLng p1, double az, double distance) { if (coslng < -1.0) { coslng = -1.0; } - lon = constrainLng(p1.getLonRad() + Math.atan2(sinlng, coslng)); + lon = constrainLng(p1.getLonRad() + FastMath.atan2(sinlng, coslng)); } } return new LatLng(lat, lon); diff --git a/libs/h3/src/main/java/org/elasticsearch/h3/Vec3d.java b/libs/h3/src/main/java/org/elasticsearch/h3/Vec3d.java index 814f0afa8d78..a05abec44519 100644 --- a/libs/h3/src/main/java/org/elasticsearch/h3/Vec3d.java +++ b/libs/h3/src/main/java/org/elasticsearch/h3/Vec3d.java @@ -81,10 +81,10 @@ private double pointSquareDist(double x, double y, double z) { * @return The H3 index. */ static long geoToH3(int res, double lat, double lon) { - final double cosLat = Math.cos(lat); - final double z = Math.sin(lat); - final double x = Math.cos(lon) * cosLat; - final double y = Math.sin(lon) * cosLat; + final double cosLat = FastMath.cos(lat); + final double z = FastMath.sin(lat); + final double x = FastMath.cos(lon) * cosLat; + final double y = FastMath.sin(lon) * cosLat; // determine the icosahedron face int face = 0; double sqd = Vec3d.faceCenterPoint[0].pointSquareDist(x, y, z); @@ -96,7 +96,7 @@ static long geoToH3(int res, double lat, double lon) { } } // cos(r) = 1 - 2 * sin^2(r/2) = 1 - 2 * (sqd / 4) = 1 - sqd/2 - double r = Math.acos(1 - sqd / 2); + double r = FastMath.acos(1 - sqd / 2); if (r < Constants.EPSILON) { return FaceIJK.faceIjkToH3(res, face, new CoordIJK(0, 0, 0)); @@ -113,7 +113,7 @@ static long geoToH3(int res, double lat, double lon) { } // perform gnomonic scaling of r - r = Math.tan(r); + r = FastMath.tan(r); // scale for current resolution length u r /= Constants.RES0_U_GNOMONIC; @@ -122,7 +122,7 @@ static long geoToH3(int res, double lat, double lon) { } // we now have (r, theta) in hex2d with theta ccw from x-axes // convert to face and centered IJK coordinates - return FaceIJK.faceIjkToH3(res, face, Vec2d.hex2dToCoordIJK(r * Math.cos(theta), r * Math.sin(theta))); + return FaceIJK.faceIjkToH3(res, face, Vec2d.hex2dToCoordIJK(r * FastMath.cos(theta), r * FastMath.sin(theta))); } /** @@ -164,7 +164,7 @@ private static double square(double x) { final double c1c2Z = c1X * c2Y - c1Y * c2X; final double sign = Math.signum(dotProduct(this.x, this.y, this.z, c1c2X, c1c2Y, c1c2Z)); - return Math.atan2(sign * magnitude(c1c2X, c1c2Y, c1c2Z), dotProduct(c1X, c1Y, c1Z, c2X, c2Y, c2Z)); + return FastMath.atan2(sign * magnitude(c1c2X, c1c2Y, c1c2Z), dotProduct(c1X, c1Y, c1Z, c2X, c2Y, c2Z)); } /** diff --git a/libs/h3/src/test/java/org/elasticsearch/h3/FastMathTests.java b/libs/h3/src/test/java/org/elasticsearch/h3/FastMathTests.java new file mode 100644 index 000000000000..76b15188a182 --- /dev/null +++ b/libs/h3/src/test/java/org/elasticsearch/h3/FastMathTests.java @@ -0,0 +1,128 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.h3; + +import org.elasticsearch.test.ESTestCase; + +import java.util.function.DoubleSupplier; +import java.util.function.DoubleUnaryOperator; + +public class FastMathTests extends ESTestCase { + + // accuracy for cos(x) + static double COS_DELTA = 1E-15; + // accuracy for sin(x) + static double SIN_DELTA = 1E-15; + // accuracy for asin(x) + static double ASIN_DELTA = 1E-14; + // accuracy for acos(x) + static double ACOS_DELTA = 1E-14; + // accuracy for tan(x) + static double TAN_DELTA = 1E-14; + // accuracy for atan(x) + static double ATAN_DELTA = 1E-14; + // accuracy for atan2(x) + static double ATAN2_DELTA = 1E-14; + + public void testSin() { + doTest(Math::sin, FastMath::sin, d -> SIN_DELTA, () -> randomDoubleBetween(-2 * Math.PI, 2 * Math.PI, true)); + } + + public void testCos() { + doTest(Math::cos, FastMath::cos, d -> COS_DELTA, () -> randomDoubleBetween(-2 * Math.PI, 2 * Math.PI, true)); + } + + public void testTan() { + doTest( + Math::tan, + FastMath::tan, + d -> Math.max(TAN_DELTA, Math.abs(Math.tan(d)) * TAN_DELTA), + () -> randomDoubleBetween(-2 * Math.PI, 2 * Math.PI, true) + ); + } + + public void testAsin() { + doTest(Math::asin, FastMath::asin, d -> ASIN_DELTA, () -> randomDoubleBetween(-2, 2, true)); + } + + public void testAcos() { + doTest(Math::acos, FastMath::acos, d -> ACOS_DELTA, () -> randomDoubleBetween(-2, 2, true)); + } + + public void testAtan() { + doTest( + Math::atan, + FastMath::atan, + d -> ATAN_DELTA, + () -> randomDoubleBetween(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, true) + ); + } + + private void doTest(DoubleUnaryOperator expected, DoubleUnaryOperator actual, DoubleUnaryOperator delta, DoubleSupplier supplier) { + assertEquals(expected.applyAsDouble(Double.NaN), actual.applyAsDouble(Double.NaN), delta.applyAsDouble(Double.NaN)); + assertEquals( + expected.applyAsDouble(Double.NEGATIVE_INFINITY), + actual.applyAsDouble(Double.NEGATIVE_INFINITY), + delta.applyAsDouble(Double.POSITIVE_INFINITY) + ); + assertEquals( + expected.applyAsDouble(Double.POSITIVE_INFINITY), + actual.applyAsDouble(Double.POSITIVE_INFINITY), + delta.applyAsDouble(Double.POSITIVE_INFINITY) + ); + assertEquals( + expected.applyAsDouble(Double.MAX_VALUE), + actual.applyAsDouble(Double.MAX_VALUE), + delta.applyAsDouble(Double.MAX_VALUE) + ); + assertEquals( + expected.applyAsDouble(Double.MIN_VALUE), + actual.applyAsDouble(Double.MIN_VALUE), + delta.applyAsDouble(Double.MIN_VALUE) + ); + assertEquals(expected.applyAsDouble(0), actual.applyAsDouble(0), delta.applyAsDouble(0)); + for (int i = 0; i < 10000; i++) { + double d = supplier.getAsDouble(); + assertEquals(expected.applyAsDouble(d), actual.applyAsDouble(d), delta.applyAsDouble(d)); + } + } + + public void testAtan2() { + assertEquals(Math.atan2(Double.NaN, Double.NaN), FastMath.atan2(Double.NaN, Double.NaN), ATAN2_DELTA); + assertEquals( + Math.atan2(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY), + FastMath.atan2(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY), + ATAN2_DELTA + ); + assertEquals( + Math.atan2(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY), + FastMath.atan2(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY), + ATAN2_DELTA + ); + assertEquals(Math.atan2(Double.MAX_VALUE, Double.MAX_VALUE), FastMath.atan2(Double.MAX_VALUE, Double.MAX_VALUE), ATAN2_DELTA); + assertEquals(Math.atan2(Double.MIN_VALUE, Double.MIN_VALUE), FastMath.atan2(Double.MIN_VALUE, Double.MIN_VALUE), ATAN2_DELTA); + assertEquals(Math.atan2(0, 0), FastMath.atan2(0, 0), ATAN2_DELTA); + for (int i = 0; i < 10000; i++) { + double x = randomDoubleBetween(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, true); + double y = randomDoubleBetween(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, true); + assertEquals(Math.atan2(x, y), FastMath.atan2(x, y), ATAN2_DELTA); + } + } +} diff --git a/libs/h3/src/test/java/org/elasticsearch/h3/ParentChildNavigationTests.java b/libs/h3/src/test/java/org/elasticsearch/h3/ParentChildNavigationTests.java index 2ceae7c794a7..730d93a6f54e 100644 --- a/libs/h3/src/test/java/org/elasticsearch/h3/ParentChildNavigationTests.java +++ b/libs/h3/src/test/java/org/elasticsearch/h3/ParentChildNavigationTests.java @@ -142,8 +142,8 @@ public void testNoChildrenIntersecting() { } public void testIssue91915() { - GeoPolygon polygon1 = getGeoPolygon("8cc373cb54069ff"); - GeoPolygon polygon2 = getGeoPolygon("8cc373cb54065ff"); + GeoPolygon polygon1 = getGeoPolygon("8ec82ea0650155f"); + GeoPolygon polygon2 = getGeoPolygon("8ec82ea06501447"); // these polygons are disjoint but due to https://github.com/apache/lucene/issues/11883 // they are reported as intersects. Once this is fixed this test will fail, we should adjust // testNoChildrenIntersecting From 91701130363fd2270bd1c1fe593d2075f3a727b0 Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Tue, 20 Dec 2022 13:39:41 +0000 Subject: [PATCH 316/919] [HealthAPI] Add support for the FEATURE_STATE affected resource (#92296) The `shards_availability` indicator diagnoses the condition where indices need to be restored from snapshot. Starting with 8.0 using feature_states when restoring from snapshot is mandatory. This adds support for the `FEATURE_STATE` affected resource to aid with building up the snapshot restore API call (which will need to include all the indices and feature states reported by the restore-from-snapshot diagnosis). Note that the health API will not report any indices that are part of a feature state. --- ...sAvailabilityHealthIndicatorBenchmark.java | 4 +- docs/changelog/92296.yaml | 6 + .../data/restore-from-snapshot.asciidoc | 30 +++ ...rdsAvailabilityHealthIndicatorService.java | 129 ++++++++-- .../org/elasticsearch/health/Diagnosis.java | 1 + .../java/org/elasticsearch/node/Node.java | 13 +- ...ailabilityHealthIndicatorServiceTests.java | 241 +++++++++++++++++- 7 files changed, 387 insertions(+), 37 deletions(-) create mode 100644 docs/changelog/92296.yaml diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/routing/allocation/ShardsAvailabilityHealthIndicatorBenchmark.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/routing/allocation/ShardsAvailabilityHealthIndicatorBenchmark.java index 20e8106d93c0..d402ad47fbd0 100644 --- a/benchmarks/src/main/java/org/elasticsearch/benchmark/routing/allocation/ShardsAvailabilityHealthIndicatorBenchmark.java +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/routing/allocation/ShardsAvailabilityHealthIndicatorBenchmark.java @@ -29,6 +29,7 @@ import org.elasticsearch.health.HealthIndicatorResult; import org.elasticsearch.health.node.HealthInfo; import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.indices.SystemIndices; import org.elasticsearch.tasks.TaskManager; import org.elasticsearch.threadpool.ThreadPool; import org.openjdk.jmh.annotations.Benchmark; @@ -45,6 +46,7 @@ import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -171,7 +173,7 @@ public void setUp() throws Exception { new TaskManager(Settings.EMPTY, threadPool, Collections.emptySet()) ); clusterService.getClusterApplierService().setInitialState(initialClusterState); - indicatorService = new ShardsAvailabilityHealthIndicatorService(clusterService, allocationService); + indicatorService = new ShardsAvailabilityHealthIndicatorService(clusterService, allocationService, new SystemIndices(List.of())); } private int toInt(String v) { diff --git a/docs/changelog/92296.yaml b/docs/changelog/92296.yaml new file mode 100644 index 000000000000..75ecd5885edb --- /dev/null +++ b/docs/changelog/92296.yaml @@ -0,0 +1,6 @@ +pr: 92296 +summary: "[HealthAPI] Add support for the FEATURE_STATE affected resource" +area: Health +type: feature +issues: + - 91353 diff --git a/docs/reference/tab-widgets/troubleshooting/data/restore-from-snapshot.asciidoc b/docs/reference/tab-widgets/troubleshooting/data/restore-from-snapshot.asciidoc index 0a199bd6e48d..589965d8ab07 100644 --- a/docs/reference/tab-widgets/troubleshooting/data/restore-from-snapshot.asciidoc +++ b/docs/reference/tab-widgets/troubleshooting/data/restore-from-snapshot.asciidoc @@ -210,6 +210,21 @@ POST _snapshot/my_repository/snapshot-20200617/_restore <1> The indices to restore. + <2> We also want to restore the aliases. ++ +NOTE: If any <> need to be restored we'll need to specify them using the +`feature_states` field and the indices that belong to the feature states we restore must not be specified under `indices`. +The <> returns both the `indices` and `feature_states` that need to be restored for the restore from snapshot diagnosis. e.g.: ++ +[source,console] +---- +POST _snapshot/my_repository/snapshot-20200617/_restore +{ + "feature_states": [ "geoip" ], + "indices": "kibana_sample_data_flights,.ds-my-data-stream-2022.06.17-000001", + "include_aliases": true +} +---- +// TEST[skip:illustration purposes only] . Finally we can verify that the indices health is now `green` via the <>. + @@ -430,6 +445,21 @@ POST _snapshot/my_repository/snapshot-20200617/_restore <1> The indices to restore. + <2> We also want to restore the aliases. ++ +NOTE: If any <> need to be restored we'll need to specify them using the +`feature_states` field and the indices that belong to the feature states we restore must not be specified under `indices`. +The <> returns both the `indices` and `feature_states` that need to be restored for the restore from snapshot diagnosis. e.g.: ++ +[source,console] +---- +POST _snapshot/my_repository/snapshot-20200617/_restore +{ + "feature_states": [ "geoip" ], + "indices": "kibana_sample_data_flights,.ds-my-data-stream-2022.06.17-000001", + "include_aliases": true +} +---- +// TEST[skip:illustration purposes only] . Finally we can verify that the indices health is now `green` via the <>. + diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java index 00863ba2d59d..51e53d086739 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java @@ -43,6 +43,7 @@ import org.elasticsearch.health.ImpactArea; import org.elasticsearch.health.SimpleHealthIndicatorDetails; import org.elasticsearch.health.node.HealthInfo; +import org.elasticsearch.indices.SystemIndices; import org.elasticsearch.snapshots.SnapshotShardSizeInfo; import java.util.ArrayList; @@ -59,6 +60,8 @@ import java.util.stream.Stream; import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; import static org.elasticsearch.cluster.health.ClusterShardHealth.getInactivePrimaryHealth; import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_INCLUDE_GROUP_PREFIX; import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_INCLUDE_GROUP_SETTING; @@ -67,6 +70,7 @@ import static org.elasticsearch.cluster.routing.allocation.decider.EnableAllocationDecider.INDEX_ROUTING_ALLOCATION_ENABLE_SETTING; import static org.elasticsearch.cluster.routing.allocation.decider.ShardsLimitAllocationDecider.CLUSTER_TOTAL_SHARDS_PER_NODE_SETTING; import static org.elasticsearch.cluster.routing.allocation.decider.ShardsLimitAllocationDecider.INDEX_TOTAL_SHARDS_PER_NODE_SETTING; +import static org.elasticsearch.health.Diagnosis.Resource.Type.FEATURE_STATE; import static org.elasticsearch.health.Diagnosis.Resource.Type.INDEX; import static org.elasticsearch.health.HealthStatus.GREEN; import static org.elasticsearch.health.HealthStatus.RED; @@ -96,9 +100,16 @@ public class ShardsAvailabilityHealthIndicatorService implements HealthIndicator private final ClusterService clusterService; private final AllocationService allocationService; - public ShardsAvailabilityHealthIndicatorService(ClusterService clusterService, AllocationService allocationService) { + private final SystemIndices systemIndices; + + public ShardsAvailabilityHealthIndicatorService( + ClusterService clusterService, + AllocationService allocationService, + SystemIndices systemIndices + ) { this.clusterService = clusterService; this.allocationService = allocationService; + this.systemIndices = systemIndices; } @Override @@ -760,7 +771,7 @@ private Optional checkNotEnoughNodesInDataTier( } } - private class ShardAllocationStatus { + class ShardAllocationStatus { private final ShardAllocationCounts primaries = new ShardAllocationCounts(); private final ShardAllocationCounts replicas = new ShardAllocationCounts(); private final Metadata clusterMetadata; @@ -908,28 +919,108 @@ public List getDiagnosis(boolean verbose, int maxAffectedResourcesCou if (diagnosisToAffectedIndices.isEmpty()) { return List.of(); } else { - return diagnosisToAffectedIndices.entrySet() - .stream() - .map( - e -> new Diagnosis( - e.getKey(), - List.of( - new Diagnosis.Resource( - INDEX, - e.getValue() - .stream() - .sorted(indicesComparatorByPriorityAndName(clusterMetadata)) - .limit(Math.min(e.getValue().size(), maxAffectedResourcesCount)) - .collect(Collectors.toList()) - ) + + return diagnosisToAffectedIndices.entrySet().stream().map(e -> { + List affectedResources = new ArrayList<>(1); + if (e.getKey().equals(ACTION_RESTORE_FROM_SNAPSHOT)) { + Set restoreFromSnapshotIndices = e.getValue(); + if (restoreFromSnapshotIndices != null && restoreFromSnapshotIndices.isEmpty() == false) { + affectedResources = getRestoreFromSnapshotAffectedResources( + clusterMetadata, + systemIndices, + restoreFromSnapshotIndices, + maxAffectedResourcesCount + ); + } + } else { + affectedResources.add( + new Diagnosis.Resource( + INDEX, + e.getValue() + .stream() + .sorted(indicesComparatorByPriorityAndName(clusterMetadata)) + .limit(Math.min(e.getValue().size(), maxAffectedResourcesCount)) + .collect(Collectors.toList()) ) - ) - ) - .collect(Collectors.toList()); + ); + } + return new Diagnosis(e.getKey(), affectedResources); + }).collect(Collectors.toList()); } } else { return List.of(); } } + + /** + * The restore from snapshot operation requires the user to specify indices and feature states. + * The indices that are part of the feature states must not be specified. This method loops through all the + * identified unassigned indices and returns the affected {@link Diagnosis.Resource}s of type `INDEX` + * and if applicable `FEATURE_STATE` + */ + static List getRestoreFromSnapshotAffectedResources( + Metadata metadata, + SystemIndices systemIndices, + Set restoreFromSnapshotIndices, + int maxAffectedResourcesCount + ) { + List affectedResources = new ArrayList<>(2); + + Set affectedIndices = new HashSet<>(restoreFromSnapshotIndices); + Set affectedFeatureStates = new HashSet<>(); + Map> featureToSystemIndices = systemIndices.getFeatures() + .stream() + .collect( + toMap( + SystemIndices.Feature::getName, + feature -> feature.getIndexDescriptors() + .stream() + .flatMap(descriptor -> descriptor.getMatchingIndices(metadata).stream()) + .collect(toSet()) + ) + ); + + for (Map.Entry> featureToIndices : featureToSystemIndices.entrySet()) { + for (String featureIndex : featureToIndices.getValue()) { + if (restoreFromSnapshotIndices.contains(featureIndex)) { + affectedFeatureStates.add(featureToIndices.getKey()); + affectedIndices.remove(featureIndex); + } + } + } + + Map> featureToDsBackingIndices = systemIndices.getFeatures() + .stream() + .collect( + toMap( + SystemIndices.Feature::getName, + feature -> feature.getDataStreamDescriptors() + .stream() + .flatMap(descriptor -> descriptor.getBackingIndexNames(metadata).stream()) + .collect(toSet()) + ) + ); + + // the shards_availability indicator works with indices so let's remove the feature states data streams backing indices from + // the list of affected indices (the feature state will cover the restore of these indices too) + for (Map.Entry> featureToBackingIndices : featureToDsBackingIndices.entrySet()) { + for (String featureIndex : featureToBackingIndices.getValue()) { + if (restoreFromSnapshotIndices.contains(featureIndex)) { + affectedFeatureStates.add(featureToBackingIndices.getKey()); + affectedIndices.remove(featureIndex); + } + } + } + + if (affectedIndices.isEmpty() == false) { + affectedResources.add(new Diagnosis.Resource(INDEX, affectedIndices.stream().limit(maxAffectedResourcesCount).toList())); + } + if (affectedFeatureStates.isEmpty() == false) { + affectedResources.add( + new Diagnosis.Resource(FEATURE_STATE, affectedFeatureStates.stream().limit(maxAffectedResourcesCount).toList()) + ); + } + return affectedResources; + } } } diff --git a/server/src/main/java/org/elasticsearch/health/Diagnosis.java b/server/src/main/java/org/elasticsearch/health/Diagnosis.java index 343fe86d8745..a190dd3d5df0 100644 --- a/server/src/main/java/org/elasticsearch/health/Diagnosis.java +++ b/server/src/main/java/org/elasticsearch/health/Diagnosis.java @@ -44,6 +44,7 @@ public enum Type { INDEX("indices"), NODE("nodes"), SLM_POLICY("slm_policies"), + FEATURE_STATE("feature_states"), SNAPSHOT_REPOSITORY("snapshot_repositories"); private final String displayValue; diff --git a/server/src/main/java/org/elasticsearch/node/Node.java b/server/src/main/java/org/elasticsearch/node/Node.java index 58b089940b21..8b6bb425b28a 100644 --- a/server/src/main/java/org/elasticsearch/node/Node.java +++ b/server/src/main/java/org/elasticsearch/node/Node.java @@ -997,7 +997,13 @@ protected Node( discoveryModule.getCoordinator(), masterHistoryService ); - HealthService healthService = createHealthService(clusterService, clusterModule, coordinationDiagnosticsService, threadPool); + HealthService healthService = createHealthService( + clusterService, + clusterModule, + coordinationDiagnosticsService, + threadPool, + systemIndices + ); HealthMetadataService healthMetadataService = HealthMetadataService.create(clusterService, settings); LocalHealthMonitor localHealthMonitor = LocalHealthMonitor.create(settings, clusterService, nodeService, threadPool, client); HealthInfoCache nodeHealthOverview = HealthInfoCache.create(clusterService); @@ -1199,7 +1205,8 @@ private HealthService createHealthService( ClusterService clusterService, ClusterModule clusterModule, CoordinationDiagnosticsService coordinationDiagnosticsService, - ThreadPool threadPool + ThreadPool threadPool, + SystemIndices systemIndices ) { List preflightHealthIndicatorServices = Collections.singletonList( new StableMasterHealthIndicatorService(coordinationDiagnosticsService, clusterService) @@ -1207,7 +1214,7 @@ private HealthService createHealthService( var serverHealthIndicatorServices = new ArrayList<>( List.of( new RepositoryIntegrityHealthIndicatorService(clusterService), - new ShardsAvailabilityHealthIndicatorService(clusterService, clusterModule.getAllocationService()) + new ShardsAvailabilityHealthIndicatorService(clusterService, clusterModule.getAllocationService(), systemIndices) ) ); serverHealthIndicatorServices.add(new DiskHealthIndicatorService(clusterService)); diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorServiceTests.java index 7682a10483e7..b3439b202b7a 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorServiceTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.Version; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.ComposableIndexTemplate; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.metadata.NodesShutdownMetadata; @@ -23,6 +24,7 @@ import org.elasticsearch.cluster.routing.RoutingTable; import org.elasticsearch.cluster.routing.ShardRouting; import org.elasticsearch.cluster.routing.UnassignedInfo; +import org.elasticsearch.cluster.routing.allocation.ShardsAvailabilityHealthIndicatorService.ShardAllocationStatus; import org.elasticsearch.cluster.routing.allocation.decider.AwarenessAllocationDecider; import org.elasticsearch.cluster.routing.allocation.decider.Decision; import org.elasticsearch.cluster.routing.allocation.decider.EnableAllocationDecider; @@ -43,7 +45,12 @@ import org.elasticsearch.health.node.HealthInfo; import org.elasticsearch.index.Index; import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.indices.ExecutorNames; +import org.elasticsearch.indices.SystemDataStreamDescriptor; +import org.elasticsearch.indices.SystemIndexDescriptor; +import org.elasticsearch.indices.SystemIndices; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.ThreadPool; import org.mockito.stubbing.Answer; import java.util.ArrayList; @@ -56,7 +63,10 @@ import java.util.UUID; import java.util.stream.Collectors; +import static java.util.Collections.emptyList; import static java.util.stream.Collectors.toMap; +import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.createBackingIndex; +import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.newInstance; import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_INCLUDE_GROUP_PREFIX; import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_PREFIX; import static org.elasticsearch.cluster.metadata.SingleNodeShutdownMetadata.Type.RESTART; @@ -83,14 +93,18 @@ import static org.elasticsearch.cluster.routing.allocation.decider.ShardsLimitAllocationDecider.CLUSTER_TOTAL_SHARDS_PER_NODE_SETTING; import static org.elasticsearch.common.util.CollectionUtils.concatLists; import static org.elasticsearch.core.TimeValue.timeValueSeconds; +import static org.elasticsearch.health.Diagnosis.Resource.Type.FEATURE_STATE; import static org.elasticsearch.health.Diagnosis.Resource.Type.INDEX; import static org.elasticsearch.health.HealthStatus.GREEN; import static org.elasticsearch.health.HealthStatus.RED; import static org.elasticsearch.health.HealthStatus.YELLOW; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.emptyCollectionOf; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -114,8 +128,8 @@ public void testShouldBeGreenWhenAllPrimariesAndReplicasAreStarted() { GREEN, "This cluster has all shards available.", Map.of("started_primaries", 2, "started_replicas", 1), - Collections.emptyList(), - Collections.emptyList() + emptyList(), + emptyList() ) ) ); @@ -353,8 +367,8 @@ public void testShouldBeGreenWhenThereAreRestartingReplicas() { GREEN, "This cluster has 1 restarting replica shard.", Map.of("started_primaries", 1, "restarting_replicas", 1), - Collections.emptyList(), - Collections.emptyList() + emptyList(), + emptyList() ) ) ); @@ -374,8 +388,8 @@ public void testShouldBeGreenWhenThereAreNoReplicasExpected() { GREEN, "This cluster has all shards available.", Map.of("started_primaries", 1), - Collections.emptyList(), - Collections.emptyList() + emptyList(), + emptyList() ) ) ); @@ -436,8 +450,8 @@ public void testShouldBeGreenWhenThereAreInitializingPrimaries() { GREEN, "This cluster has 1 creating primary shard.", Map.of("creating_primaries", 1), - Collections.emptyList(), - Collections.emptyList() + emptyList(), + emptyList() ) ) ); @@ -457,8 +471,8 @@ public void testShouldBeGreenWhenThereAreRestartingPrimaries() { GREEN, "This cluster has 1 restarting primary shard.", Map.of("restarting_primaries", 1), - Collections.emptyList(), - Collections.emptyList() + emptyList(), + emptyList() ) ) ); @@ -503,7 +517,7 @@ public void testShouldBeRedWhenRestartingPrimariesReachedAllocationDelayAndNoRep ); } - public void testUserActionsNotGeneratedWhenNotDrillingDown() { + public void testDiagnosisNotGeneratedWhenNotDrillingDown() { // Index definition, 1 primary no replicas IndexMetadata indexMetadata = IndexMetadata.builder("red-index") .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT).build()) @@ -562,6 +576,149 @@ public void testDiagnoseRestoreIndexAfterDataLoss() { assertThat(definitions, contains(ACTION_RESTORE_FROM_SNAPSHOT)); } + public void testRestoreFromSnapshotReportsFeatureStates() { + // this test adds a mix of regular and system indices and data streams + // we'll test the `shards_availability` indicator correctly reports the + // affected feature states and indices + + IndexMetadata featureIndex = IndexMetadata.builder(".feature-index") + .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT).build()) + .numberOfShards(1) + .numberOfReplicas(0) + .build(); + + IndexMetadata regularIndex = IndexMetadata.builder("regular-index") + .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT).build()) + .numberOfShards(1) + .numberOfReplicas(0) + .build(); + + String featureDataStreamName = ".test-ds-feature"; + IndexMetadata backingIndex = createBackingIndex(featureDataStreamName, 1).build(); + + ShardRouting featureIndexRouting = createShardRouting( + new ShardId(featureIndex.getIndex(), 0), + true, + new ShardAllocation(randomNodeId(), UNAVAILABLE, noShardCopy()) + ); + + ShardRouting regularIndexRouting = createShardRouting( + new ShardId(regularIndex.getIndex(), 0), + true, + new ShardAllocation(randomNodeId(), UNAVAILABLE, noShardCopy()) + ); + + ShardRouting backingIndexRouting = createShardRouting( + new ShardId(backingIndex.getIndex(), 0), + true, + new ShardAllocation(randomNodeId(), UNAVAILABLE, noShardCopy()) + ); + + var clusterState = createClusterStateWith( + List.of(featureIndex, regularIndex, backingIndex), + List.of( + IndexRoutingTable.builder(featureIndex.getIndex()).addShard(featureIndexRouting).build(), + IndexRoutingTable.builder(regularIndex.getIndex()).addShard(regularIndexRouting).build(), + IndexRoutingTable.builder(backingIndex.getIndex()).addShard(backingIndexRouting).build() + ), + List.of(), + List.of() + ); + + // add the data stream to the cluster state + Metadata.Builder mdBuilder = Metadata.builder(clusterState.metadata()) + .put(newInstance(featureDataStreamName, List.of(backingIndex.getIndex()))); + ClusterState state = ClusterState.builder(clusterState).metadata(mdBuilder).build(); + + var service = createAllocationHealthIndicatorService( + Settings.EMPTY, + state, + Map.of(), + getSystemIndices(featureDataStreamName, ".test-ds-*", ".feature-*") + ); + HealthIndicatorResult result = service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO); + + assertThat(result.status(), is(HealthStatus.RED)); + assertThat(result.diagnosisList().size(), is(1)); + Diagnosis diagnosis = result.diagnosisList().get(0); + List affectedResources = diagnosis.affectedResources(); + assertThat("expecting we report a resource of type INDEX and one of type FEATURE_STATE", affectedResources.size(), is(2)); + for (Diagnosis.Resource resource : affectedResources) { + if (resource.getType() == INDEX) { + assertThat(resource.getValues(), hasItems("regular-index")); + } else { + assertThat(resource.getType(), is(FEATURE_STATE)); + assertThat(resource.getValues(), hasItems("feature-with-system-data-stream", "feature-with-system-index")); + } + } + } + + public void testGetRestoreFromSnapshotAffectedResources() { + String featureDataStreamName = ".test-ds-feature"; + IndexMetadata backingIndex = createBackingIndex(featureDataStreamName, 1).build(); + + List indexMetadataList = List.of( + IndexMetadata.builder(".feature-index") + .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT).build()) + .numberOfShards(1) + .numberOfReplicas(0) + .build(), + IndexMetadata.builder("regular-index") + .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT).build()) + .numberOfShards(1) + .numberOfReplicas(0) + .build(), + backingIndex + ); + + Metadata.Builder metadataBuilder = Metadata.builder(); + Map indexMetadataMap = new HashMap<>(); + for (IndexMetadata indexMetadata : indexMetadataList) { + indexMetadataMap.put(indexMetadata.getIndex().getName(), indexMetadata); + } + metadataBuilder.indices(indexMetadataMap); + metadataBuilder.put(newInstance(featureDataStreamName, List.of(backingIndex.getIndex()))); + Metadata metadata = metadataBuilder.build(); + { + List affectedResources = ShardAllocationStatus.getRestoreFromSnapshotAffectedResources( + metadata, + getSystemIndices(featureDataStreamName, ".test-ds-*", ".feature-*"), + Set.of(backingIndex.getIndex().getName(), ".feature-index", "regular-index"), + 10 + ); + + assertThat(affectedResources.size(), is(2)); + for (Diagnosis.Resource resource : affectedResources) { + if (resource.getType() == INDEX) { + assertThat(resource.getValues(), hasItems("regular-index")); + } else { + assertThat(resource.getType(), is(FEATURE_STATE)); + assertThat(resource.getValues(), hasItems("feature-with-system-data-stream", "feature-with-system-index")); + } + } + } + + { + List affectedResources = ShardAllocationStatus.getRestoreFromSnapshotAffectedResources( + metadata, + getSystemIndices(featureDataStreamName, ".test-ds-*", ".feature-*"), + Set.of(backingIndex.getIndex().getName(), ".feature-index", "regular-index"), + 0 + ); + + assertThat(affectedResources.size(), is(2)); + for (Diagnosis.Resource resource : affectedResources) { + if (resource.getType() == INDEX) { + assertThat(resource.getValues(), emptyCollectionOf(String.class)); + } else { + assertThat(resource.getType(), is(FEATURE_STATE)); + assertThat(resource.getValues(), emptyCollectionOf(String.class)); + } + } + } + + } + public void testDiagnoseUnknownAllocationDeciderIssue() { // Index definition, 1 primary no replicas IndexMetadata indexMetadata = IndexMetadata.builder("red-index") @@ -1253,6 +1410,53 @@ public void testLimitNumberOfAffectedResources() { } } + /** + * Creates the {@link SystemIndices} with one standalone system index and a system data stream + */ + private SystemIndices getSystemIndices( + String featureDataStreamName, + String systemDataStreamPattern, + String standaloneSystemIndexPattern + ) { + return new SystemIndices( + List.of( + new SystemIndices.Feature( + "feature-with-system-index", + "testing", + List.of(new SystemIndexDescriptor(standaloneSystemIndexPattern, "feature with index")) + ), + new SystemIndices.Feature( + "feature-with-system-data-stream", + "feature with data stream", + List.of(), + List.of( + new SystemDataStreamDescriptor( + featureDataStreamName, + "description", + SystemDataStreamDescriptor.Type.EXTERNAL, + new ComposableIndexTemplate( + List.of(systemDataStreamPattern), + null, + null, + null, + null, + null, + new ComposableIndexTemplate.DataStreamTemplate() + ), + Map.of(), + List.of("test"), + new ExecutorNames( + ThreadPool.Names.SYSTEM_CRITICAL_READ, + ThreadPool.Names.SYSTEM_READ, + ThreadPool.Names.SYSTEM_WRITE + ) + ) + ) + ) + ) + ); + } + private HealthIndicatorResult createExpectedResult( HealthStatus status, String symptom, @@ -1271,7 +1475,7 @@ private HealthIndicatorResult createExpectedResult( } private HealthIndicatorResult createExpectedTruncatedResult(HealthStatus status, String symptom, List impacts) { - return new HealthIndicatorResult(NAME, status, symptom, HealthIndicatorDetails.EMPTY, impacts, Collections.emptyList()); + return new HealthIndicatorResult(NAME, status, symptom, HealthIndicatorDetails.EMPTY, impacts, emptyList()); } private static ClusterState createClusterStateWith(List indexRoutes, List nodeShutdowns) { @@ -1534,13 +1738,22 @@ private static ShardsAvailabilityHealthIndicatorService createShardsAvailability ClusterState clusterState, final Map decisions ) { - return createShardsAvailabilityIndicatorService(Settings.EMPTY, clusterState, decisions); + return createAllocationHealthIndicatorService(Settings.EMPTY, clusterState, decisions, new SystemIndices(List.of())); } private static ShardsAvailabilityHealthIndicatorService createShardsAvailabilityIndicatorService( Settings nodeSettings, ClusterState clusterState, final Map decisions + ) { + return createAllocationHealthIndicatorService(nodeSettings, clusterState, decisions, new SystemIndices(List.of())); + } + + private static ShardsAvailabilityHealthIndicatorService createAllocationHealthIndicatorService( + Settings nodeSettings, + ClusterState clusterState, + final Map decisions, + SystemIndices systemIndices ) { var clusterService = mock(ClusterService.class); when(clusterService.state()).thenReturn(clusterState); @@ -1552,6 +1765,6 @@ private static ShardsAvailabilityHealthIndicatorService createShardsAvailability var key = new ShardRoutingKey(shardRouting.getIndexName(), shardRouting.getId(), shardRouting.primary()); return decisions.getOrDefault(key, ShardAllocationDecision.NOT_TAKEN); }); - return new ShardsAvailabilityHealthIndicatorService(clusterService, allocationService); + return new ShardsAvailabilityHealthIndicatorService(clusterService, allocationService, systemIndices); } } From 45cbbe4b94bfe124bff4ad5c202a19ccfd9e2539 Mon Sep 17 00:00:00 2001 From: Jake Landis Date: Tue, 20 Dec 2022 13:42:37 -0600 Subject: [PATCH 317/919] Bump reactor netty version (#92457) Bump reactor netty version to 1.0.24 --- docs/changelog/92457.yaml | 5 +++++ gradle/verification-metadata.xml | 22 ++++++---------------- modules/repository-azure/build.gradle | 2 +- 3 files changed, 12 insertions(+), 17 deletions(-) create mode 100644 docs/changelog/92457.yaml diff --git a/docs/changelog/92457.yaml b/docs/changelog/92457.yaml new file mode 100644 index 000000000000..e8a2e7207cad --- /dev/null +++ b/docs/changelog/92457.yaml @@ -0,0 +1,5 @@ +pr: 92457 +summary: Bump reactor netty version +area: Snapshot/Restore +type: upgrade +issues: [] diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 2d9d18c07d4d..6876b8996d9a 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1384,24 +1384,14 @@ - - - + + + - - - - - - - - - - - - - + + + diff --git a/modules/repository-azure/build.gradle b/modules/repository-azure/build.gradle index 9d71430efd23..0101c0d4df7c 100644 --- a/modules/repository-azure/build.gradle +++ b/modules/repository-azure/build.gradle @@ -35,7 +35,7 @@ versions << [ 'stax2API': '4.2.1', 'woodstox': '6.4.0', - 'reactorNetty': '1.0.23', + 'reactorNetty': '1.0.24', 'reactorCore': '3.4.23', 'reactiveStreams': '1.0.4', ] From d919beab5b227e0bed0d013c17eb82ae9a6e5e01 Mon Sep 17 00:00:00 2001 From: Costin Leau Date: Tue, 20 Dec 2022 22:38:33 +0200 Subject: [PATCH 318/919] QL: Introduce parameterized rule and executor (#92428) Extend the QL rule (and executor) to allow passing a Context object to pass runtime information to certain rules. This avoids having to encapsulate the state into the rule by passing them as argument allowing the same rule instance to be reused across multiple queries at the same time. A common example of this are Analyzers which rely on the session Configuration or IndexResolution to perform analysis. To minimize the amount of change the existing Rule and RuleExecutor are kept in place with minimal changes and two subclasses are introduced: ParameterizedRule and ParameterizedRuleExecutor - the former accepts passing of the Context object to each rule while the latter keeps the state and passes it when encountering a ParameterizedRule. The majority of the PR is propagating this changes in the tests, mainly replacing the creation of the Analyzer; to prevent future refactoring noise, the creation has been centralized into an utility class that handles the various variants. --- docs/changelog/92428.yaml | 5 ++ .../xpack/eql/analysis/Analyzer.java | 40 ++++----- .../xpack/eql/analysis/AnalyzerContext.java | 13 +++ .../xpack/eql/analysis/AnalyzerRule.java | 28 ------- .../xpack/eql/optimizer/Optimizer.java | 14 ++-- .../xpack/eql/planner/Mapper.java | 5 +- .../xpack/eql/planner/QueryFolder.java | 7 +- .../xpack/eql/session/EqlSession.java | 3 +- .../xpack/eql/analysis/AnalyzerTestUtils.java | 40 +++++++++ .../xpack/eql/analysis/AnalyzerTests.java | 6 +- .../xpack/eql/analysis/VerifierTests.java | 8 +- .../xpack/eql/optimizer/OptimizerTests.java | 6 +- .../xpack/eql/optimizer/TomlFoldTests.java | 8 +- .../AbstractQueryTranslatorTestCase.java | 6 +- .../xpack/eql/stats/VerifierMetricsTests.java | 8 +- .../xpack/ql/analyzer/AnalyzerRules.java | 20 ++++- .../xpack/ql/optimizer/OptimizerRules.java | 7 +- .../xpack/ql/rule/ParameterizedRule.java | 19 +++++ .../ql/rule/ParameterizedRuleExecutor.java | 31 +++++++ .../org/elasticsearch/xpack/ql/rule/Rule.java | 10 +-- .../xpack/ql/rule/RuleExecutor.java | 41 +++++----- .../xpack/sql/analysis/analyzer/Analyzer.java | 81 +++++++++---------- .../analysis/analyzer/AnalyzerContext.java | 27 +++++++ .../xpack/sql/optimizer/Optimizer.java | 16 ++-- .../xpack/sql/plan/logical/command/Debug.java | 4 +- .../xpack/sql/planner/Mapper.java | 5 +- .../xpack/sql/planner/QueryFolder.java | 10 +-- .../xpack/sql/session/SqlSession.java | 10 ++- .../analysis/analyzer/AnalyzerTestUtils.java | 46 +++++++++++ .../sql/analysis/analyzer/AnalyzerTests.java | 7 +- .../analyzer/FieldAttributeTests.java | 43 ++++++---- .../analyzer/VerifierErrorMessagesTests.java | 8 +- .../scalar/DatabaseFunctionTests.java | 7 +- .../function/scalar/UserFunctionTests.java | 7 +- .../scalar/datetime/CurrentDateTimeTests.java | 6 +- .../scalar/datetime/CurrentTimeTests.java | 6 +- .../sql/optimizer/OptimizerRunTests.java | 9 +-- .../logical/command/sys/SysColumnsTests.java | 6 +- .../logical/command/sys/SysTablesTests.java | 11 +-- .../logical/command/sys/SysTypesTests.java | 4 +- .../xpack/sql/planner/QueryFolderTests.java | 5 +- .../sql/planner/QueryTranslatorSpecTests.java | 7 +- .../sql/planner/QueryTranslatorTests.java | 5 +- .../xpack/sql/planner/VerifierTests.java | 7 +- .../xpack/sql/stats/VerifierMetricsTests.java | 5 +- 45 files changed, 402 insertions(+), 265 deletions(-) create mode 100644 docs/changelog/92428.yaml create mode 100644 x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/analysis/AnalyzerContext.java delete mode 100644 x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/analysis/AnalyzerRule.java create mode 100644 x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/AnalyzerTestUtils.java create mode 100644 x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/rule/ParameterizedRule.java create mode 100644 x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/rule/ParameterizedRuleExecutor.java create mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/analyzer/AnalyzerContext.java create mode 100644 x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/AnalyzerTestUtils.java diff --git a/docs/changelog/92428.yaml b/docs/changelog/92428.yaml new file mode 100644 index 000000000000..dda2ff132feb --- /dev/null +++ b/docs/changelog/92428.yaml @@ -0,0 +1,5 @@ +pr: 92428 +summary: Introduce parameterized rule and executor +area: Query Languages +type: enhancement +issues: [] diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/analysis/Analyzer.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/analysis/Analyzer.java index b4664ee3a3c3..623d0a51da3e 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/analysis/Analyzer.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/analysis/Analyzer.java @@ -9,6 +9,8 @@ import org.elasticsearch.xpack.eql.expression.OptionalMissingAttribute; import org.elasticsearch.xpack.eql.expression.OptionalUnresolvedAttribute; +import org.elasticsearch.xpack.ql.analyzer.AnalyzerRules; +import org.elasticsearch.xpack.ql.analyzer.AnalyzerRules.AnalyzerRule; import org.elasticsearch.xpack.ql.common.Failure; import org.elasticsearch.xpack.ql.expression.Attribute; import org.elasticsearch.xpack.ql.expression.Expression; @@ -17,12 +19,11 @@ import org.elasticsearch.xpack.ql.expression.UnresolvedAttribute; import org.elasticsearch.xpack.ql.expression.function.Function; import org.elasticsearch.xpack.ql.expression.function.FunctionDefinition; -import org.elasticsearch.xpack.ql.expression.function.FunctionRegistry; import org.elasticsearch.xpack.ql.expression.function.UnresolvedFunction; import org.elasticsearch.xpack.ql.plan.logical.Filter; import org.elasticsearch.xpack.ql.plan.logical.LogicalPlan; +import org.elasticsearch.xpack.ql.rule.ParameterizedRuleExecutor; import org.elasticsearch.xpack.ql.rule.RuleExecutor; -import org.elasticsearch.xpack.ql.session.Configuration; import org.elasticsearch.xpack.ql.type.DataTypes; import java.util.Collection; @@ -32,27 +33,28 @@ import static org.elasticsearch.xpack.eql.analysis.AnalysisUtils.resolveAgainstList; import static org.elasticsearch.xpack.ql.analyzer.AnalyzerRules.AddMissingEqualsToBoolField; -public class Analyzer extends RuleExecutor { +public class Analyzer extends ParameterizedRuleExecutor { + + private static final Iterable> rules; + + static { + var optional = new Batch<>("Optional", Limiter.ONCE, new ResolveOrReplaceOptionalRefs()); + var resolution = new Batch<>("Resolution", new ResolveRefs(), new ResolveFunctions()); + var cleanup = new Batch<>("Finish Analysis", Limiter.ONCE, new AddMissingEqualsToBoolField()); + + rules = asList(optional, resolution, cleanup); + } - private final Configuration configuration; - private final FunctionRegistry functionRegistry; private final Verifier verifier; - public Analyzer(Configuration configuration, FunctionRegistry functionRegistry, Verifier verifier) { - this.configuration = configuration; - this.functionRegistry = functionRegistry; + public Analyzer(AnalyzerContext context, Verifier verifier) { + super(context); this.verifier = verifier; } @Override - protected Iterable.Batch> batches() { - Batch optional = new Batch("Optional", Limiter.ONCE, new ResolveOrReplaceOptionalRefs()); - - Batch resolution = new Batch("Resolution", new ResolveRefs(), new ResolveFunctions()); - - Batch cleanup = new Batch("Finish Analysis", Limiter.ONCE, new AddMissingEqualsToBoolField()); - - return asList(optional, resolution, cleanup); + protected Iterable> batches() { + return rules; } public LogicalPlan analyze(LogicalPlan plan) { @@ -99,10 +101,12 @@ protected LogicalPlan rule(LogicalPlan plan) { } } - private class ResolveFunctions extends AnalyzerRule { + private static class ResolveFunctions extends AnalyzerRules.ParameterizedAnalyzerRule { @Override - protected LogicalPlan rule(LogicalPlan plan) { + protected LogicalPlan rule(LogicalPlan plan, AnalyzerContext context) { + var configuration = context.configuration(); + var functionRegistry = context.functionRegistry(); return plan.transformExpressionsUp(UnresolvedFunction.class, uf -> { if (uf.analyzed()) { return uf; diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/analysis/AnalyzerContext.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/analysis/AnalyzerContext.java new file mode 100644 index 000000000000..7a09e363734f --- /dev/null +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/analysis/AnalyzerContext.java @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.eql.analysis; + +import org.elasticsearch.xpack.ql.expression.function.FunctionRegistry; +import org.elasticsearch.xpack.ql.session.Configuration; + +public record AnalyzerContext(Configuration configuration, FunctionRegistry functionRegistry) {} diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/analysis/AnalyzerRule.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/analysis/AnalyzerRule.java deleted file mode 100644 index 2df98c739824..000000000000 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/analysis/AnalyzerRule.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.eql.analysis; - -import org.elasticsearch.xpack.ql.plan.logical.LogicalPlan; -import org.elasticsearch.xpack.ql.rule.Rule; - -public abstract class AnalyzerRule extends Rule { - - // transformUp (post-order) - that is first children and then the node - // but with a twist; only if the tree is not resolved or analyzed - @Override - public final LogicalPlan apply(LogicalPlan plan) { - return plan.transformUp(typeToken(), t -> t.analyzed() || skipResolved() && t.resolved() ? t : rule(t)); - } - - @Override - protected abstract LogicalPlan rule(SubPlan plan); - - protected boolean skipResolved() { - return true; - } -} diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/optimizer/Optimizer.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/optimizer/Optimizer.java index 53fe7391018f..b090e768a358 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/optimizer/Optimizer.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/optimizer/Optimizer.java @@ -78,8 +78,8 @@ public LogicalPlan optimize(LogicalPlan verified) { } @Override - protected Iterable.Batch> batches() { - Batch substitutions = new Batch( + protected Iterable> batches() { + var substitutions = new Batch<>( "Substitution", Limiter.ONCE, new ReplaceWildcards(), @@ -89,7 +89,7 @@ protected Iterable.Batch> batches() { new AddMandatoryJoinKeyFilter() ); - Batch operators = new Batch( + var operators = new Batch<>( "Operator Optimization", new ConstantFolding(), // boolean @@ -107,13 +107,13 @@ protected Iterable.Batch> batches() { new PushDownAndCombineFilters() ); - Batch constraints = new Batch("Infer constraints", Limiter.ONCE, new PropagateJoinKeyConstraints()); + var constraints = new Batch<>("Infer constraints", Limiter.ONCE, new PropagateJoinKeyConstraints()); - Batch ordering = new Batch("Implicit Order", new SortByLimit(), new PushDownOrderBy()); + var ordering = new Batch<>("Implicit Order", new SortByLimit(), new PushDownOrderBy()); - Batch local = new Batch("Skip Elasticsearch", new SkipEmptyFilter(), new SkipEmptyJoin(), new SkipQueryOnLimitZero()); + var local = new Batch<>("Skip Elasticsearch", new SkipEmptyFilter(), new SkipEmptyJoin(), new SkipQueryOnLimitZero()); - Batch label = new Batch("Set as Optimized", Limiter.ONCE, new SetAsOptimized()); + var label = new Batch<>("Set as Optimized", Limiter.ONCE, new SetAsOptimized()); return asList(substitutions, operators, constraints, operators, ordering, local, label); } diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/planner/Mapper.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/planner/Mapper.java index 721c7910699b..2e20d75ab55a 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/planner/Mapper.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/planner/Mapper.java @@ -50,8 +50,8 @@ PhysicalPlan map(LogicalPlan plan) { } @Override - protected Iterable.Batch> batches() { - Batch conversion = new Batch("Mapping", new SimpleExecMapper()); + protected Iterable> batches() { + var conversion = new Batch<>("Mapping", new SimpleExecMapper()); return Arrays.asList(conversion); } @@ -135,7 +135,6 @@ public final PhysicalPlan apply(PhysicalPlan plan) { } @SuppressWarnings("unchecked") - @Override protected final PhysicalPlan rule(UnplannedExec plan) { LogicalPlan subPlan = plan.plan(); if (subPlanToken.isInstance(subPlan)) { diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/planner/QueryFolder.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/planner/QueryFolder.java index a6ce44221fec..cd53ce2adfc7 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/planner/QueryFolder.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/planner/QueryFolder.java @@ -40,9 +40,9 @@ PhysicalPlan fold(PhysicalPlan plan) { } @Override - protected Iterable.Batch> batches() { - Batch fold = new Batch("Fold queries", new FoldProject(), new FoldFilter(), new FoldOrderBy(), new FoldLimit()); - Batch finish = new Batch("Finish query", Limiter.ONCE, new PlanOutputToQueryRef()); + protected Iterable> batches() { + var fold = new Batch<>("Fold queries", new FoldProject(), new FoldFilter(), new FoldOrderBy(), new FoldLimit()); + var finish = new Batch<>("Finish query", Limiter.ONCE, new PlanOutputToQueryRef()); return Arrays.asList(fold, finish); } @@ -139,7 +139,6 @@ public final PhysicalPlan apply(PhysicalPlan plan) { return plan.transformUp(typeToken(), this::rule); } - @Override protected abstract PhysicalPlan rule(SubPlan plan); } diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/session/EqlSession.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/session/EqlSession.java index 9ec908c696a0..3c55275b8dee 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/session/EqlSession.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/session/EqlSession.java @@ -13,6 +13,7 @@ import org.elasticsearch.common.breaker.CircuitBreaker; import org.elasticsearch.tasks.TaskCancelledException; import org.elasticsearch.xpack.eql.analysis.Analyzer; +import org.elasticsearch.xpack.eql.analysis.AnalyzerContext; import org.elasticsearch.xpack.eql.analysis.PostAnalyzer; import org.elasticsearch.xpack.eql.analysis.PreAnalyzer; import org.elasticsearch.xpack.eql.analysis.Verifier; @@ -59,7 +60,7 @@ public EqlSession( this.indexResolver = indexResolver; this.preAnalyzer = preAnalyzer; this.postAnalyzer = postAnalyzer; - this.analyzer = new Analyzer(cfg, functionRegistry, verifier); + this.analyzer = new Analyzer(new AnalyzerContext(cfg, functionRegistry), verifier); this.optimizer = optimizer; this.planner = planner; this.circuitBreaker = circuitBreaker; diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/AnalyzerTestUtils.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/AnalyzerTestUtils.java new file mode 100644 index 000000000000..c93bb693f736 --- /dev/null +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/AnalyzerTestUtils.java @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.eql.analysis; + +import org.elasticsearch.xpack.eql.expression.function.EqlFunctionRegistry; +import org.elasticsearch.xpack.eql.session.EqlConfiguration; +import org.elasticsearch.xpack.eql.stats.Metrics; +import org.elasticsearch.xpack.ql.expression.function.FunctionRegistry; + +import static org.elasticsearch.xpack.eql.EqlTestUtils.TEST_CFG; + +public final class AnalyzerTestUtils { + + private AnalyzerTestUtils() {} + + public static Analyzer analyzer() { + return new Analyzer(new AnalyzerContext(TEST_CFG, new EqlFunctionRegistry()), new Verifier(new Metrics())); + } + + public static Analyzer analyzer(Verifier verifier) { + return analyzer(TEST_CFG, new EqlFunctionRegistry(), verifier); + } + + public static Analyzer analyzer(EqlConfiguration configuration) { + return analyzer(configuration, new EqlFunctionRegistry()); + } + + public static Analyzer analyzer(EqlConfiguration configuration, FunctionRegistry registry) { + return analyzer(configuration, registry, new Verifier(new Metrics())); + } + + public static Analyzer analyzer(EqlConfiguration configuration, FunctionRegistry registry, Verifier verifier) { + return new Analyzer(new AnalyzerContext(configuration, registry), verifier); + } +} diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/AnalyzerTests.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/AnalyzerTests.java index 5d6b2e52b901..eec3af3a23d9 100644 --- a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/AnalyzerTests.java +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/AnalyzerTests.java @@ -10,7 +10,6 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.eql.expression.OptionalMissingAttribute; import org.elasticsearch.xpack.eql.expression.OptionalResolvedAttribute; -import org.elasticsearch.xpack.eql.expression.function.EqlFunctionRegistry; import org.elasticsearch.xpack.eql.expression.function.scalar.string.Concat; import org.elasticsearch.xpack.eql.expression.function.scalar.string.ToString; import org.elasticsearch.xpack.eql.parser.EqlParser; @@ -19,7 +18,6 @@ import org.elasticsearch.xpack.eql.plan.logical.LimitWithOffset; import org.elasticsearch.xpack.eql.plan.logical.Sample; import org.elasticsearch.xpack.eql.plan.logical.Sequence; -import org.elasticsearch.xpack.eql.stats.Metrics; import org.elasticsearch.xpack.ql.expression.Expression; import org.elasticsearch.xpack.ql.expression.FieldAttribute; import org.elasticsearch.xpack.ql.expression.Literal; @@ -41,7 +39,7 @@ import java.util.List; import java.util.Map; -import static org.elasticsearch.xpack.eql.EqlTestUtils.TEST_CFG; +import static org.elasticsearch.xpack.eql.analysis.AnalyzerTestUtils.analyzer; public class AnalyzerTests extends ESTestCase { @@ -265,7 +263,7 @@ public void testOptionalFieldsAsSampleKey() { private LogicalPlan accept(IndexResolution resolution, String eql) { PreAnalyzer preAnalyzer = new PreAnalyzer(); - Analyzer analyzer = new Analyzer(TEST_CFG, new EqlFunctionRegistry(), new Verifier(new Metrics())); + Analyzer analyzer = analyzer(); EqlParser parser = new EqlParser(); LogicalPlan plan = parser.createStatement(eql); return analyzer.analyze(preAnalyzer.preAnalyze(plan, resolution)); diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/VerifierTests.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/VerifierTests.java index 4001348c3062..c9ad2380ee54 100644 --- a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/VerifierTests.java +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/VerifierTests.java @@ -9,14 +9,11 @@ import org.elasticsearch.core.TimeValue; import org.elasticsearch.tasks.TaskId; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.eql.EqlTestUtils; -import org.elasticsearch.xpack.eql.expression.function.EqlFunctionRegistry; import org.elasticsearch.xpack.eql.parser.EqlParser; import org.elasticsearch.xpack.eql.parser.ParsingException; import org.elasticsearch.xpack.eql.plan.logical.KeyedFilter; import org.elasticsearch.xpack.eql.plan.logical.Sample; import org.elasticsearch.xpack.eql.session.EqlConfiguration; -import org.elasticsearch.xpack.eql.stats.Metrics; import org.elasticsearch.xpack.ql.expression.EmptyAttribute; import org.elasticsearch.xpack.ql.index.EsIndex; import org.elasticsearch.xpack.ql.index.IndexResolution; @@ -29,6 +26,7 @@ import java.util.function.Function; import static java.util.Collections.emptyMap; +import static org.elasticsearch.xpack.eql.analysis.AnalyzerTestUtils.analyzer; import static org.hamcrest.Matchers.startsWith; public class VerifierTests extends ESTestCase { @@ -48,7 +46,7 @@ private IndexResolution loadIndexResolution(String name) { private LogicalPlan accept(IndexResolution resolution, String eql) { EqlParser parser = new EqlParser(); PreAnalyzer preAnalyzer = new PreAnalyzer(); - Analyzer analyzer = new Analyzer(EqlTestUtils.TEST_CFG, new EqlFunctionRegistry(), new Verifier(new Metrics())); + Analyzer analyzer = analyzer(); LogicalPlan plan = parser.createStatement(eql); return analyzer.analyze(preAnalyzer.preAnalyze(plan, resolution)); @@ -471,7 +469,7 @@ private LogicalPlan analyzeWithVerifierFunction(Function metrics) { private Counters eql(String query) { Metrics metrics = new Metrics(); Verifier verifier = new Verifier(metrics); - Analyzer analyzer = new Analyzer(EqlTestUtils.randomConfiguration(), eqlFunctionRegistry, verifier); + Analyzer analyzer = analyzer(EqlTestUtils.randomConfiguration(), eqlFunctionRegistry, verifier); analyzer.analyze(preAnalyzer.preAnalyze(parser.createStatement(query), index)); return metrics.stats(); } @@ -189,9 +191,7 @@ private static class MetricsHolder { MetricsHolder() { this.metrics = new long[FeatureMetric.values().length]; - for (int i = 0; i < this.metrics.length; i++) { - this.metrics[i] = 0; - } + Arrays.fill(this.metrics, 0); } void set(Set metricSet) { diff --git a/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/analyzer/AnalyzerRules.java b/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/analyzer/AnalyzerRules.java index 10f510436c95..16d5c58ef3d9 100644 --- a/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/analyzer/AnalyzerRules.java +++ b/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/analyzer/AnalyzerRules.java @@ -14,6 +14,7 @@ import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.Equals; import org.elasticsearch.xpack.ql.plan.logical.Filter; import org.elasticsearch.xpack.ql.plan.logical.LogicalPlan; +import org.elasticsearch.xpack.ql.rule.ParameterizedRule; import org.elasticsearch.xpack.ql.rule.Rule; import static java.util.Arrays.asList; @@ -66,7 +67,6 @@ public final LogicalPlan apply(LogicalPlan plan) { return plan.transformUp(typeToken(), t -> t.analyzed() || skipResolved() && t.resolved() ? t : rule(t)); } - @Override protected abstract LogicalPlan rule(SubPlan plan); protected boolean skipResolved() { @@ -74,6 +74,24 @@ protected boolean skipResolved() { } } + public abstract static class ParameterizedAnalyzerRule extends ParameterizedRule< + SubPlan, + LogicalPlan, + P> { + + // transformUp (post-order) - that is first children and then the node + // but with a twist; only if the tree is not resolved or analyzed + public final LogicalPlan apply(LogicalPlan plan, P context) { + return plan.transformUp(typeToken(), t -> t.analyzed() || skipResolved() && t.resolved() ? t : rule(t, context)); + } + + protected abstract LogicalPlan rule(SubPlan plan, P context); + + protected boolean skipResolved() { + return true; + } + } + public abstract static class BaseAnalyzerRule extends AnalyzerRule { @Override diff --git a/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/optimizer/OptimizerRules.java b/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/optimizer/OptimizerRules.java index 89f720111e0c..0a1370844519 100644 --- a/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/optimizer/OptimizerRules.java +++ b/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/optimizer/OptimizerRules.java @@ -1619,7 +1619,6 @@ public final LogicalPlan apply(LogicalPlan plan) { return rule(plan); } - @Override protected final LogicalPlan rule(LogicalPlan plan) { // eliminate redundant casts return plan.transformExpressionsUp(castType, this::maybePruneCast); @@ -1766,12 +1765,10 @@ public LogicalPlan apply(LogicalPlan plan) { return plan; } - @Override - protected LogicalPlan rule(LogicalPlan plan) { + private void rule(LogicalPlan plan) { if (plan.optimized() == false) { plan.setOptimized(); } - return plan; } } @@ -1794,7 +1791,6 @@ public final LogicalPlan apply(LogicalPlan plan) { : plan.transformUp(typeToken(), this::rule); } - @Override protected abstract LogicalPlan rule(SubPlan plan); } @@ -1817,7 +1813,6 @@ public final LogicalPlan apply(LogicalPlan plan) { : plan.transformExpressionsUp(expressionTypeToken, this::rule); } - @Override protected LogicalPlan rule(LogicalPlan plan) { return plan; } diff --git a/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/rule/ParameterizedRule.java b/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/rule/ParameterizedRule.java new file mode 100644 index 000000000000..0b3fac1d894a --- /dev/null +++ b/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/rule/ParameterizedRule.java @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.ql.rule; + +import org.elasticsearch.xpack.ql.tree.Node; + +public abstract class ParameterizedRule, P> extends Rule { + + public abstract T apply(T t, P p); + + public T apply(T t) { + throw new RuleExecutionException("Cannot call parameterized rule without parameter"); + } +} diff --git a/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/rule/ParameterizedRuleExecutor.java b/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/rule/ParameterizedRuleExecutor.java new file mode 100644 index 000000000000..bff63fe919da --- /dev/null +++ b/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/rule/ParameterizedRuleExecutor.java @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.ql.rule; + +import org.elasticsearch.xpack.ql.tree.Node; + +import java.util.function.Function; + +public abstract class ParameterizedRuleExecutor, Context> extends RuleExecutor { + + private final Context context; + + protected ParameterizedRuleExecutor(Context context) { + this.context = context; + } + + protected Context context() { + return context; + } + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected Function transform(Rule rule) { + return (rule instanceof ParameterizedRule pr) ? t -> (TreeType) pr.apply(t, context) : t -> rule.apply(t); + } +} diff --git a/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/rule/Rule.java b/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/rule/Rule.java index 355f104ef13b..6a0b1b7169a2 100644 --- a/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/rule/Rule.java +++ b/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/rule/Rule.java @@ -11,17 +11,15 @@ import org.elasticsearch.xpack.ql.tree.Node; import org.elasticsearch.xpack.ql.util.ReflectionUtils; -import java.util.function.UnaryOperator; - /** * Rules that apply transformation to a tree. In addition, performs * type filtering so that a rule that the rule implementation doesn't * have to manually filter. *

    * Rules could could be built as lambdas but most - * rules are much larger so we keep them as full blown subclasses. + * rules are much larger, so we keep them as full-blown subclasses. */ -public abstract class Rule> implements UnaryOperator { +public abstract class Rule> { protected Logger log = LogManager.getLogger(getClass()); @@ -44,10 +42,10 @@ public String name() { return name; } - protected abstract T rule(E e); - @Override public String toString() { return name(); } + + public abstract T apply(T t); } diff --git a/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/rule/RuleExecutor.java b/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/rule/RuleExecutor.java index 9ca63bc62adf..2b71ca1cef85 100644 --- a/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/rule/RuleExecutor.java +++ b/x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/rule/RuleExecutor.java @@ -16,6 +16,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; public abstract class RuleExecutor> { @@ -45,7 +46,7 @@ boolean reached(int numberOfRuns) { } } - public class Batch { + public static class Batch> { private final String name; private final Rule[] rules; private final Limiter limit; @@ -68,19 +69,19 @@ public String name() { } } - private final Iterable batches = batches(); + private final Iterable> batches = batches(); - protected abstract Iterable.Batch> batches(); + protected abstract Iterable> batches(); public class Transformation { private final TreeType before, after; - private final Rule rule; + private final String name; private Boolean lazyHasChanged; - Transformation(TreeType plan, Rule rule) { - this.rule = rule; - before = plan; - after = rule.apply(before); + Transformation(String name, TreeType plan, Function transform) { + this.name = name; + this.before = plan; + this.after = transform.apply(before); } public boolean hasChanged() { @@ -90,8 +91,8 @@ public boolean hasChanged() { return lazyHasChanged; } - public String ruleName() { - return rule.name(); + public String name() { + return name; } public TreeType before() { @@ -106,9 +107,9 @@ public TreeType after() { public class ExecutionInfo { private final TreeType before, after; - private final Map> transformations; + private final Map, List> transformations; - ExecutionInfo(TreeType before, TreeType after, Map> transformations) { + ExecutionInfo(TreeType before, TreeType after, Map, List> transformations) { this.before = before; this.after = after; this.transformations = transformations; @@ -122,23 +123,23 @@ public TreeType after() { return after; } - public Map> transformations() { + public Map, List> transformations() { return transformations; } } - protected TreeType execute(TreeType plan) { + protected final TreeType execute(TreeType plan) { return executeWithInfo(plan).after; } - protected ExecutionInfo executeWithInfo(TreeType plan) { + protected final ExecutionInfo executeWithInfo(TreeType plan) { TreeType currentPlan = plan; long totalDuration = 0; - Map> transformations = new LinkedHashMap<>(); + Map, List> transformations = new LinkedHashMap<>(); - for (Batch batch : batches) { + for (Batch batch : batches) { int batchRuns = 0; List tfs = new ArrayList<>(); transformations.put(batch, tfs); @@ -156,7 +157,7 @@ protected ExecutionInfo executeWithInfo(TreeType plan) { if (log.isTraceEnabled()) { log.trace("About to apply rule {}", rule); } - Transformation tf = new Transformation(currentPlan, rule); + Transformation tf = new Transformation(rule.name(), currentPlan, transform(rule)); tfs.add(tf); currentPlan = tf.after; @@ -198,4 +199,8 @@ protected ExecutionInfo executeWithInfo(TreeType plan) { return new ExecutionInfo(plan, currentPlan, transformations); } + + protected Function transform(Rule rule) { + return rule::apply; + } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/analyzer/Analyzer.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/analyzer/Analyzer.java index 14a25fc2afb4..c74771e9c794 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/analyzer/Analyzer.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/analyzer/Analyzer.java @@ -9,6 +9,7 @@ import org.elasticsearch.common.logging.LoggerMessageFormat; import org.elasticsearch.core.Tuple; import org.elasticsearch.xpack.ql.analyzer.AnalyzerRules.AddMissingEqualsToBoolField; +import org.elasticsearch.xpack.ql.analyzer.AnalyzerRules.ParameterizedAnalyzerRule; import org.elasticsearch.xpack.ql.capabilities.Resolvables; import org.elasticsearch.xpack.ql.common.Failure; import org.elasticsearch.xpack.ql.expression.Alias; @@ -28,7 +29,6 @@ import org.elasticsearch.xpack.ql.expression.UnresolvedStar; import org.elasticsearch.xpack.ql.expression.function.Function; import org.elasticsearch.xpack.ql.expression.function.FunctionDefinition; -import org.elasticsearch.xpack.ql.expression.function.FunctionRegistry; import org.elasticsearch.xpack.ql.expression.function.FunctionResolutionStrategy; import org.elasticsearch.xpack.ql.expression.function.Functions; import org.elasticsearch.xpack.ql.expression.function.UnresolvedFunction; @@ -43,6 +43,7 @@ import org.elasticsearch.xpack.ql.plan.logical.Project; import org.elasticsearch.xpack.ql.plan.logical.UnaryPlan; import org.elasticsearch.xpack.ql.plan.logical.UnresolvedRelation; +import org.elasticsearch.xpack.ql.rule.ParameterizedRuleExecutor; import org.elasticsearch.xpack.ql.rule.RuleExecutor; import org.elasticsearch.xpack.ql.type.DataType; import org.elasticsearch.xpack.ql.type.DataTypes; @@ -58,7 +59,6 @@ import org.elasticsearch.xpack.sql.plan.logical.Pivot; import org.elasticsearch.xpack.sql.plan.logical.SubQueryAlias; import org.elasticsearch.xpack.sql.plan.logical.With; -import org.elasticsearch.xpack.sql.session.SqlConfiguration; import org.elasticsearch.xpack.sql.type.SqlDataTypeConverter; import java.util.ArrayList; @@ -79,36 +79,13 @@ import static org.elasticsearch.xpack.ql.analyzer.AnalyzerRules.BaseAnalyzerRule; import static org.elasticsearch.xpack.ql.util.CollectionUtils.combine; -public class Analyzer extends RuleExecutor { - /** - * Valid functions. - */ - private final FunctionRegistry functionRegistry; - /** - * Information about the index against which the SQL is being analyzed. - */ - private final IndexResolution indexResolution; - /** - * Per-request specific settings needed in some of the functions (timezone, username and clustername), - * to which they are attached. - */ - private final SqlConfiguration configuration; - /** - * The verifier has the role of checking the analyzed tree for failures and build a list of failures. - */ - private final Verifier verifier; +public class Analyzer extends ParameterizedRuleExecutor { - public Analyzer(SqlConfiguration configuration, FunctionRegistry functionRegistry, IndexResolution results, Verifier verifier) { - this.configuration = configuration; - this.functionRegistry = functionRegistry; - this.indexResolution = results; - this.verifier = verifier; - } + private static final Iterable> rules; - @Override - protected Iterable.Batch> batches() { - Batch substitution = new Batch("Substitution", new CTESubstitution()); - Batch resolution = new Batch( + static { + var substitution = new Batch<>("Substitution", new CTESubstitution()); + var resolution = new Batch<>( "Resolution", new ResolveTable(), new ResolveRefs(), @@ -123,14 +100,30 @@ protected Iterable.Batch> batches() { new ResolveAggsInOrderBy() // new ImplicitCasting() ); - Batch finish = new Batch( + var finish = new Batch<>( "Finish Analysis", new ReplaceSubQueryAliases(), // Should be run before pruning SubqueryAliases new PruneSubQueryAliases(), new AddMissingEqualsToBoolField(), CleanAliases.INSTANCE ); - return Arrays.asList(substitution, resolution, finish); + rules = Arrays.asList(substitution, resolution, finish); + } + + /** + * The verifier has the role of checking the analyzed tree for failures and build a list of failures. + */ + private final Verifier verifier; + + public Analyzer(AnalyzerContext context, Verifier verifier) { + super(context); + context.analyzeWithoutVerify().set(this::execute); + this.verifier = verifier; + } + + @Override + protected Iterable> batches() { + return rules; } public LogicalPlan analyze(LogicalPlan plan) { @@ -149,7 +142,7 @@ public ExecutionInfo debugAnalyze(LogicalPlan plan) { } public LogicalPlan verify(LogicalPlan plan) { - Collection failures = verifier.verify(plan, configuration.version()); + Collection failures = verifier.verify(plan, context().configuration().version()); if (failures.isEmpty() == false) { throw new VerificationException(failures); } @@ -310,9 +303,10 @@ protected boolean skipResolved() { } } - private class ResolveTable extends AnalyzerRule { - @Override - protected LogicalPlan rule(UnresolvedRelation plan) { + private static class ResolveTable extends ParameterizedAnalyzerRule { + + protected LogicalPlan rule(UnresolvedRelation plan, AnalyzerContext context) { + IndexResolution indexResolution = context.indexResolution(); if (indexResolution.isValid() == false) { return plan.unresolvedMessage().equals(indexResolution.toString()) ? plan @@ -339,7 +333,7 @@ protected LogicalPlan rule(UnresolvedRelation plan) { } } - private class ResolveRefs extends BaseAnalyzerRule { + private static class ResolveRefs extends BaseAnalyzerRule { @Override protected LogicalPlan doRule(LogicalPlan plan) { @@ -901,10 +895,12 @@ private Expression replaceAliases(Expression condition, List { + private static class ResolveFunctions extends ParameterizedAnalyzerRule { @Override - protected LogicalPlan rule(LogicalPlan plan) { + protected LogicalPlan rule(LogicalPlan plan, AnalyzerContext context) { + var functionRegistry = context.functionRegistry(); + var configuration = context.configuration(); return plan.transformExpressionsUp(UnresolvedFunction.class, uf -> { if (uf.analyzed()) { return uf; @@ -1063,7 +1059,7 @@ protected boolean skipResolved() { // Handle aggs in HAVING. To help folding any aggs not found in Aggregation // will be pushed down to the Aggregate and then projected. This also simplifies the Verifier's job. // - private class ResolveAggsInHaving extends AnalyzerRule { + private static class ResolveAggsInHaving extends ParameterizedAnalyzerRule { @Override protected boolean skipResolved() { @@ -1071,7 +1067,7 @@ protected boolean skipResolved() { } @Override - protected LogicalPlan rule(Filter f) { + protected LogicalPlan rule(Filter f, AnalyzerContext context) { // HAVING = Filter followed by an Agg // tag::noformat - https://bugs.eclipse.org/bugs/show_bug.cgi?id=574437 if (f.child() instanceof Aggregate agg && agg.resolved()) { @@ -1092,7 +1088,8 @@ protected LogicalPlan rule(Filter f) { combine(agg.aggregates(), new Alias(f.source(), ".having", condition)) ); - tryResolvingCondition = (Aggregate) analyze(tryResolvingCondition, false); + var analyze = context.analyzeWithoutVerify().get(); + tryResolvingCondition = (Aggregate) analyze.apply(tryResolvingCondition); // if it got resolved if (tryResolvingCondition.resolved()) { diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/analyzer/AnalyzerContext.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/analyzer/AnalyzerContext.java new file mode 100644 index 000000000000..8c0ff5e78e94 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/analyzer/AnalyzerContext.java @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.sql.analysis.analyzer; + +import org.elasticsearch.xpack.ql.expression.function.FunctionRegistry; +import org.elasticsearch.xpack.ql.index.IndexResolution; +import org.elasticsearch.xpack.ql.plan.logical.LogicalPlan; +import org.elasticsearch.xpack.ql.util.Holder; +import org.elasticsearch.xpack.sql.session.SqlConfiguration; + +import java.util.function.Function; + +public record AnalyzerContext( + SqlConfiguration configuration, + FunctionRegistry functionRegistry, + IndexResolution indexResolution, + Holder> analyzeWithoutVerify +) { + public AnalyzerContext(SqlConfiguration configuration, FunctionRegistry functionRegistry, IndexResolution indexResolution) { + this(configuration, functionRegistry, indexResolution, new Holder<>()); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/optimizer/Optimizer.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/optimizer/Optimizer.java index 617c9226b493..cdfac737e7fe 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/optimizer/Optimizer.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/optimizer/Optimizer.java @@ -121,8 +121,8 @@ public LogicalPlan optimize(LogicalPlan verified) { } @Override - protected Iterable.Batch> batches() { - Batch substitutions = new Batch( + protected Iterable> batches() { + var substitutions = new Batch<>( "Substitutions", Limiter.ONCE, new RewritePivot(), @@ -130,9 +130,9 @@ protected Iterable.Batch> batches() { new ReplaceAggregatesWithLiterals() ); - Batch refs = new Batch("Replace References", Limiter.ONCE, new ReplaceReferenceAttributeWithSource()); + var refs = new Batch<>("Replace References", Limiter.ONCE, new ReplaceReferenceAttributeWithSource()); - Batch operators = new Batch( + var operators = new Batch<>( "Operator Optimization", // combining new CombineProjections(), @@ -166,7 +166,7 @@ protected Iterable.Batch> batches() { new PushDownAndCombineFilters() ); - Batch aggregate = new Batch( + var aggregate = new Batch<>( "Aggregation Rewrite", new ReplaceMinMaxWithTopHits(), new ReplaceAggsWithMatrixStats(), @@ -178,7 +178,7 @@ protected Iterable.Batch> batches() { new ReplaceAggsWithPercentileRanks() ); - Batch local = new Batch( + var local = new Batch<>( "Skip Elasticsearch", new SkipQueryOnLimitZero(), new SkipQueryForLiteralAggregations(), @@ -188,7 +188,7 @@ protected Iterable.Batch> batches() { new PruneLiteralsInGroupBy() ); - Batch label = new Batch("Set as Optimized", Limiter.ONCE, CleanAliases.INSTANCE, new SetAsOptimized()); + var label = new Batch<>("Set as Optimized", Limiter.ONCE, CleanAliases.INSTANCE, new SetAsOptimized()); return Arrays.asList(substitutions, refs, operators, aggregate, local, label); } @@ -588,7 +588,6 @@ public LogicalPlan apply(LogicalPlan plan) { return rule(plan); } - @Override protected LogicalPlan rule(LogicalPlan plan) { Map aliases = new LinkedHashMap<>(); List attrs = new ArrayList<>(); @@ -1262,7 +1261,6 @@ abstract static class OptimizerBasicRule extends Rule @Override public abstract LogicalPlan apply(LogicalPlan plan); - @Override protected LogicalPlan rule(LogicalPlan plan) { return plan; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/command/Debug.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/command/Debug.java index cbc97856682a..56754c48442b 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/command/Debug.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/command/Debug.java @@ -106,7 +106,7 @@ private void handleInfo(ExecutionInfo info, ActionListener listener) { sb.append(entry.getKey().name()); sb.append("***"); for (Transformation tf : entry.getValue()) { - sb.append(tf.ruleName()); + sb.append(tf.name()); sb.append("\n"); sb.append(NodeUtils.diffString(tf.before(), tf.after())); sb.append("\n"); @@ -127,7 +127,7 @@ private void handleInfo(ExecutionInfo info, ActionListener listener) { int counter = 0; for (Transformation tf : entry.getValue()) { if (tf.hasChanged()) { - plans.put(tf.ruleName() + "#" + ++counter, tf.after()); + plans.put(tf.name() + "#" + ++counter, tf.after()); } } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/planner/Mapper.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/planner/Mapper.java index 50012d607699..2f94b5e869a3 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/planner/Mapper.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/planner/Mapper.java @@ -45,8 +45,8 @@ public PhysicalPlan map(LogicalPlan plan) { } @Override - protected Iterable.Batch> batches() { - Batch conversion = new Batch("Mapping", new JoinMapper(), new SimpleExecMapper()); + protected Iterable> batches() { + var conversion = new Batch<>("Mapping", new JoinMapper(), new SimpleExecMapper()); return Arrays.asList(conversion); } @@ -136,7 +136,6 @@ public final PhysicalPlan apply(PhysicalPlan plan) { } @SuppressWarnings("unchecked") - @Override protected final PhysicalPlan rule(UnplannedExec plan) { LogicalPlan subPlan = plan.plan(); if (subPlanToken.isInstance(subPlan)) { diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/planner/QueryFolder.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/planner/QueryFolder.java index ecdc6fe86a7d..e566f1dd0c34 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/planner/QueryFolder.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/planner/QueryFolder.java @@ -116,8 +116,8 @@ PhysicalPlan fold(PhysicalPlan plan) { } @Override - protected Iterable.Batch> batches() { - Batch rollup = new Batch( + protected Iterable> batches() { + var rollup = new Batch<>( "Fold queries", new FoldPivot(), new FoldAggregate(), @@ -127,9 +127,8 @@ protected Iterable.Batch> batches() { new FoldLimit() ); - Batch local = new Batch("Local queries", new LocalLimit(), new PropagateEmptyLocal()); - - Batch finish = new Batch("Finish query", Limiter.ONCE, new PlanOutputToQueryRef()); + var local = new Batch<>("Local queries", new LocalLimit(), new PropagateEmptyLocal()); + var finish = new Batch<>("Finish query", Limiter.ONCE, new PlanOutputToQueryRef()); return Arrays.asList(rollup, local, finish); } @@ -943,7 +942,6 @@ public final PhysicalPlan apply(PhysicalPlan plan) { return plan.transformUp(typeToken(), this::rule); } - @Override protected abstract PhysicalPlan rule(SubPlan plan); } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/session/SqlSession.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/session/SqlSession.java index 85b411344989..2b333f41047a 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/session/SqlSession.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/session/SqlSession.java @@ -20,6 +20,7 @@ import org.elasticsearch.xpack.ql.plan.logical.LogicalPlan; import org.elasticsearch.xpack.ql.rule.RuleExecutor; import org.elasticsearch.xpack.sql.analysis.analyzer.Analyzer; +import org.elasticsearch.xpack.sql.analysis.analyzer.AnalyzerContext; import org.elasticsearch.xpack.sql.analysis.analyzer.PreAnalyzer; import org.elasticsearch.xpack.sql.analysis.analyzer.PreAnalyzer.PreAnalysis; import org.elasticsearch.xpack.sql.analysis.analyzer.TableInfo; @@ -116,12 +117,12 @@ public void analyzedPlan(LogicalPlan parsed, boolean verify, ActionListener { - Analyzer analyzer = new Analyzer( + AnalyzerContext context = new AnalyzerContext( configuration, functionRegistry, - IndexCompatibility.compatible(r, Version.fromId(configuration.version().id)), - verifier + IndexCompatibility.compatible(r, Version.fromId(configuration.version().id)) ); + Analyzer analyzer = new Analyzer(context, verifier); return analyzer.analyze(parsed, verify); }, listener); } @@ -133,7 +134,8 @@ public void debugAnalyzedPlan(LogicalPlan parsed, ActionListener { - Analyzer analyzer = new Analyzer(configuration, functionRegistry, r, verifier); + AnalyzerContext context = new AnalyzerContext(configuration, functionRegistry, r); + Analyzer analyzer = new Analyzer(context, verifier); return analyzer.debugAnalyze(parsed); }, listener); } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/AnalyzerTestUtils.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/AnalyzerTestUtils.java new file mode 100644 index 000000000000..38e14e9ab401 --- /dev/null +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/AnalyzerTestUtils.java @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.sql.analysis.analyzer; + +import org.elasticsearch.xpack.ql.expression.function.FunctionRegistry; +import org.elasticsearch.xpack.ql.index.IndexResolution; +import org.elasticsearch.xpack.sql.expression.function.SqlFunctionRegistry; +import org.elasticsearch.xpack.sql.session.SqlConfiguration; +import org.elasticsearch.xpack.sql.stats.Metrics; + +import static org.elasticsearch.xpack.sql.SqlTestUtils.TEST_CFG; + +public final class AnalyzerTestUtils { + + private AnalyzerTestUtils() {} + + public static Analyzer analyzer(IndexResolution resolution) { + return analyzer(TEST_CFG, new SqlFunctionRegistry(), resolution); + } + + public static Analyzer analyzer(IndexResolution resolution, Verifier verifier) { + return analyzer(TEST_CFG, new SqlFunctionRegistry(), resolution, verifier); + } + + public static Analyzer analyzer(SqlConfiguration configuration, IndexResolution resolution) { + return analyzer(configuration, new SqlFunctionRegistry(), resolution); + } + + public static Analyzer analyzer(SqlConfiguration configuration, FunctionRegistry registry, IndexResolution resolution) { + return analyzer(configuration, registry, resolution, new Verifier(new Metrics())); + } + + public static Analyzer analyzer( + SqlConfiguration configuration, + FunctionRegistry registry, + IndexResolution resolution, + Verifier verifier + ) { + return new Analyzer(new AnalyzerContext(configuration, registry, resolution), verifier); + } +} diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/AnalyzerTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/AnalyzerTests.java index ff68f6f3b11c..bac11297fef8 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/AnalyzerTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/AnalyzerTests.java @@ -35,13 +35,14 @@ public class AnalyzerTests extends ESTestCase { private final SqlParser parser = new SqlParser(); - private final Analyzer analyzer = new Analyzer( + private final AnalyzerContext context = new AnalyzerContext( SqlTestUtils.TEST_CFG, new SqlFunctionRegistry(), - IndexResolution.valid(new EsIndex("test", loadMapping("mapping-basic.json"))), - new Verifier(new Metrics()) + IndexResolution.valid(new EsIndex("test", loadMapping("mapping-basic.json"))) ); + private final Analyzer analyzer = new Analyzer(context, new Verifier(new Metrics())); + private LogicalPlan analyze(String sql) { return analyzer.analyze(parser.createStatement(sql), false); } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/FieldAttributeTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/FieldAttributeTests.java index fa7151e1f875..41518ce093d2 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/FieldAttributeTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/FieldAttributeTests.java @@ -76,7 +76,7 @@ public FieldAttributeTests() { EsIndex test = new EsIndex("test", mapping); getIndexResult = IndexResolution.valid(test); - analyzer = new Analyzer(SqlTestUtils.TEST_CFG, functionRegistry, getIndexResult, verifier); + analyzer = analyzer(functionRegistry, getIndexResult, verifier); } private LogicalPlan plan(String sql) { @@ -197,7 +197,7 @@ public void testFieldAmbiguity() { EsIndex index = new EsIndex("test", mapping); getIndexResult = IndexResolution.valid(index); - analyzer = new Analyzer(SqlTestUtils.TEST_CFG, functionRegistry, getIndexResult, verifier); + analyzer = analyzer(functionRegistry, getIndexResult, verifier); VerificationException ex = expectThrows(VerificationException.class, () -> plan("SELECT test.bar FROM test")); assertEquals( @@ -232,7 +232,7 @@ public void testAggregations() { Map mapping = TypesTests.loadMapping("mapping-basic.json"); EsIndex index = new EsIndex("test", mapping); getIndexResult = IndexResolution.valid(index); - analyzer = new Analyzer(SqlTestUtils.TEST_CFG, functionRegistry, getIndexResult, verifier); + analyzer = analyzer(functionRegistry, getIndexResult, verifier); LogicalPlan plan = plan("SELECT sum(salary) AS s FROM test"); assertThat(plan, instanceOf(Aggregate.class)); @@ -265,7 +265,7 @@ public void testGroupByAmbiguity() { Map mapping = TypesTests.loadMapping("mapping-basic.json"); EsIndex index = new EsIndex("test", mapping); getIndexResult = IndexResolution.valid(index); - analyzer = new Analyzer(SqlTestUtils.TEST_CFG, functionRegistry, getIndexResult, verifier); + analyzer = analyzer(functionRegistry, getIndexResult, verifier); VerificationException ex = expectThrows( VerificationException.class, @@ -323,7 +323,7 @@ public void testUnsignedLongVersionCompatibility() { SqlConfiguration sqlConfig = SqlTestUtils.randomConfiguration(SqlVersion.fromId(preUnsignedLong.id)); for (String sql : List.of(query, queryWithLiteral, queryWithCastLiteral, queryWithAlias, queryWithArithmetic, queryWithCast)) { - analyzer = new Analyzer( + analyzer = analyzer( sqlConfig, functionRegistry, loadCompatibleIndexResolution("mapping-numeric.json", preUnsignedLong), @@ -333,7 +333,7 @@ public void testUnsignedLongVersionCompatibility() { assertThat(ex.getMessage(), containsString("Found 1 problem\nline 1:8: Cannot use field [unsigned_long]")); for (Version v : List.of(INTRODUCING_UNSIGNED_LONG, postUnsignedLong)) { - analyzer = new Analyzer( + analyzer = analyzer( SqlTestUtils.randomConfiguration(SqlVersion.fromId(v.id)), functionRegistry, loadCompatibleIndexResolution("mapping-numeric.json", v), @@ -362,7 +362,7 @@ public void testVersionTypeVersionCompatibility() { SqlConfiguration sqlConfig = SqlTestUtils.randomConfiguration(SqlVersion.fromId(preVersion.id)); for (String sql : List.of(query, queryWithCastLiteral, queryWithAlias, queryWithCast)) { - analyzer = new Analyzer( + analyzer = analyzer( sqlConfig, functionRegistry, loadCompatibleIndexResolution("mapping-version.json", preVersion), @@ -372,7 +372,7 @@ public void testVersionTypeVersionCompatibility() { assertThat(ex.getMessage(), containsString("Cannot use field [version_number]")); for (Version v : List.of(INTRODUCING_VERSION_FIELD_TYPE, postVersion)) { - analyzer = new Analyzer( + analyzer = analyzer( SqlTestUtils.randomConfiguration(SqlVersion.fromId(v.id)), functionRegistry, loadCompatibleIndexResolution("mapping-version.json", v), @@ -393,7 +393,7 @@ public void testVersionTypeVersionCompatibility() { public void testNonProjectedUnsignedLongVersionCompatibility() { Version preUnsignedLong = Version.fromId(INTRODUCING_UNSIGNED_LONG.id - SqlVersion.MINOR_MULTIPLIER); SqlConfiguration sqlConfig = SqlTestUtils.randomConfiguration(SqlVersion.fromId(preUnsignedLong.id)); - analyzer = new Analyzer( + analyzer = analyzer( sqlConfig, functionRegistry, loadCompatibleIndexResolution("mapping-numeric.json", preUnsignedLong), @@ -427,7 +427,7 @@ public void testNestedUnsignedLongVersionCompatibility() { String sql = "SELECT container.ul as unsigned_long FROM test"; Version preUnsignedLong = Version.fromId(INTRODUCING_UNSIGNED_LONG.id - SqlVersion.MINOR_MULTIPLIER); - analyzer = new Analyzer( + analyzer = analyzer( SqlTestUtils.randomConfiguration(SqlVersion.fromId(preUnsignedLong.id)), functionRegistry, compatibleIndexResolution(props, preUnsignedLong), @@ -438,7 +438,7 @@ public void testNestedUnsignedLongVersionCompatibility() { Version postUnsignedLong = Version.fromId(INTRODUCING_UNSIGNED_LONG.id + SqlVersion.MINOR_MULTIPLIER); for (Version v : List.of(INTRODUCING_UNSIGNED_LONG, postUnsignedLong)) { - analyzer = new Analyzer( + analyzer = analyzer( SqlTestUtils.randomConfiguration(SqlVersion.fromId(v.id)), functionRegistry, compatibleIndexResolution(props, v), @@ -463,7 +463,7 @@ public void testUnsignedLongStarExpandedVersionControlled() { for (SqlVersion version : List.of(preUnsignedLong, SqlVersion.fromId(INTRODUCING_UNSIGNED_LONG.id), postUnsignedLong)) { SqlConfiguration config = SqlTestUtils.randomConfiguration(version); // the mapping is mutated when making it "compatible", so it needs to be reloaded inside the loop. - analyzer = new Analyzer( + analyzer = analyzer( config, functionRegistry, loadCompatibleIndexResolution("mapping-numeric.json", Version.fromId(version.id)), @@ -481,7 +481,7 @@ public void testUnsignedLongStarExpandedVersionControlled() { } public void testFunctionOverNonExistingFieldAsArgumentAndSameAlias() throws Exception { - analyzer = new Analyzer(SqlTestUtils.TEST_CFG, functionRegistry, loadIndexResolution("mapping-basic.json"), verifier); + analyzer = analyzer(SqlTestUtils.TEST_CFG, functionRegistry, loadIndexResolution("mapping-basic.json"), verifier); VerificationException ex = expectThrows( VerificationException.class, @@ -491,7 +491,7 @@ public void testFunctionOverNonExistingFieldAsArgumentAndSameAlias() throws Exce } public void testFunctionWithExpressionOverNonExistingFieldAsArgumentAndSameAlias() throws Exception { - analyzer = new Analyzer(SqlTestUtils.TEST_CFG, functionRegistry, loadIndexResolution("mapping-basic.json"), verifier); + analyzer = analyzer(SqlTestUtils.TEST_CFG, functionRegistry, loadIndexResolution("mapping-basic.json"), verifier); VerificationException ex = expectThrows( VerificationException.class, @@ -503,7 +503,7 @@ public void testFunctionWithExpressionOverNonExistingFieldAsArgumentAndSameAlias public void testExpandStarOnIndexWithoutColumns() { EsIndex test = new EsIndex("test", Collections.emptyMap()); getIndexResult = IndexResolution.valid(test); - analyzer = new Analyzer(SqlTestUtils.TEST_CFG, functionRegistry, getIndexResult, verifier); + analyzer = analyzer(SqlTestUtils.TEST_CFG, functionRegistry, getIndexResult, verifier); LogicalPlan plan = plan("SELECT * FROM test"); @@ -529,4 +529,17 @@ private static IndexResolution compatibleIndexResolution(String properties, Vers EsIndex index = new EsIndex("test", mapping); return IndexCompatibility.compatible(IndexResolution.valid(index), version); } + + private static Analyzer analyzer( + SqlConfiguration configuration, + FunctionRegistry functionRegistry, + IndexResolution resolution, + Verifier verifier + ) { + return new Analyzer(new AnalyzerContext(configuration, functionRegistry, resolution), verifier); + } + + private static Analyzer analyzer(FunctionRegistry functionRegistry, IndexResolution resolution, Verifier verifier) { + return analyzer(SqlTestUtils.TEST_CFG, functionRegistry, resolution, verifier); + } } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java index 480f955b68f2..6fb3122fb0eb 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java @@ -12,7 +12,6 @@ import org.elasticsearch.xpack.ql.plan.logical.LogicalPlan; import org.elasticsearch.xpack.ql.type.EsField; import org.elasticsearch.xpack.sql.analysis.index.IndexResolverTests; -import org.elasticsearch.xpack.sql.expression.function.SqlFunctionRegistry; import org.elasticsearch.xpack.sql.expression.function.aggregate.First; import org.elasticsearch.xpack.sql.expression.function.aggregate.Last; import org.elasticsearch.xpack.sql.expression.function.scalar.math.Round; @@ -25,7 +24,6 @@ import org.elasticsearch.xpack.sql.expression.predicate.conditional.Least; import org.elasticsearch.xpack.sql.expression.predicate.conditional.NullIf; import org.elasticsearch.xpack.sql.parser.SqlParser; -import org.elasticsearch.xpack.sql.stats.Metrics; import java.util.Arrays; import java.util.HashMap; @@ -39,7 +37,7 @@ import static java.util.Collections.singletonMap; import static org.elasticsearch.xpack.ql.type.DataTypes.KEYWORD; import static org.elasticsearch.xpack.ql.type.DataTypes.OBJECT; -import static org.elasticsearch.xpack.sql.SqlTestUtils.TEST_CFG; +import static org.elasticsearch.xpack.sql.analysis.analyzer.AnalyzerTestUtils.analyzer; import static org.elasticsearch.xpack.sql.types.SqlTypesTests.loadMapping; public class VerifierErrorMessagesTests extends ESTestCase { @@ -54,7 +52,7 @@ private String error(String sql) { } private String error(IndexResolution getIndexResult, String sql) { - Analyzer analyzer = new Analyzer(TEST_CFG, new SqlFunctionRegistry(), getIndexResult, new Verifier(new Metrics())); + Analyzer analyzer = analyzer(getIndexResult); VerificationException e = expectThrows(VerificationException.class, () -> analyzer.analyze(parser.createStatement(sql), true)); String message = e.getMessage(); assertTrue(message.startsWith("Found ")); @@ -74,7 +72,7 @@ private EsIndex getTestEsIndex() { } private LogicalPlan accept(IndexResolution resolution, String sql) { - Analyzer analyzer = new Analyzer(TEST_CFG, new SqlFunctionRegistry(), resolution, new Verifier(new Metrics())); + Analyzer analyzer = analyzer(resolution); return analyzer.analyze(parser.createStatement(sql), true); } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/DatabaseFunctionTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/DatabaseFunctionTests.java index fb3a42aca0db..e62eb570f9a8 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/DatabaseFunctionTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/DatabaseFunctionTests.java @@ -15,15 +15,14 @@ import org.elasticsearch.xpack.ql.plan.logical.Project; import org.elasticsearch.xpack.sql.action.Protocol; import org.elasticsearch.xpack.sql.analysis.analyzer.Analyzer; -import org.elasticsearch.xpack.sql.analysis.analyzer.Verifier; -import org.elasticsearch.xpack.sql.expression.function.SqlFunctionRegistry; import org.elasticsearch.xpack.sql.parser.SqlParser; import org.elasticsearch.xpack.sql.proto.Mode; import org.elasticsearch.xpack.sql.session.SqlConfiguration; -import org.elasticsearch.xpack.sql.stats.Metrics; import org.elasticsearch.xpack.sql.types.SqlTypesTests; import org.elasticsearch.xpack.sql.util.DateUtils; +import static org.elasticsearch.xpack.sql.analysis.analyzer.AnalyzerTestUtils.analyzer; + public class DatabaseFunctionTests extends ESTestCase { public void testDatabaseFunctionOutput() { @@ -49,7 +48,7 @@ public void testDatabaseFunctionOutput() { null, randomBoolean() ); - Analyzer analyzer = new Analyzer(sqlConfig, new SqlFunctionRegistry(), IndexResolution.valid(test), new Verifier(new Metrics())); + Analyzer analyzer = analyzer(sqlConfig, IndexResolution.valid(test)); Project result = (Project) analyzer.analyze(parser.createStatement("SELECT DATABASE()"), true); NamedExpression ne = result.projections().get(0); diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/UserFunctionTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/UserFunctionTests.java index 21a73f1b445a..a939b3c5540a 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/UserFunctionTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/UserFunctionTests.java @@ -15,15 +15,14 @@ import org.elasticsearch.xpack.ql.plan.logical.Project; import org.elasticsearch.xpack.sql.action.Protocol; import org.elasticsearch.xpack.sql.analysis.analyzer.Analyzer; -import org.elasticsearch.xpack.sql.analysis.analyzer.Verifier; -import org.elasticsearch.xpack.sql.expression.function.SqlFunctionRegistry; import org.elasticsearch.xpack.sql.parser.SqlParser; import org.elasticsearch.xpack.sql.proto.Mode; import org.elasticsearch.xpack.sql.session.SqlConfiguration; -import org.elasticsearch.xpack.sql.stats.Metrics; import org.elasticsearch.xpack.sql.types.SqlTypesTests; import org.elasticsearch.xpack.sql.util.DateUtils; +import static org.elasticsearch.xpack.sql.analysis.analyzer.AnalyzerTestUtils.analyzer; + public class UserFunctionTests extends ESTestCase { public void testNoUsernameFunctionOutput() { @@ -48,7 +47,7 @@ public void testNoUsernameFunctionOutput() { null, randomBoolean() ); - Analyzer analyzer = new Analyzer(sqlConfig, new SqlFunctionRegistry(), IndexResolution.valid(test), new Verifier(new Metrics())); + Analyzer analyzer = analyzer(sqlConfig, IndexResolution.valid(test)); Project result = (Project) analyzer.analyze(parser.createStatement("SELECT USER()"), true); NamedExpression ne = result.projections().get(0); diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/CurrentDateTimeTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/CurrentDateTimeTests.java index 090794b86941..c14daee2c143 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/CurrentDateTimeTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/CurrentDateTimeTests.java @@ -15,11 +15,8 @@ import org.elasticsearch.xpack.ql.tree.AbstractNodeTestCase; import org.elasticsearch.xpack.sql.SqlTestUtils; import org.elasticsearch.xpack.sql.analysis.analyzer.Analyzer; -import org.elasticsearch.xpack.sql.analysis.analyzer.Verifier; -import org.elasticsearch.xpack.sql.expression.function.SqlFunctionRegistry; import org.elasticsearch.xpack.sql.parser.ParsingException; import org.elasticsearch.xpack.sql.parser.SqlParser; -import org.elasticsearch.xpack.sql.stats.Metrics; import org.elasticsearch.xpack.sql.types.SqlTypesTests; import java.time.ZoneId; @@ -29,6 +26,7 @@ import static org.elasticsearch.xpack.ql.tree.Source.EMPTY; import static org.elasticsearch.xpack.sql.SqlTestUtils.literal; +import static org.elasticsearch.xpack.sql.analysis.analyzer.AnalyzerTestUtils.analyzer; public class CurrentDateTimeTests extends AbstractNodeTestCase { @@ -93,7 +91,7 @@ public void testInvalidPrecision() { new EsIndex("test", SqlTypesTests.loadMapping("mapping-multi-field-with-nested.json")) ); - Analyzer analyzer = new Analyzer(SqlTestUtils.TEST_CFG, new SqlFunctionRegistry(), indexResolution, new Verifier(new Metrics())); + Analyzer analyzer = analyzer(indexResolution); ParsingException e = expectThrows( ParsingException.class, () -> analyzer.analyze(parser.createStatement("SELECT CURRENT_TIMESTAMP(100000000000000)"), true) diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/CurrentTimeTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/CurrentTimeTests.java index 4c845728f1c2..6f4f9a646e71 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/CurrentTimeTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/CurrentTimeTests.java @@ -15,11 +15,8 @@ import org.elasticsearch.xpack.ql.tree.AbstractNodeTestCase; import org.elasticsearch.xpack.sql.SqlTestUtils; import org.elasticsearch.xpack.sql.analysis.analyzer.Analyzer; -import org.elasticsearch.xpack.sql.analysis.analyzer.Verifier; -import org.elasticsearch.xpack.sql.expression.function.SqlFunctionRegistry; import org.elasticsearch.xpack.sql.parser.ParsingException; import org.elasticsearch.xpack.sql.parser.SqlParser; -import org.elasticsearch.xpack.sql.stats.Metrics; import org.elasticsearch.xpack.sql.types.SqlTypesTests; import java.time.OffsetTime; @@ -30,6 +27,7 @@ import static org.elasticsearch.xpack.ql.tree.Source.EMPTY; import static org.elasticsearch.xpack.sql.SqlTestUtils.literal; +import static org.elasticsearch.xpack.sql.analysis.analyzer.AnalyzerTestUtils.analyzer; public class CurrentTimeTests extends AbstractNodeTestCase { @@ -94,7 +92,7 @@ public void testInvalidPrecision() { new EsIndex("test", SqlTypesTests.loadMapping("mapping-multi-field-with-nested.json")) ); - Analyzer analyzer = new Analyzer(SqlTestUtils.TEST_CFG, new SqlFunctionRegistry(), indexResolution, new Verifier(new Metrics())); + Analyzer analyzer = analyzer(indexResolution); ParsingException e = expectThrows( ParsingException.class, () -> analyzer.analyze(parser.createStatement("SELECT CURRENT_TIME(100000000000000)"), true) diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerRunTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerRunTests.java index c56a2eaeae34..9ce49721ba2a 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerRunTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerRunTests.java @@ -12,7 +12,6 @@ import org.elasticsearch.xpack.ql.expression.FieldAttribute; import org.elasticsearch.xpack.ql.expression.Literal; import org.elasticsearch.xpack.ql.expression.UnresolvedAttribute; -import org.elasticsearch.xpack.ql.expression.function.FunctionRegistry; import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.BinaryComparison; import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.Equals; import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.GreaterThan; @@ -28,11 +27,8 @@ import org.elasticsearch.xpack.ql.plan.logical.LogicalPlan; import org.elasticsearch.xpack.ql.plan.logical.UnaryPlan; import org.elasticsearch.xpack.ql.type.EsField; -import org.elasticsearch.xpack.sql.SqlTestUtils; import org.elasticsearch.xpack.sql.analysis.analyzer.Analyzer; -import org.elasticsearch.xpack.sql.analysis.analyzer.Verifier; import org.elasticsearch.xpack.sql.parser.SqlParser; -import org.elasticsearch.xpack.sql.stats.Metrics; import org.elasticsearch.xpack.sql.types.SqlTypesTests; import java.time.ZonedDateTime; @@ -48,12 +44,12 @@ import static org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.BinaryComparisonProcessor.BinaryComparisonOperation.LTE; import static org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.BinaryComparisonProcessor.BinaryComparisonOperation.NEQ; import static org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.BinaryComparisonProcessor.BinaryComparisonOperation.NULLEQ; +import static org.elasticsearch.xpack.sql.analysis.analyzer.AnalyzerTestUtils.analyzer; public class OptimizerRunTests extends ESTestCase { private final SqlParser parser; private final IndexResolution getIndexResult; - private final FunctionRegistry functionRegistry; private final Analyzer analyzer; private final Optimizer optimizer; private static final Map> COMPARISONS = new HashMap<>() { @@ -71,13 +67,12 @@ public class OptimizerRunTests extends ESTestCase { public OptimizerRunTests() { parser = new SqlParser(); - functionRegistry = new FunctionRegistry(); Map mapping = SqlTypesTests.loadMapping("mapping-multi-field-variation.json"); EsIndex test = new EsIndex("test", mapping); getIndexResult = IndexResolution.valid(test); - analyzer = new Analyzer(SqlTestUtils.TEST_CFG, functionRegistry, getIndexResult, new Verifier(new Metrics())); + analyzer = analyzer(getIndexResult); optimizer = new Optimizer(); } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysColumnsTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysColumnsTests.java index fe903ff45571..df14cb132af9 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysColumnsTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysColumnsTests.java @@ -10,7 +10,6 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.core.Tuple; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.ql.expression.function.FunctionRegistry; import org.elasticsearch.xpack.ql.index.EsIndex; import org.elasticsearch.xpack.ql.index.IndexCompatibility; import org.elasticsearch.xpack.ql.index.IndexResolution; @@ -18,7 +17,6 @@ import org.elasticsearch.xpack.ql.type.EsField; import org.elasticsearch.xpack.sql.action.Protocol; import org.elasticsearch.xpack.sql.analysis.analyzer.Analyzer; -import org.elasticsearch.xpack.sql.analysis.analyzer.Verifier; import org.elasticsearch.xpack.sql.parser.SqlParser; import org.elasticsearch.xpack.sql.plan.logical.command.Command; import org.elasticsearch.xpack.sql.proto.Mode; @@ -28,7 +26,6 @@ import org.elasticsearch.xpack.sql.session.SchemaRowSet; import org.elasticsearch.xpack.sql.session.SqlConfiguration; import org.elasticsearch.xpack.sql.session.SqlSession; -import org.elasticsearch.xpack.sql.stats.Metrics; import org.elasticsearch.xpack.sql.util.DateUtils; import java.sql.Types; @@ -50,6 +47,7 @@ import static org.elasticsearch.xpack.ql.index.VersionCompatibilityChecks.isTypeSupportedInVersion; import static org.elasticsearch.xpack.ql.type.DataTypes.UNSIGNED_LONG; import static org.elasticsearch.xpack.ql.type.DataTypes.VERSION; +import static org.elasticsearch.xpack.sql.analysis.analyzer.AnalyzerTestUtils.analyzer; import static org.elasticsearch.xpack.sql.proto.Mode.isDriver; import static org.elasticsearch.xpack.sql.types.SqlTypesTests.loadMapping; import static org.mockito.ArgumentMatchers.any; @@ -392,7 +390,7 @@ private Tuple sql( Map mapping ) { EsIndex test = new EsIndex("test", mapping); - Analyzer analyzer = new Analyzer(config, new FunctionRegistry(), IndexResolution.valid(test), new Verifier(new Metrics())); + Analyzer analyzer = analyzer(config, IndexResolution.valid(test)); Command cmd = (Command) analyzer.analyze(parser.createStatement(sql, params, UTC), true); IndexResolver resolver = mock(IndexResolver.class); diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysTablesTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysTablesTests.java index 11f2d09be613..9316c663bdfc 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysTablesTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysTablesTests.java @@ -9,7 +9,6 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.core.Tuple; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.ql.expression.function.FunctionRegistry; import org.elasticsearch.xpack.ql.index.EsIndex; import org.elasticsearch.xpack.ql.index.IndexResolution; import org.elasticsearch.xpack.ql.index.IndexResolver; @@ -20,7 +19,6 @@ import org.elasticsearch.xpack.sql.SqlTestUtils; import org.elasticsearch.xpack.sql.action.Protocol; import org.elasticsearch.xpack.sql.analysis.analyzer.Analyzer; -import org.elasticsearch.xpack.sql.analysis.analyzer.Verifier; import org.elasticsearch.xpack.sql.parser.SqlParser; import org.elasticsearch.xpack.sql.plan.logical.command.Command; import org.elasticsearch.xpack.sql.proto.Mode; @@ -28,7 +26,6 @@ import org.elasticsearch.xpack.sql.session.SchemaRowSet; import org.elasticsearch.xpack.sql.session.SqlConfiguration; import org.elasticsearch.xpack.sql.session.SqlSession; -import org.elasticsearch.xpack.sql.stats.Metrics; import org.elasticsearch.xpack.sql.types.SqlTypesTests; import org.elasticsearch.xpack.sql.util.DateUtils; @@ -44,6 +41,7 @@ import static org.elasticsearch.action.ActionListener.wrap; import static org.elasticsearch.xpack.ql.index.IndexResolver.SQL_TABLE; import static org.elasticsearch.xpack.ql.index.IndexResolver.SQL_VIEW; +import static org.elasticsearch.xpack.sql.analysis.analyzer.AnalyzerTestUtils.analyzer; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; @@ -368,12 +366,7 @@ private SqlTypedParamValue param(Object value) { private Tuple sql(String sql, List params, SqlConfiguration cfg) { EsIndex test = new EsIndex("test", mapping); - Analyzer analyzer = new Analyzer( - SqlTestUtils.TEST_CFG, - new FunctionRegistry(), - IndexResolution.valid(test), - new Verifier(new Metrics()) - ); + Analyzer analyzer = analyzer(IndexResolution.valid(test)); Command cmd = (Command) analyzer.analyze(parser.createStatement(sql, params, cfg.zoneId()), true); IndexResolver resolver = mock(IndexResolver.class); diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysTypesTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysTypesTests.java index 7a6481983615..922ee7f81243 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysTypesTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/plan/logical/command/sys/SysTypesTests.java @@ -9,7 +9,6 @@ import org.elasticsearch.Version; import org.elasticsearch.core.Tuple; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.ql.expression.function.FunctionRegistry; import org.elasticsearch.xpack.ql.index.EsIndex; import org.elasticsearch.xpack.ql.index.IndexResolution; import org.elasticsearch.xpack.ql.index.IndexResolver; @@ -38,6 +37,7 @@ import static org.elasticsearch.xpack.ql.index.VersionCompatibilityChecks.isTypeSupportedInVersion; import static org.elasticsearch.xpack.ql.type.DataTypes.UNSIGNED_LONG; import static org.elasticsearch.xpack.ql.type.DataTypes.VERSION; +import static org.elasticsearch.xpack.sql.analysis.analyzer.AnalyzerTestUtils.analyzer; import static org.elasticsearch.xpack.sql.plan.logical.command.sys.SysColumnsTests.UNSIGNED_LONG_TEST_VERSIONS; import static org.elasticsearch.xpack.sql.plan.logical.command.sys.SysColumnsTests.VERSION_FIELD_TEST_VERSIONS; import static org.mockito.Mockito.mock; @@ -67,7 +67,7 @@ private Tuple sql(String sql, Mode mode, SqlVersion version false ); EsIndex test = new EsIndex("test", SqlTypesTests.loadMapping("mapping-multi-field-with-nested.json", true)); - Analyzer analyzer = new Analyzer(configuration, new FunctionRegistry(), IndexResolution.valid(test), null); + Analyzer analyzer = analyzer(configuration, IndexResolution.valid(test)); Command cmd = (Command) analyzer.analyze(parser.createStatement(sql), false); IndexResolver resolver = mock(IndexResolver.class); diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryFolderTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryFolderTests.java index bc9402401ee1..72d5fc286465 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryFolderTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryFolderTests.java @@ -19,7 +19,6 @@ import org.elasticsearch.xpack.ql.type.EsField; import org.elasticsearch.xpack.sql.SqlTestUtils; import org.elasticsearch.xpack.sql.analysis.analyzer.Analyzer; -import org.elasticsearch.xpack.sql.analysis.analyzer.Verifier; import org.elasticsearch.xpack.sql.expression.function.SqlFunctionRegistry; import org.elasticsearch.xpack.sql.optimizer.Optimizer; import org.elasticsearch.xpack.sql.parser.SqlParser; @@ -31,7 +30,6 @@ import org.elasticsearch.xpack.sql.querydsl.container.QueryContainer; import org.elasticsearch.xpack.sql.session.EmptyExecutable; import org.elasticsearch.xpack.sql.session.SingletonExecutable; -import org.elasticsearch.xpack.sql.stats.Metrics; import org.elasticsearch.xpack.sql.types.SqlTypesTests; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -42,6 +40,7 @@ import static java.util.Arrays.asList; import static java.util.stream.Collectors.toList; +import static org.elasticsearch.xpack.sql.analysis.analyzer.AnalyzerTestUtils.analyzer; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.startsWith; @@ -60,7 +59,7 @@ public static void init() { Map mapping = SqlTypesTests.loadMapping("mapping-multi-field-variation.json"); EsIndex test = new EsIndex("test", mapping); IndexResolution getIndexResult = IndexResolution.valid(test); - analyzer = new Analyzer(SqlTestUtils.TEST_CFG, new SqlFunctionRegistry(), getIndexResult, new Verifier(new Metrics())); + analyzer = analyzer(getIndexResult); optimizer = new Optimizer(); planner = new Planner(); } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorSpecTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorSpecTests.java index 3cc1023e37d1..1df5058f65c4 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorSpecTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorSpecTests.java @@ -16,13 +16,10 @@ import org.elasticsearch.xpack.ql.plan.logical.LogicalPlan; import org.elasticsearch.xpack.ql.type.EsField; import org.elasticsearch.xpack.sql.analysis.analyzer.Analyzer; -import org.elasticsearch.xpack.sql.analysis.analyzer.Verifier; -import org.elasticsearch.xpack.sql.expression.function.SqlFunctionRegistry; import org.elasticsearch.xpack.sql.optimizer.Optimizer; import org.elasticsearch.xpack.sql.parser.SqlParser; import org.elasticsearch.xpack.sql.plan.physical.EsQueryExec; import org.elasticsearch.xpack.sql.plan.physical.PhysicalPlan; -import org.elasticsearch.xpack.sql.stats.Metrics; import org.elasticsearch.xpack.sql.types.SqlTypesTests; import org.elasticsearch.xpack.sql.util.DateUtils; import org.hamcrest.Matcher; @@ -32,7 +29,7 @@ import java.util.List; import java.util.Map; -import static org.elasticsearch.xpack.sql.SqlTestUtils.TEST_CFG; +import static org.elasticsearch.xpack.sql.analysis.analyzer.AnalyzerTestUtils.analyzer; public class QueryTranslatorSpecTests extends ESTestCase { @@ -49,7 +46,7 @@ private static class TestContext { Map mapping = SqlTypesTests.loadMapping(mappingFile); EsIndex test = new EsIndex("test", mapping); IndexResolution getIndexResult = IndexResolution.valid(test); - analyzer = new Analyzer(TEST_CFG, new SqlFunctionRegistry(), getIndexResult, new Verifier(new Metrics())); + analyzer = analyzer(getIndexResult); optimizer = new Optimizer(); planner = new Planner(); } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java index b18d3c52f98c..f363e06318d9 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java @@ -48,7 +48,6 @@ import org.elasticsearch.xpack.ql.querydsl.query.WildcardQuery; import org.elasticsearch.xpack.ql.type.EsField; import org.elasticsearch.xpack.sql.analysis.analyzer.Analyzer; -import org.elasticsearch.xpack.sql.analysis.analyzer.Verifier; import org.elasticsearch.xpack.sql.expression.function.SqlFunctionRegistry; import org.elasticsearch.xpack.sql.expression.function.aggregate.ExtendedStatsEnclosed; import org.elasticsearch.xpack.sql.expression.function.aggregate.MatrixStatsEnclosed; @@ -75,7 +74,6 @@ import org.elasticsearch.xpack.sql.querydsl.agg.GroupByDateHistogram; import org.elasticsearch.xpack.sql.querydsl.container.MetricAggRef; import org.elasticsearch.xpack.sql.session.SingletonExecutable; -import org.elasticsearch.xpack.sql.stats.Metrics; import org.elasticsearch.xpack.sql.types.SqlTypesTests; import org.elasticsearch.xpack.sql.util.DateUtils; import org.hamcrest.Matcher; @@ -102,6 +100,7 @@ import static org.elasticsearch.xpack.ql.type.DataTypes.TEXT; import static org.elasticsearch.xpack.sql.SqlTestUtils.TEST_CFG; import static org.elasticsearch.xpack.sql.SqlTestUtils.literal; +import static org.elasticsearch.xpack.sql.analysis.analyzer.AnalyzerTestUtils.analyzer; import static org.elasticsearch.xpack.sql.expression.function.scalar.math.MathProcessor.MathOperation.E; import static org.elasticsearch.xpack.sql.expression.function.scalar.math.MathProcessor.MathOperation.PI; import static org.elasticsearch.xpack.sql.planner.QueryTranslator.DATE_FORMAT; @@ -130,7 +129,7 @@ private static class TestContext { Map mapping = SqlTypesTests.loadMapping(mappingFile); EsIndex test = new EsIndex("test", mapping); IndexResolution getIndexResult = IndexResolution.valid(test); - analyzer = new Analyzer(TEST_CFG, sqlFunctionRegistry, getIndexResult, new Verifier(new Metrics())); + analyzer = analyzer(getIndexResult); optimizer = new Optimizer(); planner = new Planner(); } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/VerifierTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/VerifierTests.java index fdb87f9ab1c9..0e2e50cb73fc 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/VerifierTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/VerifierTests.java @@ -11,14 +11,11 @@ import org.elasticsearch.xpack.ql.index.EsIndex; import org.elasticsearch.xpack.ql.index.IndexResolution; import org.elasticsearch.xpack.sql.analysis.analyzer.Analyzer; -import org.elasticsearch.xpack.sql.analysis.analyzer.Verifier; -import org.elasticsearch.xpack.sql.expression.function.SqlFunctionRegistry; import org.elasticsearch.xpack.sql.parser.SqlParser; import org.elasticsearch.xpack.sql.plan.physical.EsQueryExec; import org.elasticsearch.xpack.sql.plan.physical.PhysicalPlan; -import org.elasticsearch.xpack.sql.stats.Metrics; -import static org.elasticsearch.xpack.sql.SqlTestUtils.TEST_CFG; +import static org.elasticsearch.xpack.sql.analysis.analyzer.AnalyzerTestUtils.analyzer; import static org.elasticsearch.xpack.sql.types.SqlTypesTests.loadMapping; public class VerifierTests extends ESTestCase { @@ -27,7 +24,7 @@ public class VerifierTests extends ESTestCase { private final IndexResolution indexResolution = IndexResolution.valid( new EsIndex("test", loadMapping("mapping-multi-field-with-nested.json")) ); - private final Analyzer analyzer = new Analyzer(TEST_CFG, new SqlFunctionRegistry(), indexResolution, new Verifier(new Metrics())); + private final Analyzer analyzer = analyzer(indexResolution); private final Planner planner = new Planner(); private PhysicalPlan verify(String sql) { diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/stats/VerifierMetricsTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/stats/VerifierMetricsTests.java index da6c96a84684..7612c453d699 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/stats/VerifierMetricsTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/stats/VerifierMetricsTests.java @@ -12,15 +12,14 @@ import org.elasticsearch.xpack.ql.index.EsIndex; import org.elasticsearch.xpack.ql.index.IndexResolution; import org.elasticsearch.xpack.ql.type.EsField; -import org.elasticsearch.xpack.sql.SqlTestUtils; import org.elasticsearch.xpack.sql.analysis.analyzer.Analyzer; import org.elasticsearch.xpack.sql.analysis.analyzer.Verifier; -import org.elasticsearch.xpack.sql.expression.function.SqlFunctionRegistry; import org.elasticsearch.xpack.sql.parser.SqlParser; import org.elasticsearch.xpack.sql.types.SqlTypesTests; import java.util.Map; +import static org.elasticsearch.xpack.sql.analysis.analyzer.AnalyzerTestUtils.analyzer; import static org.elasticsearch.xpack.sql.stats.FeatureMetric.COMMAND; import static org.elasticsearch.xpack.sql.stats.FeatureMetric.GROUPBY; import static org.elasticsearch.xpack.sql.stats.FeatureMetric.HAVING; @@ -258,7 +257,7 @@ private Counters sql(String sql, Verifier v) { verifier = new Verifier(metrics); } - Analyzer analyzer = new Analyzer(SqlTestUtils.TEST_CFG, new SqlFunctionRegistry(), IndexResolution.valid(test), verifier); + Analyzer analyzer = analyzer(IndexResolution.valid(test), verifier); analyzer.analyze(parser.createStatement(sql), true); return metrics == null ? null : metrics.stats(); From 9a3d39a1d4ce2bd152f99a4b97dc37135393dc64 Mon Sep 17 00:00:00 2001 From: Joe Gallo Date: Tue, 20 Dec 2022 17:38:50 -0500 Subject: [PATCH 319/919] Correctly handle an exception case for ingest failure (#92455) --- docs/changelog/92455.yaml | 5 ++ ...ed_pipeline.yml => 240_final_pipeline.yml} | 0 .../elasticsearch/ingest/IngestService.java | 62 ++++++------------- .../ingest/IngestServiceTests.java | 27 +++++--- 4 files changed, 44 insertions(+), 50 deletions(-) create mode 100644 docs/changelog/92455.yaml rename modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/{240_required_pipeline.yml => 240_final_pipeline.yml} (100%) diff --git a/docs/changelog/92455.yaml b/docs/changelog/92455.yaml new file mode 100644 index 000000000000..e7f77c315e6b --- /dev/null +++ b/docs/changelog/92455.yaml @@ -0,0 +1,5 @@ +pr: 92455 +summary: Correctly handle an exception case for ingest failure +area: Ingest Node +type: bug +issues: [] diff --git a/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/240_required_pipeline.yml b/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/240_final_pipeline.yml similarity index 100% rename from modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/240_required_pipeline.yml rename to modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/240_final_pipeline.yml diff --git a/server/src/main/java/org/elasticsearch/ingest/IngestService.java b/server/src/main/java/org/elasticsearch/ingest/IngestService.java index 8a28b00a4186..3df1072c6861 100644 --- a/server/src/main/java/org/elasticsearch/ingest/IngestService.java +++ b/server/src/main/java/org/elasticsearch/ingest/IngestService.java @@ -22,6 +22,7 @@ import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.ingest.DeletePipelineRequest; import org.elasticsearch.action.ingest.PutPipelineRequest; +import org.elasticsearch.action.support.CountDownActionListener; import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.client.internal.Client; import org.elasticsearch.cluster.ClusterChangedEvent; @@ -72,7 +73,6 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -687,15 +687,16 @@ public void onFailure(Exception e) { @Override protected void doRun() { final Thread originalThread = Thread.currentThread(); - final AtomicInteger counter = new AtomicInteger(numberOfActionRequests); + final ActionListener onFinished = new CountDownActionListener( + numberOfActionRequests, + () -> onCompletion.accept(originalThread, null) + ); + int i = 0; for (DocWriteRequest actionRequest : actionRequests) { IndexRequest indexRequest = TransportBulkAction.getIndexWriteRequest(actionRequest); if (indexRequest == null) { - if (counter.decrementAndGet() == 0) { - onCompletion.accept(originalThread, null); - } - assert counter.get() >= 0; + onFinished.onResponse(null); i++; continue; } @@ -715,25 +716,12 @@ protected void doRun() { } else if (IngestService.NOOP_PIPELINE_NAME.equals(finalPipelineId) == false) { pipelines = List.of(finalPipelineId); } else { - if (counter.decrementAndGet() == 0) { - onCompletion.accept(originalThread, null); - } - assert counter.get() >= 0; + onFinished.onResponse(null); i++; continue; } - executePipelines( - i, - pipelines.iterator(), - hasFinalPipeline, - indexRequest, - onDropped, - onFailure, - counter, - onCompletion, - originalThread - ); + executePipelines(i, pipelines.iterator(), hasFinalPipeline, indexRequest, onDropped, onFailure, onFinished); i++; } @@ -748,9 +736,7 @@ private void executePipelines( final IndexRequest indexRequest, final IntConsumer onDropped, final BiConsumer onFailure, - final AtomicInteger counter, - final BiConsumer onCompletion, - final Thread originalThread + final ActionListener onFinished ) { assert it.hasNext(); final String pipelineId = it.next(); @@ -778,6 +764,9 @@ private void executePipelines( e ); onFailure.accept(slot, e); + // document failed! no further processing of this doc + onFinished.onResponse(null); + return; } Iterator newIt = it; @@ -791,6 +780,9 @@ private void executePipelines( slot, new IllegalStateException("final pipeline [" + pipelineId + "] can't change the target index") ); + // document failed! no further processing of this doc + onFinished.onResponse(null); + return; } else { indexRequest.isPipelineResolved(false); resolvePipelines(null, indexRequest, state.metadata()); @@ -804,22 +796,9 @@ private void executePipelines( } if (newIt.hasNext()) { - executePipelines( - slot, - newIt, - newHasFinalPipeline, - indexRequest, - onDropped, - onFailure, - counter, - onCompletion, - originalThread - ); + executePipelines(slot, newIt, newHasFinalPipeline, indexRequest, onDropped, onFailure, onFinished); } else { - if (counter.decrementAndGet() == 0) { - onCompletion.accept(originalThread, null); - } - assert counter.get() >= 0; + onFinished.onResponse(null); } }); } catch (Exception e) { @@ -828,10 +807,7 @@ private void executePipelines( e ); onFailure.accept(slot, e); - if (counter.decrementAndGet() == 0) { - onCompletion.accept(originalThread, null); - } - assert counter.get() >= 0; + onFinished.onResponse(null); } } diff --git a/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java b/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java index d24c89ae768b..06d2ce7896db 100644 --- a/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java +++ b/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java @@ -1136,20 +1136,34 @@ public void testExecutePropagateAllMetadataUpdates() throws Exception { public void testExecuteFailure() throws Exception { final CompoundProcessor processor = mockCompoundProcessor(); - IngestService ingestService = createWithProcessors(Map.of("mock", (factories, tag, description, config) -> processor)); - PutPipelineRequest putRequest = new PutPipelineRequest( - "_id", + IngestService ingestService = createWithProcessors( + Map.of( + "mock", + (factories, tag, description, config) -> processor, + "set", + (factories, tag, description, config) -> new FakeProcessor("set", "", "", (ingestDocument) -> fail()) + ) + ); + PutPipelineRequest putRequest1 = new PutPipelineRequest( + "_id1", new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON ); + // given that set -> fail() above, it's a failure if a document executes against this pipeline + PutPipelineRequest putRequest2 = new PutPipelineRequest( + "_id2", + new BytesArray("{\"processors\": [{\"set\" : {}}]}"), + XContentType.JSON + ); ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty ClusterState previousClusterState = clusterState; - clusterState = executePut(putRequest, clusterState); + clusterState = executePut(putRequest1, clusterState); + clusterState = executePut(putRequest2, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); final IndexRequest indexRequest = new IndexRequest("_index").id("_id") .source(Map.of()) - .setPipeline("_id") - .setFinalPipeline("_none"); + .setPipeline("_id1") + .setFinalPipeline("_id2"); doThrow(new RuntimeException()).when(processor) .execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), Map.of()), any()); @SuppressWarnings("unchecked") @@ -2057,7 +2071,6 @@ private static IngestService createWithProcessors() { } private static IngestService createWithProcessors(Map processors) { - Client client = mock(Client.class); ThreadPool threadPool = mock(ThreadPool.class); when(threadPool.generic()).thenReturn(EsExecutors.DIRECT_EXECUTOR_SERVICE); From e53a0f8028f0a64781136ccd29cb5d3b0dc193ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Tue, 20 Dec 2022 23:46:03 +0100 Subject: [PATCH 320/919] Remove unused SearchParseException (#92446) This exception is only used as random exception in tests and isn't used in production code since 7.11, so we should remove it. ParsingException or XContentParseException are preferred choices when parsing location for errors is available. --- .../index/rankeval/RankEvalResponseTests.java | 3 - .../elasticsearch/ElasticsearchException.java | 7 +- .../search/SearchParseException.java | 85 ------------------- .../ElasticsearchExceptionTests.java | 19 +---- .../ExceptionSerializationTests.java | 12 +-- 5 files changed, 5 insertions(+), 121 deletions(-) delete mode 100644 server/src/main/java/org/elasticsearch/search/SearchParseException.java diff --git a/modules/rank-eval/src/test/java/org/elasticsearch/index/rankeval/RankEvalResponseTests.java b/modules/rank-eval/src/test/java/org/elasticsearch/index/rankeval/RankEvalResponseTests.java index b602f2b0a119..d4ec7ba9b9ef 100644 --- a/modules/rank-eval/src/test/java/org/elasticsearch/index/rankeval/RankEvalResponseTests.java +++ b/modules/rank-eval/src/test/java/org/elasticsearch/index/rankeval/RankEvalResponseTests.java @@ -23,7 +23,6 @@ import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.search.SearchHit; -import org.elasticsearch.search.SearchParseException; import org.elasticsearch.search.SearchShardTarget; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xcontent.ToXContent; @@ -44,7 +43,6 @@ import static java.util.Collections.singleton; import static org.elasticsearch.common.xcontent.XContentHelper.toXContent; -import static org.elasticsearch.test.TestSearchContext.SHARD_TARGET; import static org.elasticsearch.test.XContentTestUtils.insertRandomFields; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent; import static org.hamcrest.Matchers.instanceOf; @@ -54,7 +52,6 @@ public class RankEvalResponseTests extends ESTestCase { private static final Exception[] RANDOM_EXCEPTIONS = new Exception[] { new ClusterBlockException(singleton(NoMasterBlockService.NO_MASTER_BLOCK_WRITES)), new CircuitBreakingException("Data too large", 123, 456, CircuitBreaker.Durability.PERMANENT), - new SearchParseException(SHARD_TARGET, "Parse failure", new XContentLocation(12, 98)), new IllegalArgumentException("Closed resource", new RuntimeException("Resource")), new SearchPhaseExecutionException( "search", diff --git a/server/src/main/java/org/elasticsearch/ElasticsearchException.java b/server/src/main/java/org/elasticsearch/ElasticsearchException.java index 51718a226173..57500fe6d8c5 100644 --- a/server/src/main/java/org/elasticsearch/ElasticsearchException.java +++ b/server/src/main/java/org/elasticsearch/ElasticsearchException.java @@ -1090,12 +1090,7 @@ private enum ElasticsearchExceptionHandle { 71, UNKNOWN_VERSION_ADDED ), - SEARCH_PARSE_EXCEPTION( - org.elasticsearch.search.SearchParseException.class, - org.elasticsearch.search.SearchParseException::new, - 72, - UNKNOWN_VERSION_ADDED - ), + // 72 was SearchParseException, only used in tests after 7.11 CONCURRENT_SNAPSHOT_EXECUTION_EXCEPTION( org.elasticsearch.snapshots.ConcurrentSnapshotExecutionException.class, org.elasticsearch.snapshots.ConcurrentSnapshotExecutionException::new, diff --git a/server/src/main/java/org/elasticsearch/search/SearchParseException.java b/server/src/main/java/org/elasticsearch/search/SearchParseException.java deleted file mode 100644 index 04c463eb4104..000000000000 --- a/server/src/main/java/org/elasticsearch/search/SearchParseException.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.search; - -import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.core.Nullable; -import org.elasticsearch.rest.RestStatus; -import org.elasticsearch.xcontent.XContentBuilder; -import org.elasticsearch.xcontent.XContentLocation; - -import java.io.IOException; - -public class SearchParseException extends SearchException { - - public static final int UNKNOWN_POSITION = -1; - private final int lineNumber; - private final int columnNumber; - - public SearchParseException(SearchShardTarget shardTarget, String msg, @Nullable XContentLocation location) { - this(shardTarget, msg, location, null); - } - - public SearchParseException(SearchShardTarget shardTarget, String msg, @Nullable XContentLocation location, Throwable cause) { - super(shardTarget, msg, cause); - int lineNumber = UNKNOWN_POSITION; - int columnNumber = UNKNOWN_POSITION; - if (location != null) { - lineNumber = location.lineNumber(); - columnNumber = location.columnNumber(); - } - this.columnNumber = columnNumber; - this.lineNumber = lineNumber; - } - - public SearchParseException(StreamInput in) throws IOException { - super(in); - lineNumber = in.readInt(); - columnNumber = in.readInt(); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - out.writeInt(lineNumber); - out.writeInt(columnNumber); - } - - @Override - public RestStatus status() { - return RestStatus.BAD_REQUEST; - } - - @Override - protected void metadataToXContent(XContentBuilder builder, Params params) throws IOException { - if (lineNumber != UNKNOWN_POSITION) { - builder.field("line", lineNumber); - builder.field("col", columnNumber); - } - } - - /** - * Line number of the location of the error - * - * @return the line number or -1 if unknown - */ - public int getLineNumber() { - return lineNumber; - } - - /** - * Column number of the location of the error - * - * @return the column number or -1 if unknown - */ - public int getColumnNumber() { - return columnNumber; - } -} diff --git a/server/src/test/java/org/elasticsearch/ElasticsearchExceptionTests.java b/server/src/test/java/org/elasticsearch/ElasticsearchExceptionTests.java index 1c89e93467ad..da5ef8984c22 100644 --- a/server/src/test/java/org/elasticsearch/ElasticsearchExceptionTests.java +++ b/server/src/test/java/org/elasticsearch/ElasticsearchExceptionTests.java @@ -37,7 +37,6 @@ import org.elasticsearch.rest.RestStatus; import org.elasticsearch.script.ScriptException; import org.elasticsearch.search.SearchContextMissingException; -import org.elasticsearch.search.SearchParseException; import org.elasticsearch.search.SearchShardTarget; import org.elasticsearch.search.internal.ShardSearchContextId; import org.elasticsearch.test.ESTestCase; @@ -46,7 +45,6 @@ import org.elasticsearch.xcontent.XContent; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; -import org.elasticsearch.xcontent.XContentLocation; import org.elasticsearch.xcontent.XContentParseException; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentType; @@ -63,7 +61,6 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; -import static org.elasticsearch.test.TestSearchContext.SHARD_TARGET; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent; import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.hasItems; @@ -543,12 +540,6 @@ public void testToXContent() throws IOException { } }"""); } - { - ElasticsearchException e = new SearchParseException(SHARD_TARGET, "foo", new XContentLocation(1, 0)); - - assertExceptionAsJson(e, """ - {"type":"search_parse_exception","reason":"foo","line":1,"col":0}"""); - } { ElasticsearchException ex = new ElasticsearchException( "foo", @@ -1202,7 +1193,7 @@ public static Tuple randomExceptions() { Throwable actual; ElasticsearchException expected; - int type = randomIntBetween(0, 5); + int type = randomIntBetween(0, 4); switch (type) { case 0 -> { actual = new ClusterBlockException(singleton(NoMasterBlockService.NO_MASTER_BLOCK_WRITES)); @@ -1215,17 +1206,13 @@ public static Tuple randomExceptions() { expected = new ElasticsearchException("Elasticsearch exception [type=parsing_exception, reason=Unknown identifier]"); } case 2 -> { - actual = new SearchParseException(SHARD_TARGET, "Parse failure", new XContentLocation(12, 98)); - expected = new ElasticsearchException("Elasticsearch exception [type=search_parse_exception, reason=Parse failure]"); - } - case 3 -> { actual = new IllegalArgumentException("Closed resource", new RuntimeException("Resource")); expected = new ElasticsearchException( "Elasticsearch exception [type=illegal_argument_exception, reason=Closed resource]", new ElasticsearchException("Elasticsearch exception [type=runtime_exception, reason=Resource]") ); } - case 4 -> { + case 3 -> { actual = new SearchPhaseExecutionException( "search", "all shards failed", @@ -1240,7 +1227,7 @@ public static Tuple randomExceptions() { ); expected.addMetadata("es.phase", "search"); } - case 5 -> { + case 4 -> { actual = new ElasticsearchException( "Parsing failed", new ParsingException(9, 42, "Wrong state", new NullPointerException("Unexpected null value")) diff --git a/server/src/test/java/org/elasticsearch/ExceptionSerializationTests.java b/server/src/test/java/org/elasticsearch/ExceptionSerializationTests.java index 270d79afb20a..62250cd8a67f 100644 --- a/server/src/test/java/org/elasticsearch/ExceptionSerializationTests.java +++ b/server/src/test/java/org/elasticsearch/ExceptionSerializationTests.java @@ -69,7 +69,6 @@ import org.elasticsearch.rest.action.admin.indices.AliasesNotFoundException; import org.elasticsearch.search.SearchContextMissingException; import org.elasticsearch.search.SearchException; -import org.elasticsearch.search.SearchParseException; import org.elasticsearch.search.SearchShardTarget; import org.elasticsearch.search.aggregations.MultiBucketConsumerService; import org.elasticsearch.search.aggregations.UnsupportedAggregationOnDownsampledIndex; @@ -87,7 +86,6 @@ import org.elasticsearch.transport.NoSeedNodeLeftException; import org.elasticsearch.transport.NoSuchRemoteClusterException; import org.elasticsearch.transport.TcpTransport; -import org.elasticsearch.xcontent.XContentLocation; import java.io.EOFException; import java.io.FileNotFoundException; @@ -118,7 +116,6 @@ import static java.util.Collections.emptySet; import static java.util.Collections.singleton; import static org.elasticsearch.cluster.routing.TestShardRouting.newShardRouting; -import static org.elasticsearch.test.TestSearchContext.SHARD_TARGET; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; @@ -394,13 +391,6 @@ public void testAliasesMissingException() throws IOException { assertArrayEquals(new String[] { "one", "two", "three" }, ex.getResourceId().toArray(new String[0])); } - public void testSearchParseException() throws IOException { - SearchParseException ex = serialize(new SearchParseException(SHARD_TARGET, "foo", new XContentLocation(66, 666))); - assertEquals("foo", ex.getMessage()); - assertEquals(66, ex.getLineNumber()); - assertEquals(666, ex.getColumnNumber()); - } - public void testIllegalIndexShardStateException() throws IOException { ShardId id = new ShardId("foo", "_na_", 1); IndexShardState state = randomFrom(IndexShardState.values()); @@ -740,7 +730,7 @@ public void testIds() { ids.put(69, org.elasticsearch.snapshots.SnapshotMissingException.class); ids.put(70, org.elasticsearch.action.PrimaryMissingActionException.class); ids.put(71, org.elasticsearch.action.FailedNodeException.class); - ids.put(72, org.elasticsearch.search.SearchParseException.class); + ids.put(72, null); // was SearchParseException, only used in tests since 7.11 ids.put(73, org.elasticsearch.snapshots.ConcurrentSnapshotExecutionException.class); ids.put(74, org.elasticsearch.common.blobstore.BlobStoreException.class); ids.put(75, org.elasticsearch.cluster.IncompatibleClusterStateVersionException.class); From e7605a365632aad52241b54bc05adc9aac8a2b37 Mon Sep 17 00:00:00 2001 From: Hendrik Muhs Date: Wed, 21 Dec 2022 08:50:02 +0100 Subject: [PATCH 321/919] add support for includes and excludes in MultiValuesSourceFieldConfig (#92396) add support for includes and excludes in MultiValuesSourceFieldConfig, this will allow aggregations based on it implement includes and excludes based on regular expressions or a list of values. --- .../WeightedAvgAggregationBuilder.java | 4 +- .../support/MultiValuesSourceFieldConfig.java | 69 +++++++++++++++++-- .../support/MultiValuesSourceParseHelper.java | 11 ++- .../support/IncludeExcludeTests.java | 29 ++++++++ .../MultiValuesSourceFieldConfigTests.java | 5 +- .../MultiTermsAggregationBuilder.java | 3 +- .../TopMetricsAggregationBuilder.java | 1 + .../ttest/TTestAggregationBuilder.java | 4 +- .../FrequentItemSetsAggregationBuilder.java | 3 +- .../GeoLineAggregationBuilder.java | 4 +- 10 files changed, 117 insertions(+), 16 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/WeightedAvgAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/WeightedAvgAggregationBuilder.java index fb426be5b05b..a93ccfec9e4f 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/WeightedAvgAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/WeightedAvgAggregationBuilder.java @@ -46,8 +46,8 @@ public class WeightedAvgAggregationBuilder extends MultiValuesSourceAggregationB ); static { MultiValuesSourceParseHelper.declareCommon(PARSER, true, ValueType.NUMERIC); - MultiValuesSourceParseHelper.declareField(VALUE_FIELD.getPreferredName(), PARSER, true, false, false, false); - MultiValuesSourceParseHelper.declareField(WEIGHT_FIELD.getPreferredName(), PARSER, true, false, false, false); + MultiValuesSourceParseHelper.declareField(VALUE_FIELD.getPreferredName(), PARSER, true, false, false, false, false); + MultiValuesSourceParseHelper.declareField(WEIGHT_FIELD.getPreferredName(), PARSER, true, false, false, false, false); } public static void registerUsage(ValuesSourceRegistry.Builder builder) { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/support/MultiValuesSourceFieldConfig.java b/server/src/main/java/org/elasticsearch/search/aggregations/support/MultiValuesSourceFieldConfig.java index ad9e4c406c4a..ff1949fb5fb7 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/support/MultiValuesSourceFieldConfig.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/support/MultiValuesSourceFieldConfig.java @@ -17,6 +17,7 @@ import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.script.Script; import org.elasticsearch.search.aggregations.AggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.terms.IncludeExclude; import org.elasticsearch.xcontent.ObjectParser; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.ToXContentObject; @@ -38,6 +39,7 @@ public class MultiValuesSourceFieldConfig implements Writeable, ToXContentObject private final QueryBuilder filter; // supported only if heterogeneous == true private final ValueType userValueTypeHint; + private final IncludeExclude includeExclude; private final String format; private static final String NAME = "field_config"; @@ -50,6 +52,7 @@ public class MultiValuesSourceFieldConfig implements Writeable, ToXContentObject * @param timezoneAware - allows specifying timezone * @param filtered - allows specifying filters on the values * @param heterogeneous - allows specifying value-source specific format and user value type hint + * @param supportsIncludesExcludes - allows specifying includes and excludes * @param - parser context * @return configured parser */ @@ -57,7 +60,8 @@ public static ObjectParser parserBu boolean scriptable, boolean timezoneAware, boolean filtered, - boolean heterogeneous + boolean heterogeneous, + boolean supportsIncludesExcludes ) { ObjectParser parser = new ObjectParser<>( @@ -116,6 +120,23 @@ public static ObjectParser parserBu ObjectParser.ValueType.STRING ); } + + if (supportsIncludesExcludes) { + parser.declareField( + (b, v) -> b.setIncludeExclude(IncludeExclude.merge(v, b.getIncludeExclude())), + IncludeExclude::parseInclude, + IncludeExclude.INCLUDE_FIELD, + ObjectParser.ValueType.OBJECT_ARRAY_OR_STRING + ); + + parser.declareField( + (b, v) -> b.setIncludeExclude(IncludeExclude.merge(b.getIncludeExclude(), v)), + IncludeExclude::parseExclude, + IncludeExclude.EXCLUDE_FIELD, + ObjectParser.ValueType.STRING_ARRAY + ); + } + return parser; }; @@ -126,7 +147,8 @@ protected MultiValuesSourceFieldConfig( ZoneId timeZone, QueryBuilder filter, ValueType userValueTypeHint, - String format + String format, + IncludeExclude includeExclude ) { this.fieldName = fieldName; this.missing = missing; @@ -135,6 +157,7 @@ protected MultiValuesSourceFieldConfig( this.filter = filter; this.userValueTypeHint = userValueTypeHint; this.format = format; + this.includeExclude = includeExclude; } public MultiValuesSourceFieldConfig(StreamInput in) throws IOException { @@ -158,6 +181,11 @@ public MultiValuesSourceFieldConfig(StreamInput in) throws IOException { this.userValueTypeHint = null; this.format = null; } + if (in.getVersion().onOrAfter(Version.V_8_7_0)) { + this.includeExclude = in.readOptionalWriteable(IncludeExclude::new); + } else { + this.includeExclude = null; + } } public Object getMissing() { @@ -188,6 +216,10 @@ public String getFormat() { return format; } + public IncludeExclude getIncludeExclude() { + return includeExclude; + } + @Override public void writeTo(StreamOutput out) throws IOException { if (out.getVersion().onOrAfter(Version.V_7_6_0)) { @@ -205,6 +237,9 @@ public void writeTo(StreamOutput out) throws IOException { out.writeOptionalWriteable(userValueTypeHint); out.writeOptionalString(format); } + if (out.getVersion().onOrAfter(Version.V_8_7_0)) { + out.writeOptionalWriteable(includeExclude); + } } @Override @@ -232,6 +267,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws if (format != null) { builder.field(AggregationBuilder.CommonFields.FORMAT.getPreferredName(), format); } + if (includeExclude != null) { + includeExclude.toXContent(builder, params); + } + builder.endObject(); return builder; } @@ -247,12 +286,13 @@ public boolean equals(Object o) { && Objects.equals(timeZone, that.timeZone) && Objects.equals(filter, that.filter) && Objects.equals(userValueTypeHint, that.userValueTypeHint) - && Objects.equals(format, that.format); + && Objects.equals(format, that.format) + && Objects.equals(includeExclude, that.includeExclude); } @Override public int hashCode() { - return Objects.hash(fieldName, missing, script, timeZone, filter, userValueTypeHint, format); + return Objects.hash(fieldName, missing, script, timeZone, filter, userValueTypeHint, format, includeExclude); } @Override @@ -268,6 +308,7 @@ public static class Builder { private QueryBuilder filter = null; private ValueType userValueTypeHint = null; private String format = null; + private IncludeExclude includeExclude = null; public String getFieldName() { return fieldName; @@ -328,6 +369,15 @@ public String getFormat() { return format; } + public Builder setIncludeExclude(IncludeExclude includeExclude) { + this.includeExclude = includeExclude; + return this; + } + + public IncludeExclude getIncludeExclude() { + return includeExclude; + } + public MultiValuesSourceFieldConfig build() { if (Strings.isNullOrEmpty(fieldName) && script == null) { throw new IllegalArgumentException( @@ -351,7 +401,16 @@ public MultiValuesSourceFieldConfig build() { ); } - return new MultiValuesSourceFieldConfig(fieldName, missing, script, timeZone, filter, userValueTypeHint, format); + return new MultiValuesSourceFieldConfig( + fieldName, + missing, + script, + timeZone, + filter, + userValueTypeHint, + format, + includeExclude + ); } } } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/support/MultiValuesSourceParseHelper.java b/server/src/main/java/org/elasticsearch/search/aggregations/support/MultiValuesSourceParseHelper.java index 4fe17985fdc0..867380ba9cce 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/support/MultiValuesSourceParseHelper.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/support/MultiValuesSourceParseHelper.java @@ -65,12 +65,19 @@ public static void declareField( boolean scriptable, boolean timezoneAware, boolean filterable, - boolean heterogeneous + boolean heterogeneous, + boolean supportsIncludesExcludes ) { objectParser.declareField( (o, fieldConfig) -> o.field(fieldName, fieldConfig.build()), - (p, c) -> MultiValuesSourceFieldConfig.parserBuilder(scriptable, timezoneAware, filterable, heterogeneous).parse(p, null), + (p, c) -> MultiValuesSourceFieldConfig.parserBuilder( + scriptable, + timezoneAware, + filterable, + heterogeneous, + supportsIncludesExcludes + ).parse(p, null), new ParseField(fieldName), ObjectParser.ValueType.OBJECT ); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/support/IncludeExcludeTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/support/IncludeExcludeTests.java index 5605e68a7d34..27314a8112f1 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/support/IncludeExcludeTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/support/IncludeExcludeTests.java @@ -35,6 +35,35 @@ import static org.hamcrest.Matchers.equalTo; public class IncludeExcludeTests extends ESTestCase { + + public static IncludeExclude randomIncludeExclude() { + switch (randomInt(7)) { + case 0: + return new IncludeExclude("incl*de", null, null, null); + case 1: + return new IncludeExclude("incl*de", "excl*de", null, null); + case 2: + return new IncludeExclude("incl*de", null, null, new TreeSet<>(Set.of(newBytesRef("exclude")))); + case 3: + return new IncludeExclude(null, "excl*de", null, null); + case 4: + return new IncludeExclude(null, "excl*de", new TreeSet<>(Set.of(newBytesRef("include"))), null); + case 5: + return new IncludeExclude(null, null, new TreeSet<>(Set.of(newBytesRef("include"))), null); + case 6: + return new IncludeExclude( + null, + null, + new TreeSet<>(Set.of(newBytesRef("include"))), + new TreeSet<>(Set.of(newBytesRef("exclude"))) + ); + case 7: + return new IncludeExclude(null, null, null, new TreeSet<>(Set.of(newBytesRef("exclude")))); + default: + throw new IllegalArgumentException("got unexpected parameter, expected 0 <= x <= 7"); + } + } + public void testEmptyTermsWithOrds() throws IOException { IncludeExclude inexcl = new IncludeExclude(null, null, new TreeSet<>(Set.of(new BytesRef("foo"))), null); OrdinalsFilter filter = inexcl.convertToOrdinalsFilter(DocValueFormat.RAW); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/support/MultiValuesSourceFieldConfigTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/support/MultiValuesSourceFieldConfigTests.java index 05569547bd24..c88c5c4e8c9e 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/support/MultiValuesSourceFieldConfigTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/support/MultiValuesSourceFieldConfigTests.java @@ -15,6 +15,7 @@ import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.script.Script; import org.elasticsearch.search.SearchModule; +import org.elasticsearch.search.aggregations.bucket.terms.IncludeExclude; import org.elasticsearch.test.AbstractXContentSerializingTestCase; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.XContentParser; @@ -30,7 +31,7 @@ public class MultiValuesSourceFieldConfigTests extends AbstractXContentSerializi @Override protected MultiValuesSourceFieldConfig doParseInstance(XContentParser parser) throws IOException { - return MultiValuesSourceFieldConfig.parserBuilder(true, true, true, true).apply(parser, null).build(); + return MultiValuesSourceFieldConfig.parserBuilder(true, true, true, true, true).apply(parser, null).build(); } @Override @@ -43,6 +44,7 @@ protected MultiValuesSourceFieldConfig createTestInstance() { ValueType userValueTypeHint = randomBoolean() ? randomFrom(ValueType.STRING, ValueType.DOUBLE, ValueType.LONG, ValueType.DATE, ValueType.IP, ValueType.BOOLEAN) : null; + IncludeExclude includeExclude = randomBoolean() ? IncludeExcludeTests.randomIncludeExclude() : null; return new MultiValuesSourceFieldConfig.Builder().setFieldName(field) .setMissing(missing) .setScript(null) @@ -50,6 +52,7 @@ protected MultiValuesSourceFieldConfig createTestInstance() { .setFilter(filter) .setFormat(format) .setUserValueTypeHint(userValueTypeHint) + .setIncludeExclude(includeExclude) .build(); } diff --git a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/multiterms/MultiTermsAggregationBuilder.java b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/multiterms/MultiTermsAggregationBuilder.java index a9dd0ed0d55e..9b9def2edd6c 100644 --- a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/multiterms/MultiTermsAggregationBuilder.java +++ b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/multiterms/MultiTermsAggregationBuilder.java @@ -64,7 +64,8 @@ public class MultiTermsAggregationBuilder extends AbstractAggregationBuilder metricParser.parse(p, null).build(), METRIC_FIELD); diff --git a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/ttest/TTestAggregationBuilder.java b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/ttest/TTestAggregationBuilder.java index 36b1218411f0..c9fcd5e1d80d 100644 --- a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/ttest/TTestAggregationBuilder.java +++ b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/ttest/TTestAggregationBuilder.java @@ -46,8 +46,8 @@ public class TTestAggregationBuilder extends MultiValuesSourceAggregationBuilder static { MultiValuesSourceParseHelper.declareCommon(PARSER, true, ValueType.NUMERIC); - MultiValuesSourceParseHelper.declareField(A_FIELD.getPreferredName(), PARSER, true, false, true, false); - MultiValuesSourceParseHelper.declareField(B_FIELD.getPreferredName(), PARSER, true, false, true, false); + MultiValuesSourceParseHelper.declareField(A_FIELD.getPreferredName(), PARSER, true, false, true, false, false); + MultiValuesSourceParseHelper.declareField(B_FIELD.getPreferredName(), PARSER, true, false, true, false, false); PARSER.declareString(TTestAggregationBuilder::testType, TYPE_FIELD); PARSER.declareInt(TTestAggregationBuilder::tails, TAILS_FIELD); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregationBuilder.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregationBuilder.java index 4a10dd2d5a83..3fd5e899c75f 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregationBuilder.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregationBuilder.java @@ -64,7 +64,8 @@ public final class FrequentItemSetsAggregationBuilder extends AbstractAggregatio false, // scriptable false, // timezone aware false, // filtered (not defined per field, but for all fields below) - false // format + false, // format + false // includes and excludes ); PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(), (p, n) -> fieldsParser.parse(p, null).build(), FIELDS); PARSER.declareDouble(ConstructingObjectParser.optionalConstructorArg(), MINIMUM_SUPPORT); diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/GeoLineAggregationBuilder.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/GeoLineAggregationBuilder.java index 124138cda586..9434abaa25c9 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/GeoLineAggregationBuilder.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/search/aggregations/GeoLineAggregationBuilder.java @@ -50,8 +50,8 @@ public class GeoLineAggregationBuilder extends MultiValuesSourceAggregationBuild ); static { MultiValuesSourceParseHelper.declareCommon(PARSER, true, ValueType.NUMERIC); - MultiValuesSourceParseHelper.declareField(POINT_FIELD.getPreferredName(), PARSER, true, false, false, false); - MultiValuesSourceParseHelper.declareField(SORT_FIELD.getPreferredName(), PARSER, true, false, false, false); + MultiValuesSourceParseHelper.declareField(POINT_FIELD.getPreferredName(), PARSER, true, false, false, false, false); + MultiValuesSourceParseHelper.declareField(SORT_FIELD.getPreferredName(), PARSER, true, false, false, false, false); PARSER.declareString((builder, order) -> builder.sortOrder(SortOrder.fromString(order)), ORDER_FIELD); PARSER.declareBoolean(GeoLineAggregationBuilder::includeSort, INCLUDE_SORT_FIELD); PARSER.declareInt(GeoLineAggregationBuilder::size, SIZE_FIELD); From c0c105c210513b00dfe47a9c20a561d2e190aa39 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Wed, 21 Dec 2022 04:17:49 -0500 Subject: [PATCH 322/919] [DOCS] Fix typo (#92481) --- docs/reference/analysis/token-graphs.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/analysis/token-graphs.asciidoc b/docs/reference/analysis/token-graphs.asciidoc index 9881afe908eb..55d69695bd62 100644 --- a/docs/reference/analysis/token-graphs.asciidoc +++ b/docs/reference/analysis/token-graphs.asciidoc @@ -34,7 +34,7 @@ include tokens for multi-word synonyms, such as using "atm" as a synonym for "automatic teller machine." However, only some token filters, known as _graph token filters_, accurately -record the `positionLength` for multi-position tokens. This filters include: +record the `positionLength` for multi-position tokens. These filters include: * <> * <> @@ -105,4 +105,4 @@ in an invalid graph. image::images/analysis/token-graph-dns-invalid-ex.svg[align="center"] Avoid using invalid token graphs for search. Invalid graphs can cause unexpected -search results. \ No newline at end of file +search results. From 1479494dc2571fa83fbef667234a637a991241d4 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 21 Dec 2022 10:31:05 +0100 Subject: [PATCH 323/919] Send remote access headers for CCS (#92138) This PR adds remote access headers to CCS requests sent by a querying cluster, if the new remote cluster security model is configured for the target fulfilling cluster. This is done by extending SecurityServerTransportInterceptor to check if a given request requires remote access headers to be sent; if so, we fetch the remote index privileges for the authenticated subject associated with the request, construct the remote access headers (for the cluster credential and the remote access privileges of the user), and send these as transport headers. If we send remote access headers, we omit the standard _xpack_security_authentication transport header. There are several conditions a request must meet to be sent with remote access headers: the connection must be to a remote cluster, and there must be a credential configured via cluster settings for the target cluster. A few other conditions (e.g., that the authentication type is supported) are there temporarily; we will lift these as we build out support. Since the fulfilling cluster logic for handling remote access headers is not implemented yet, CCS for feature-flagged deployments with configured cluster credentials will no longer work. This is safe since CCS will continue to fall back on the legacy security model unless the feature flag is set and the cluster credential setting is configured (which requires an active change to the settings). --- .../authc/RemoteAccessAuthentication.java | 32 +- .../RemoteAccessHeadersForCcsRestIT.java | 500 ++++++++++++++++++ .../SecurityServerTransportInterceptor.java | 179 +++++++ ...curityServerTransportInterceptorTests.java | 299 +++++++++++ 4 files changed, 1008 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/remoteaccess/RemoteAccessHeadersForCcsRestIT.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RemoteAccessAuthentication.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RemoteAccessAuthentication.java index 4d65eec4ab4e..a33fdb152bef 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RemoteAccessAuthentication.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RemoteAccessAuthentication.java @@ -63,6 +63,34 @@ public List getRoleDescriptorsBytesList() { return roleDescriptorsBytesList; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RemoteAccessAuthentication that = (RemoteAccessAuthentication) o; + + if (false == authentication.equals(that.authentication)) return false; + return roleDescriptorsBytesList.equals(that.roleDescriptorsBytesList); + } + + @Override + public int hashCode() { + int result = authentication.hashCode(); + result = 31 * result + roleDescriptorsBytesList.hashCode(); + return result; + } + + @Override + public String toString() { + return "RemoteAccessAuthentication{" + + "authentication=" + + authentication + + ", roleDescriptorsBytesList=" + + roleDescriptorsBytesList + + '}'; + } + private static List toRoleDescriptorsBytesList(final RoleDescriptorsIntersection roleDescriptorsIntersection) throws IOException { // If we ever lift this restriction, we need to ensure that the serialization of each set of role descriptors to raw bytes is @@ -76,7 +104,7 @@ private static List toRoleDescriptorsBytesList(final RoleD return roleDescriptorsBytesList; } - private String encode() throws IOException { + public String encode() throws IOException { final BytesStreamOutput out = new BytesStreamOutput(); out.setVersion(authentication.getEffectiveSubject().getVersion()); Version.writeVersion(authentication.getEffectiveSubject().getVersion(), out); @@ -85,7 +113,7 @@ private String encode() throws IOException { return Base64.getEncoder().encodeToString(BytesReference.toBytes(out.bytes())); } - private static RemoteAccessAuthentication decode(final String header) throws IOException { + public static RemoteAccessAuthentication decode(final String header) throws IOException { Objects.requireNonNull(header); final byte[] bytes = Base64.getDecoder().decode(header); final StreamInput in = StreamInput.wrap(bytes); diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/remoteaccess/RemoteAccessHeadersForCcsRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/remoteaccess/RemoteAccessHeadersForCcsRestIT.java new file mode 100644 index 000000000000..b9fa4a4dae28 --- /dev/null +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/remoteaccess/RemoteAccessHeadersForCcsRestIT.java @@ -0,0 +1,500 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.remoteaccess; + +import org.apache.http.HttpEntity; +import org.apache.http.entity.ContentType; +import org.apache.http.nio.entity.NStringEntity; +import org.apache.lucene.search.TotalHits; +import org.elasticsearch.Version; +import org.elasticsearch.action.admin.cluster.shards.ClusterSearchShardsAction; +import org.elasticsearch.action.admin.cluster.shards.ClusterSearchShardsGroup; +import org.elasticsearch.action.admin.cluster.shards.ClusterSearchShardsRequest; +import org.elasticsearch.action.admin.cluster.shards.ClusterSearchShardsResponse; +import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; +import org.elasticsearch.action.admin.cluster.state.ClusterStateRequest; +import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse; +import org.elasticsearch.action.search.SearchAction; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.action.search.ShardSearchFailure; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ConcurrentCollections; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.search.SearchHit; +import org.elasticsearch.search.SearchHits; +import org.elasticsearch.search.aggregations.InternalAggregations; +import org.elasticsearch.search.internal.InternalSearchResponse; +import org.elasticsearch.test.rest.ObjectPath; +import org.elasticsearch.test.transport.MockTransportService; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TcpTransport; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.json.JsonXContent; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.AuthenticationField; +import org.elasticsearch.xpack.core.security.authc.RemoteAccessAuthentication; +import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer; +import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection; +import org.elasticsearch.xpack.core.security.user.SystemUser; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.SecurityOnTrialLicenseRestTestCase; +import org.elasticsearch.xpack.security.authz.RBACEngine; +import org.elasticsearch.xpack.security.transport.SecurityServerTransportInterceptor; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.elasticsearch.common.UUIDs.randomBase64UUID; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.is; + +public class RemoteAccessHeadersForCcsRestIT extends SecurityOnTrialLicenseRestTestCase { + @BeforeClass + public static void checkFeatureFlag() { + assumeTrue("untrusted remote cluster feature flag must be enabled", TcpTransport.isUntrustedRemoteClusterEnabled()); + } + + private static final String CLUSTER_A = "my_remote_cluster_a"; + private static final String CLUSTER_B = "my_remote_cluster_b"; + private static final String REMOTE_SEARCH_USER = "remote_search_user"; + private static final SecureString PASSWORD = new SecureString("super-secret-password".toCharArray()); + private static final String REMOTE_SEARCH_ROLE = "remote_search"; + + private final ThreadPool threadPool = new TestThreadPool(getClass().getName()); + + @Before + public void setup() throws IOException { + createUser(REMOTE_SEARCH_USER, PASSWORD, List.of(REMOTE_SEARCH_ROLE)); + + final var putRoleRequest = new Request("PUT", "/_security/role/" + REMOTE_SEARCH_ROLE); + putRoleRequest.setJsonEntity(""" + { + "indices": [ + { + "names": ["index-a"], + "privileges": ["read"] + } + ], + "remote_indices": [ + { + "names": ["index-a"], + "privileges": ["read", "read_cross_cluster"], + "clusters": ["my_remote_cluster*"] + }, + { + "names": ["index-b"], + "privileges": ["read", "read_cross_cluster"], + "clusters": ["my_remote_cluster_b"] + } + ] + }"""); + assertOK(adminClient().performRequest(putRoleRequest)); + + final var indexDocRequest = new Request("POST", "/index-a/_doc?refresh=true"); + indexDocRequest.setJsonEntity("{\"foo\": \"bar\"}"); + assertOK(adminClient().performRequest(indexDocRequest)); + } + + @After + public void cleanup() throws IOException { + deleteUser(REMOTE_SEARCH_USER); + deleteRole(REMOTE_SEARCH_ROLE); + deleteIndex(adminClient(), "index-a"); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + ThreadPool.terminate(threadPool, 10, TimeUnit.SECONDS); + } + + public void testRemoteAccessHeadersSentSingleRemote() throws Exception { + final BlockingQueue capturedHeaders = ConcurrentCollections.newBlockingQueue(); + try (MockTransportService remoteTransport = startTransport("remoteNodeA", threadPool, capturedHeaders)) { + final String clusterCredential = randomBase64UUID(random()); + final DiscoveryNode remoteNode = remoteTransport.getLocalDiscoNode(); + final boolean useProxyMode = randomBoolean(); + setupClusterSettings(CLUSTER_A, clusterCredential, remoteNode, useProxyMode); + final boolean alsoSearchLocally = randomBoolean(); + final boolean minimizeRoundtrips = randomBoolean(); + final Request searchRequest = new Request( + "GET", + String.format( + Locale.ROOT, + "/%s%s:index-a/_search?ccs_minimize_roundtrips=%s", + alsoSearchLocally ? "index-a," : "", + CLUSTER_A, + minimizeRoundtrips + ) + ); + searchRequest.setOptions( + searchRequest.getOptions() + .toBuilder() + .addHeader("Authorization", UsernamePasswordToken.basicAuthHeaderValue(REMOTE_SEARCH_USER, PASSWORD)) + ); + + final Response response = client().performRequest(searchRequest); + assertOK(response); + assertThat(ObjectPath.createFromResponse(response).evaluate("hits.total.value"), equalTo(alsoSearchLocally ? 1 : 0)); + + expectActionsAndHeadersForCluster( + List.copyOf(capturedHeaders), + useProxyMode, + minimizeRoundtrips, + clusterCredential, + new RoleDescriptorsIntersection( + List.of( + Set.of( + new RoleDescriptor( + RBACEngine.REMOTE_USER_ROLE_NAME, + null, + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices("index-a") + .privileges("read", "read_cross_cluster") + .build() }, + null, + null, + null, + null, + null, + null + ) + ) + ) + ) + ); + } + } + + public void testRemoteAccessHeadersSentMultipleRemotes() throws Exception { + final Map> capturedHeadersByCluster = Map.of( + CLUSTER_A, + ConcurrentCollections.newBlockingQueue(), + CLUSTER_B, + ConcurrentCollections.newBlockingQueue() + ); + try ( + MockTransportService remoteTransportA = startTransport("remoteNodeA", threadPool, capturedHeadersByCluster.get(CLUSTER_A)); + MockTransportService remoteTransportB = startTransport("remoteNodeB", threadPool, capturedHeadersByCluster.get(CLUSTER_B)) + ) { + final String clusterCredentialA = randomBase64UUID(random()); + final boolean useProxyModeA = randomBoolean(); + setupClusterSettings(CLUSTER_A, clusterCredentialA, remoteTransportA.getLocalDiscoNode(), useProxyModeA); + + final String clusterCredentialB = randomBase64UUID(random()); + final boolean useProxyModeB = randomBoolean(); + setupClusterSettings(CLUSTER_B, clusterCredentialB, remoteTransportB.getLocalDiscoNode(), useProxyModeB); + + final boolean minimizeRoundtrips = randomBoolean(); + final Request searchRequest = new Request( + "GET", + String.format( + Locale.ROOT, + "/%s:index-a,%s:index-*/_search?ccs_minimize_roundtrips=%s", + CLUSTER_A, + CLUSTER_B, + minimizeRoundtrips + ) + ); + searchRequest.setOptions( + searchRequest.getOptions() + .toBuilder() + .addHeader("Authorization", UsernamePasswordToken.basicAuthHeaderValue(REMOTE_SEARCH_USER, PASSWORD)) + ); + + final Response response = client().performRequest(searchRequest); + assertOK(response); + assertThat(ObjectPath.createFromResponse(response).evaluate("hits.total.value"), equalTo(0)); + + expectActionsAndHeadersForCluster( + List.copyOf(capturedHeadersByCluster.get(CLUSTER_A)), + useProxyModeA, + minimizeRoundtrips, + clusterCredentialA, + new RoleDescriptorsIntersection( + List.of( + Set.of( + new RoleDescriptor( + RBACEngine.REMOTE_USER_ROLE_NAME, + null, + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices("index-a") + .privileges("read", "read_cross_cluster") + .build() }, + null, + null, + null, + null, + null, + null + ) + ) + ) + ) + ); + expectActionsAndHeadersForCluster( + List.copyOf(capturedHeadersByCluster.get(CLUSTER_B)), + useProxyModeB, + minimizeRoundtrips, + clusterCredentialB, + new RoleDescriptorsIntersection( + List.of( + Set.of( + new RoleDescriptor( + RBACEngine.REMOTE_USER_ROLE_NAME, + null, + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices("index-a") + .privileges("read", "read_cross_cluster") + .build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices("index-b") + .privileges("read", "read_cross_cluster") + .build() }, + null, + null, + null, + null, + null, + null + ) + ) + ) + ) + ); + } + } + + private void setupClusterSettings( + final String clusterAlias, + final String clusterCredential, + final DiscoveryNode remoteNode, + boolean useProxyMode + ) throws IOException { + if (useProxyMode) { + updateRemoteClusterSettings( + clusterAlias, + Map.of("mode", "proxy", "proxy_address", remoteNode.getAddress().toString(), "authorization", clusterCredential) + ); + } else { + updateRemoteClusterSettings( + clusterAlias, + Map.of("seeds", remoteNode.getAddress().toString(), "authorization", clusterCredential) + ); + } + } + + private void expectActionsAndHeadersForCluster( + final List actualActionsWithHeaders, + boolean useProxyMode, + boolean minimizeRoundtrips, + final String clusterCredential, + final RoleDescriptorsIntersection expectedRoleDescriptorsIntersection + ) throws IOException { + final Set expectedActions = new HashSet<>(); + if (minimizeRoundtrips) { + expectedActions.add(SearchAction.NAME); + } else { + expectedActions.add(ClusterSearchShardsAction.NAME); + } + if (false == useProxyMode) { + expectedActions.add(ClusterStateAction.NAME); + } + assertThat( + actualActionsWithHeaders.stream().map(CapturedActionWithHeaders::action).collect(Collectors.toUnmodifiableSet()), + equalTo(expectedActions) + ); + for (CapturedActionWithHeaders actual : actualActionsWithHeaders) { + switch (actual.action) { + // the cluster state action is run by the system user, so we expect an authentication header, instead of remote access + // until we implement remote access handling for internal users + case ClusterStateAction.NAME -> { + assertThat(actual.headers().keySet(), containsInAnyOrder(AuthenticationField.AUTHENTICATION_KEY)); + assertThat( + decodeAuthentication(actual.headers().get(AuthenticationField.AUTHENTICATION_KEY)).getEffectiveSubject().getUser(), + is(SystemUser.INSTANCE) + ); + } + case SearchAction.NAME, ClusterSearchShardsAction.NAME -> { + assertContainsRemoteAccessHeaders(actual.headers()); + assertThat(actual.headers(), hasKey(SecurityServerTransportInterceptor.REMOTE_ACCESS_CLUSTER_CREDENTIAL_HEADER_KEY)); + assertThat( + actual.headers().get(SecurityServerTransportInterceptor.REMOTE_ACCESS_CLUSTER_CREDENTIAL_HEADER_KEY), + equalTo("ApiKey " + clusterCredential) + ); + final var actualRemoteAccessAuthentication = RemoteAccessAuthentication.decode( + actual.headers().get(RemoteAccessAuthentication.REMOTE_ACCESS_AUTHENTICATION_HEADER_KEY) + ); + final var expectedRemoteAccessAuthentication = new RemoteAccessAuthentication( + Authentication.newRealmAuthentication( + new User(REMOTE_SEARCH_USER, REMOTE_SEARCH_ROLE), + new Authentication.RealmRef( + "default_native", + "native", + // Since we are running on a multi-node cluster the actual node name may be different between runs + // so just copy the one from the actual result + actualRemoteAccessAuthentication.getAuthentication().getEffectiveSubject().getRealm().getNodeName() + ) + ), + expectedRoleDescriptorsIntersection + ); + assertThat(actualRemoteAccessAuthentication, equalTo(expectedRemoteAccessAuthentication)); + } + default -> fail("Unexpected action [" + actual.action + "]"); + } + } + } + + private static MockTransportService startTransport( + final String nodeName, + final ThreadPool threadPool, + final BlockingQueue capturedHeaders + ) { + boolean success = false; + final Settings settings = Settings.builder().put("node.name", nodeName).build(); + final ClusterName clusterName = ClusterName.CLUSTER_NAME_SETTING.get(settings); + final MockTransportService service = MockTransportService.createNewService(settings, Version.CURRENT, threadPool, null); + try { + service.registerRequestHandler( + ClusterStateAction.NAME, + ThreadPool.Names.SAME, + ClusterStateRequest::new, + (request, channel, task) -> { + capturedHeaders.add( + new CapturedActionWithHeaders(task.getAction(), Map.copyOf(threadPool.getThreadContext().getHeaders())) + ); + channel.sendResponse(new ClusterStateResponse(clusterName, ClusterState.builder(clusterName).build(), false)); + } + ); + service.registerRequestHandler( + ClusterSearchShardsAction.NAME, + ThreadPool.Names.SAME, + ClusterSearchShardsRequest::new, + (request, channel, task) -> { + capturedHeaders.add( + new CapturedActionWithHeaders(task.getAction(), Map.copyOf(threadPool.getThreadContext().getHeaders())) + ); + channel.sendResponse( + new ClusterSearchShardsResponse(new ClusterSearchShardsGroup[0], new DiscoveryNode[0], Collections.emptyMap()) + ); + } + ); + service.registerRequestHandler(SearchAction.NAME, ThreadPool.Names.SAME, SearchRequest::new, (request, channel, task) -> { + capturedHeaders.add( + new CapturedActionWithHeaders(task.getAction(), Map.copyOf(threadPool.getThreadContext().getHeaders())) + ); + channel.sendResponse( + new SearchResponse( + new InternalSearchResponse( + new SearchHits(new SearchHit[0], new TotalHits(0, TotalHits.Relation.EQUAL_TO), Float.NaN), + InternalAggregations.EMPTY, + null, + null, + false, + null, + 1 + ), + null, + 1, + 1, + 0, + 100, + ShardSearchFailure.EMPTY_ARRAY, + SearchResponse.Clusters.EMPTY + ) + ); + }); + service.start(); + service.acceptIncomingRequests(); + success = true; + return service; + } finally { + if (success == false) { + service.close(); + } + } + } + + private void assertContainsRemoteAccessHeaders(final Map actualHeaders) { + assertThat( + actualHeaders.keySet(), + containsInAnyOrder( + RemoteAccessAuthentication.REMOTE_ACCESS_AUTHENTICATION_HEADER_KEY, + SecurityServerTransportInterceptor.REMOTE_ACCESS_CLUSTER_CREDENTIAL_HEADER_KEY + ) + ); + } + + private Authentication decodeAuthentication(final String rawAuthentication) throws IOException { + final var threadContext = new ThreadContext(Settings.EMPTY); + threadContext.putHeader(AuthenticationField.AUTHENTICATION_KEY, rawAuthentication); + return Objects.requireNonNull(new AuthenticationContextSerializer().readFromContext(threadContext)); + } + + private record CapturedActionWithHeaders(String action, Map headers) {} + + private static void updateRemoteClusterSettings(final String clusterAlias, final Map settings) throws IOException { + final Request request = new Request("PUT", "/_cluster/settings"); + request.setEntity(buildUpdateSettingsRequestBody(clusterAlias, settings)); + final Response response = adminClient().performRequest(request); + assertOK(response); + assertEquals(200, response.getStatusLine().getStatusCode()); + } + + private static HttpEntity buildUpdateSettingsRequestBody(final String clusterAlias, final Map settings) + throws IOException { + final String requestBody; + try (XContentBuilder builder = JsonXContent.contentBuilder()) { + builder.startObject(); + { + builder.startObject("persistent"); + { + builder.startObject("cluster.remote." + clusterAlias); + { + for (Map.Entry entry : settings.entrySet()) { + builder.field(entry.getKey(), entry.getValue()); + } + } + builder.endObject(); + } + builder.endObject(); + } + builder.endObject(); + requestBody = Strings.toString(builder); + } + return new NStringEntity(requestBody, ContentType.APPLICATION_JSON); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java index b073c7e8eeb6..595c25100c93 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java @@ -10,6 +10,9 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.cluster.shards.ClusterSearchShardsAction; +import org.elasticsearch.action.search.SearchAction; +import org.elasticsearch.action.search.SearchTransportService; import org.elasticsearch.action.support.DestructiveOperations; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.ssl.SslConfiguration; @@ -21,7 +24,9 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.RemoteConnectionManager; import org.elasticsearch.transport.SendRequestTransportException; +import org.elasticsearch.transport.TcpTransport; import org.elasticsearch.transport.Transport; +import org.elasticsearch.transport.TransportActionProxy; import org.elasticsearch.transport.TransportChannel; import org.elasticsearch.transport.TransportInterceptor; import org.elasticsearch.transport.TransportRequest; @@ -33,7 +38,11 @@ import org.elasticsearch.transport.TransportService.ContextRestoreResponseHandler; import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.SecurityContext; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.RemoteAccessAuthentication; +import org.elasticsearch.xpack.core.security.authc.Subject; import org.elasticsearch.xpack.core.security.transport.ProfileConfigurations; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.security.authc.AuthenticationService; import org.elasticsearch.xpack.security.authz.AuthorizationService; @@ -43,13 +52,42 @@ import java.util.Collections; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.elasticsearch.xpack.core.security.SecurityField.setting; public class SecurityServerTransportInterceptor implements TransportInterceptor { + public static final String REMOTE_ACCESS_CLUSTER_CREDENTIAL_HEADER_KEY = "_remote_access_cluster_credential"; + private static final Version VERSION_REMOTE_ACCESS_HEADERS = Version.V_8_7_0; private static final Logger logger = LogManager.getLogger(SecurityServerTransportInterceptor.class); + // package private for testing + static final Set REMOTE_ACCESS_ACTION_ALLOWLIST; + static { + final Stream actions = Stream.of( + SearchAction.NAME, + ClusterSearchShardsAction.NAME, + SearchTransportService.FREE_CONTEXT_SCROLL_ACTION_NAME, + SearchTransportService.FREE_CONTEXT_ACTION_NAME, + SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME, + SearchTransportService.DFS_ACTION_NAME, + SearchTransportService.QUERY_ACTION_NAME, + SearchTransportService.QUERY_ID_ACTION_NAME, + SearchTransportService.QUERY_SCROLL_ACTION_NAME, + SearchTransportService.QUERY_FETCH_SCROLL_ACTION_NAME, + SearchTransportService.FETCH_ID_SCROLL_ACTION_NAME, + SearchTransportService.FETCH_ID_ACTION_NAME, + SearchTransportService.QUERY_CAN_MATCH_NAME, + SearchTransportService.QUERY_CAN_MATCH_NODE_NAME + ); + REMOTE_ACCESS_ACTION_ALLOWLIST = actions + // Include action, and proxy equivalent (i.e., with proxy action prefix) + .flatMap(name -> Stream.of(name, TransportActionProxy.getProxyAction(name))) + .collect(Collectors.toUnmodifiableSet()); + } private final AuthenticationService authcService; private final AuthorizationService authzService; @@ -109,6 +147,14 @@ public SecurityServerTransportInterceptor( @Override public AsyncSender interceptSender(AsyncSender sender) { + return interceptForAllRequests( + // Branching based on the feature flag is not strictly necessary here, but it makes it more obvious we are not interfering with + // non-feature-flagged deployments + TcpTransport.isUntrustedRemoteClusterEnabled() ? interceptForRemoteAccessRequests(sender) : sender + ); + } + + private AsyncSender interceptForAllRequests(AsyncSender sender) { return new AsyncSender() { @Override public void sendRequest( @@ -118,6 +164,7 @@ public void sendRequest( TransportRequestOptions options, TransportResponseHandler handler ) { + assertNoRemoteAccessHeadersInContext(); final Optional remoteClusterAlias = remoteClusterAliasResolver.apply(connection); if (PreAuthorizationUtils.shouldRemoveParentAuthorizationFromThreadContext(remoteClusterAlias, action, securityContext)) { securityContext.executeAfterRemovingParentAuthorization(original -> { @@ -134,6 +181,14 @@ public void sendRequest( sendRequestInner(sender, connection, action, request, options, handler); } } + + private void assertNoRemoteAccessHeadersInContext() { + assert securityContext.getThreadContext().getHeader(REMOTE_ACCESS_CLUSTER_CREDENTIAL_HEADER_KEY) == null + : "remote access headers should not be in security context"; + assert securityContext.getThreadContext() + .getHeader(RemoteAccessAuthentication.REMOTE_ACCESS_AUTHENTICATION_HEADER_KEY) == null + : "remote access headers should not be in security context"; + } }; } @@ -196,6 +251,129 @@ public void sendRequestInner( } } + private AsyncSender interceptForRemoteAccessRequests(final AsyncSender sender) { + return new AsyncSender() { + @Override + public void sendRequest( + Transport.Connection connection, + String action, + TransportRequest request, + TransportRequestOptions options, + TransportResponseHandler handler + ) { + final Optional remoteAccessCredentials = getRemoteAccessCredentials(connection, action); + if (remoteAccessCredentials.isPresent()) { + sendWithRemoteAccessHeaders(remoteAccessCredentials.get(), connection, action, request, options, handler); + } else { + // Send regular request, without remote access headers + try { + sender.sendRequest(connection, action, request, options, handler); + } catch (Exception e) { + handler.handleException(new SendRequestTransportException(connection.getNode(), action, e)); + } + } + } + + /** + * Returns cluster credentials if the connection is remote, cluster credentials are set up for the target cluster, and access + * via remote access headers is supported for the request type and authenticated subject. If remote access is not supported, + * this method does not return credentials even if they are configured, to signify that the request should be sent according to + * the basic security model + */ + private Optional getRemoteAccessCredentials(Transport.Connection connection, String action) { + final Optional optionalRemoteClusterAlias = remoteClusterAliasResolver.apply(connection); + if (optionalRemoteClusterAlias.isEmpty()) { + logger.trace("Connection is not remote"); + return Optional.empty(); + } + + final String remoteClusterAlias = optionalRemoteClusterAlias.get(); + final String remoteClusterCredential = remoteClusterAuthorizationResolver.resolveAuthorization(remoteClusterAlias); + if (remoteClusterCredential == null) { + logger.trace("No cluster credential is configured for remote cluster [{}]", remoteClusterAlias); + return Optional.empty(); + } + + if (false == REMOTE_ACCESS_ACTION_ALLOWLIST.contains(action)) { + logger.info("Action [{}] towards remote cluster [{}] is not allow-listed", action, remoteClusterAlias); + return Optional.empty(); + } + + final Authentication authentication = securityContext.getAuthentication(); + assert authentication != null : "authentication must be present in security context"; + final Subject effectiveSubject = authentication.getEffectiveSubject(); + if (false == effectiveSubject.getType().equals(Subject.Type.USER)) { + logger.trace( + "Effective subject of request to remote cluster [{}] has an unsupported type [{}]", + remoteClusterAlias, + effectiveSubject.getType() + ); + return Optional.empty(); + } + if (User.isInternal(effectiveSubject.getUser())) { + logger.trace("Effective subject of request to remote cluster [{}] is an internal user", remoteClusterAlias); + return Optional.empty(); + } + + return Optional.of(new RemoteAccessCredentials(remoteClusterAlias, remoteClusterCredential)); + } + + private void sendWithRemoteAccessHeaders( + final RemoteAccessCredentials remoteAccessCredentials, + final Transport.Connection connection, + final String action, + final TransportRequest request, + final TransportRequestOptions options, + final TransportResponseHandler handler + ) { + final String remoteClusterAlias = remoteAccessCredentials.clusterAlias(); + if (connection.getVersion().before(VERSION_REMOTE_ACCESS_HEADERS)) { + throw new IllegalArgumentException( + "Settings for remote cluster [" + + remoteClusterAlias + + "] indicate remote access headers should be sent but target cluster version [" + + connection.getVersion() + + "] does not support receiving them" + ); + } + + logger.debug( + "Sending [{}] request to [{}] with remote access headers for [{}] action", + request.getClass(), + remoteClusterAlias, + action + ); + + final Authentication authentication = securityContext.getAuthentication(); + assert authentication != null : "authentication must be present in security context"; + + final ThreadContext threadContext = securityContext.getThreadContext(); + final var contextRestoreHandler = new ContextRestoreResponseHandler<>(threadContext.newRestorableContext(true), handler); + authzService.retrieveRemoteAccessRoleDescriptorsIntersection( + remoteClusterAlias, + authentication.getEffectiveSubject(), + ActionListener.wrap(roleDescriptorsIntersection -> { + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { + remoteAccessCredentials.writeToContext(threadContext); + new RemoteAccessAuthentication(authentication, roleDescriptorsIntersection).writeToContext(threadContext); + sender.sendRequest(connection, action, request, options, contextRestoreHandler); + } + }, e -> contextRestoreHandler.handleException(new SendRequestTransportException(connection.getNode(), action, e))) + ); + } + + record RemoteAccessCredentials(String clusterAlias, String credentials) { + void writeToContext(final ThreadContext ctx) { + ctx.putHeader(REMOTE_ACCESS_CLUSTER_CREDENTIAL_HEADER_KEY, withApiKeyPrefix(credentials)); + } + + private String withApiKeyPrefix(final String clusterCredential) { + return "ApiKey " + clusterCredential; + } + } + }; + } + private void sendWithUser( Transport.Connection connection, String action, @@ -366,6 +544,7 @@ public void onResponse(Void unused) { } else { final Thread executingThread = Thread.currentThread(); filterListener = new AbstractFilterListener(receiveMessage) { + @Override public void onResponse(Void unused) { if (executingThread == Thread.currentThread()) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java index a9bf0fb61f9c..dbd4e7f512e1 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java @@ -7,19 +7,26 @@ package org.elasticsearch.xpack.security.transport; import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; +import org.elasticsearch.action.admin.cluster.stats.ClusterStatsRequest; import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction; import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; import org.elasticsearch.action.support.DestructiveOperations; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.core.Tuple; import org.elasticsearch.tasks.Task; import org.elasticsearch.test.ClusterServiceUtils; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.VersionUtils; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.SendRequestTransportException; +import org.elasticsearch.transport.TcpTransport; import org.elasticsearch.transport.Transport; import org.elasticsearch.transport.Transport.Connection; import org.elasticsearch.transport.TransportChannel; @@ -35,7 +42,10 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper; +import org.elasticsearch.xpack.core.security.authc.RemoteAccessAuthentication; import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection; +import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.user.AsyncSearchUser; import org.elasticsearch.xpack.core.security.user.SecurityProfileUser; import org.elasticsearch.xpack.core.security.user.SystemUser; @@ -46,27 +56,38 @@ import org.elasticsearch.xpack.security.authc.AuthenticationService; import org.elasticsearch.xpack.security.authz.AuthorizationService; import org.junit.After; +import org.mockito.ArgumentCaptor; import java.io.IOException; import java.util.Collections; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import static org.elasticsearch.test.ActionListenerUtils.anyActionListener; import static org.elasticsearch.xpack.core.ClientHelper.ASYNC_SEARCH_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_PROFILE_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.TRANSFORM_ORIGIN; +import static org.elasticsearch.xpack.core.security.authc.RemoteAccessAuthentication.REMOTE_ACCESS_AUTHENTICATION_HEADER_KEY; +import static org.elasticsearch.xpack.core.security.authz.RoleDescriptorTests.randomUniquelyNamedRoleDescriptors; +import static org.elasticsearch.xpack.security.transport.SecurityServerTransportInterceptor.REMOTE_ACCESS_ACTION_ALLOWLIST; +import static org.elasticsearch.xpack.security.transport.SecurityServerTransportInterceptor.REMOTE_ACCESS_CLUSTER_CREDENTIAL_HEADER_KEY; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -534,6 +555,284 @@ public boolean decRef() { assertTrue(exceptionSent.get()); } + public void testSendWithRemoteAccessHeaders() throws Exception { + assumeTrue("untrusted remote cluster feature flag must be enabled", TcpTransport.isUntrustedRemoteClusterEnabled()); + + final Authentication authentication = AuthenticationTestHelper.builder() + .user(new User(randomAlphaOfLengthBetween(3, 10), randomRoles())) + .realm() + .build(); + authentication.writeToContext(threadContext); + final RemoteClusterAuthorizationResolver remoteClusterAuthorizationResolver = mock(RemoteClusterAuthorizationResolver.class); + final String remoteClusterCredential = randomAlphaOfLengthBetween(10, 42); + when(remoteClusterAuthorizationResolver.resolveAuthorization(any())).thenReturn(remoteClusterCredential); + final String remoteClusterAlias = randomAlphaOfLengthBetween(5, 10); + final AuthorizationService authzService = mock(AuthorizationService.class); + // We capture the listener so that we can complete the full flow, by calling onResponse further down + @SuppressWarnings("unchecked") + final ArgumentCaptor> listenerCaptor = ArgumentCaptor.forClass(ActionListener.class); + doAnswer(i -> null).when(authzService).retrieveRemoteAccessRoleDescriptorsIntersection(any(), any(), listenerCaptor.capture()); + + final SecurityServerTransportInterceptor interceptor = new SecurityServerTransportInterceptor( + settings, + threadPool, + mock(AuthenticationService.class), + authzService, + mock(SSLService.class), + securityContext, + new DestructiveOperations( + Settings.EMPTY, + new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) + ), + remoteClusterAuthorizationResolver, + ignored -> Optional.of(remoteClusterAlias) + ); + + final AtomicBoolean calledWrappedSender = new AtomicBoolean(false); + final AtomicReference sentCredential = new AtomicReference<>(); + final AtomicReference sentRemoteAccessAuthentication = new AtomicReference<>(); + final AsyncSender sender = interceptor.interceptSender(new AsyncSender() { + @Override + public void sendRequest( + Transport.Connection connection, + String action, + TransportRequest request, + TransportRequestOptions options, + TransportResponseHandler handler + ) { + if (calledWrappedSender.compareAndSet(false, true) == false) { + fail("sender called more than once"); + } + assertThat(securityContext.getAuthentication(), nullValue()); + sentCredential.set(securityContext.getThreadContext().getHeader(REMOTE_ACCESS_CLUSTER_CREDENTIAL_HEADER_KEY)); + try { + sentRemoteAccessAuthentication.set(RemoteAccessAuthentication.readFromContext(securityContext.getThreadContext())); + } catch (IOException e) { + fail("no exceptions expected but got " + e); + } + handler.handleResponse(null); + } + }); + final Transport.Connection connection = mock(Transport.Connection.class); + when(connection.getVersion()).thenReturn(Version.CURRENT); + final Tuple actionAndReq = randomAllowlistedActionAndRequest(); + sender.sendRequest(connection, actionAndReq.v1(), actionAndReq.v2(), null, new TransportResponseHandler<>() { + @Override + public void handleResponse(TransportResponse response) { + // Headers should get restored before handle response is called + assertThat(securityContext.getAuthentication(), equalTo(authentication)); + assertThat(securityContext.getThreadContext().getHeader(REMOTE_ACCESS_AUTHENTICATION_HEADER_KEY), nullValue()); + assertThat(securityContext.getThreadContext().getHeader(REMOTE_ACCESS_CLUSTER_CREDENTIAL_HEADER_KEY), nullValue()); + } + + @Override + public void handleException(TransportException exp) { + fail("no exceptions expected but got " + exp); + } + + @Override + public TransportResponse read(StreamInput in) { + return null; + } + }); + // Call listener to complete flow + final RoleDescriptorsIntersection expectedRoleDescriptorsIntersection = new RoleDescriptorsIntersection( + randomList(0, 3, () -> Set.copyOf(randomUniquelyNamedRoleDescriptors(0, 1))) + ); + listenerCaptor.getValue().onResponse(expectedRoleDescriptorsIntersection); + assertTrue(calledWrappedSender.get()); + assertThat(sentCredential.get(), equalTo("ApiKey " + remoteClusterCredential)); + assertThat( + sentRemoteAccessAuthentication.get(), + equalTo(new RemoteAccessAuthentication(authentication, expectedRoleDescriptorsIntersection)) + ); + verify(securityContext, never()).executeAsInternalUser(any(), any(), anyConsumer()); + verify(remoteClusterAuthorizationResolver, times(1)).resolveAuthorization(eq(remoteClusterAlias)); + verify(authzService).retrieveRemoteAccessRoleDescriptorsIntersection( + eq(remoteClusterAlias), + eq(authentication.getEffectiveSubject()), + anyActionListener() + ); + assertThat(securityContext.getThreadContext().getHeader(REMOTE_ACCESS_AUTHENTICATION_HEADER_KEY), nullValue()); + assertThat(securityContext.getThreadContext().getHeader(REMOTE_ACCESS_CLUSTER_CREDENTIAL_HEADER_KEY), nullValue()); + } + + public void testSendWithUserIfRemoteAccessHeadersConditionNotMet() throws Exception { + assumeTrue("untrusted remote cluster feature flag must be enabled", TcpTransport.isUntrustedRemoteClusterEnabled()); + + boolean noCredential = randomBoolean(); + final boolean notRemoteConnection = randomBoolean(); + final boolean nonAllowlistedRequest = randomBoolean(); + final boolean unsupportedAuthentication = randomBoolean(); + // Ensure at least one condition fails + if (false == (notRemoteConnection || noCredential || nonAllowlistedRequest || unsupportedAuthentication)) { + noCredential = true; + } + final RemoteClusterAuthorizationResolver remoteClusterAuthorizationResolver = mock(RemoteClusterAuthorizationResolver.class); + when(remoteClusterAuthorizationResolver.resolveAuthorization(any())).thenReturn( + noCredential ? null : randomAlphaOfLengthBetween(10, 42) + ); + final AuthenticationTestHelper.AuthenticationTestBuilder builder = AuthenticationTestHelper.builder(); + final Authentication authentication; + if (unsupportedAuthentication) { + authentication = randomFrom(builder.apiKey().build(), builder.serviceAccount().build(), builder.internal().build()); + } else { + authentication = builder.user(new User(randomAlphaOfLengthBetween(3, 10), randomRoles())).realm().build(); + } + authentication.writeToContext(threadContext); + final Tuple actionAndReq = nonAllowlistedRequest + ? new Tuple<>(ClusterStateAction.NAME, mock(ClusterStatsRequest.class)) + : randomAllowlistedActionAndRequest(); + + final String remoteClusterAlias = randomAlphaOfLengthBetween(5, 10); + final AuthorizationService authzService = mock(AuthorizationService.class); + final SecurityServerTransportInterceptor interceptor = new SecurityServerTransportInterceptor( + settings, + threadPool, + mock(AuthenticationService.class), + authzService, + mock(SSLService.class), + securityContext, + new DestructiveOperations( + Settings.EMPTY, + new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) + ), + remoteClusterAuthorizationResolver, + ignored -> notRemoteConnection ? Optional.empty() : Optional.of(remoteClusterAlias) + ); + + final AtomicBoolean calledWrappedSender = new AtomicBoolean(false); + final AtomicReference sentAuthentication = new AtomicReference<>(); + final AsyncSender sender = interceptor.interceptSender(new AsyncSender() { + @Override + public void sendRequest( + Transport.Connection connection, + String action, + TransportRequest request, + TransportRequestOptions options, + TransportResponseHandler handler + ) { + if (calledWrappedSender.compareAndSet(false, true) == false) { + fail("sender called more than once"); + } + sentAuthentication.set(securityContext.getAuthentication()); + assertThat(securityContext.getThreadContext().getHeader(REMOTE_ACCESS_CLUSTER_CREDENTIAL_HEADER_KEY), nullValue()); + assertThat(securityContext.getThreadContext().getHeader(REMOTE_ACCESS_AUTHENTICATION_HEADER_KEY), nullValue()); + } + }); + final Transport.Connection connection = mock(Transport.Connection.class); + when(connection.getVersion()).thenReturn(Version.CURRENT); + sender.sendRequest(connection, actionAndReq.v1(), actionAndReq.v2(), null, null); + assertTrue(calledWrappedSender.get()); + assertThat(sentAuthentication.get(), equalTo(authentication)); + verify(authzService, never()).retrieveRemoteAccessRoleDescriptorsIntersection(any(), any(), anyActionListener()); + assertThat(securityContext.getThreadContext().getHeader(REMOTE_ACCESS_AUTHENTICATION_HEADER_KEY), nullValue()); + assertThat(securityContext.getThreadContext().getHeader(REMOTE_ACCESS_CLUSTER_CREDENTIAL_HEADER_KEY), nullValue()); + } + + public void testSendWithRemoteAccessHeadersThrowsOnOldConnection() throws Exception { + assumeTrue("untrusted remote cluster feature flag must be enabled", TcpTransport.isUntrustedRemoteClusterEnabled()); + + final Authentication authentication = AuthenticationTestHelper.builder() + .user( + new User( + randomAlphaOfLengthBetween(3, 10), + randomArray( + 0, + 4, + String[]::new, + () -> randomValueOtherThanMany(ReservedRolesStore::isReserved, () -> randomAlphaOfLengthBetween(1, 20)) + ) + ) + ) + .realm() + .build(); + authentication.writeToContext(threadContext); + final RemoteClusterAuthorizationResolver remoteClusterAuthorizationResolver = mock(RemoteClusterAuthorizationResolver.class); + final String remoteClusterCredential = randomAlphaOfLengthBetween(10, 42); + when(remoteClusterAuthorizationResolver.resolveAuthorization(any())).thenReturn(remoteClusterCredential); + final String remoteClusterAlias = randomAlphaOfLengthBetween(5, 10); + + final SecurityServerTransportInterceptor interceptor = new SecurityServerTransportInterceptor( + settings, + threadPool, + mock(AuthenticationService.class), + mock(AuthorizationService.class), + mock(SSLService.class), + securityContext, + new DestructiveOperations( + Settings.EMPTY, + new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) + ), + remoteClusterAuthorizationResolver, + ignored -> Optional.of(remoteClusterAlias) + ); + + final AsyncSender sender = interceptor.interceptSender(new AsyncSender() { + @Override + public void sendRequest( + Transport.Connection connection, + String action, + TransportRequest request, + TransportRequestOptions options, + TransportResponseHandler handler + ) { + fail("sender should not be called"); + } + }); + final Transport.Connection connection = mock(Transport.Connection.class); + final Version versionBeforeRemoteAccessHeaders = VersionUtils.getPreviousVersion(Version.V_8_7_0); + final Version version = VersionUtils.randomVersionBetween( + random(), + versionBeforeRemoteAccessHeaders.minimumCompatibilityVersion(), + versionBeforeRemoteAccessHeaders + ); + when(connection.getVersion()).thenReturn(version); + final Tuple actionAndReq = randomAllowlistedActionAndRequest(); + final AtomicBoolean calledHandleException = new AtomicBoolean(false); + final AtomicReference actualException = new AtomicReference<>(); + sender.sendRequest(connection, actionAndReq.v1(), actionAndReq.v2(), null, new TransportResponseHandler<>() { + @Override + public void handleResponse(TransportResponse response) { + fail("should not receive a response"); + } + + @Override + public void handleException(TransportException exp) { + if (calledHandleException.compareAndSet(false, true) == false) { + fail("handle exception called more than once"); + } + actualException.set(exp); + } + + @Override + public TransportResponse read(StreamInput in) { + fail("should not receive a response"); + return null; + } + }); + assertThat(actualException.get(), instanceOf(SendRequestTransportException.class)); + assertThat(actualException.get().getCause(), instanceOf(IllegalArgumentException.class)); + assertThat( + actualException.get().getCause().getMessage(), + equalTo( + "Settings for remote cluster [" + + remoteClusterAlias + + "] indicate remote access headers should be sent but target cluster version [" + + connection.getVersion() + + "] does not support receiving them" + ) + ); + verify(remoteClusterAuthorizationResolver, times(1)).resolveAuthorization(eq(remoteClusterAlias)); + assertThat(securityContext.getThreadContext().getHeader(REMOTE_ACCESS_AUTHENTICATION_HEADER_KEY), nullValue()); + assertThat(securityContext.getThreadContext().getHeader(REMOTE_ACCESS_CLUSTER_CREDENTIAL_HEADER_KEY), nullValue()); + } + + private Tuple randomAllowlistedActionAndRequest() { + final String action = randomFrom(REMOTE_ACCESS_ACTION_ALLOWLIST.toArray(new String[0])); + return new Tuple<>(action, mock(TransportRequest.class)); + } + private String[] randomRoles() { return generateRandomStringArray(3, 10, false, true); } From b3029c9899466fefc5e528cb7fe44b16b2591ad7 Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Wed, 21 Dec 2022 10:55:14 +0100 Subject: [PATCH 324/919] bypass checksum validation (#92461) This introduce a flag to bypass the checksum validation (for the cases if checksum is not available). Is is a temporary solution to be used in a plugin. --- .../indices/recovery/MultiFileWriter.java | 48 +++++--- .../recovery/MultiFileWriterTests.java | 114 ++++++++++++++++++ 2 files changed, 148 insertions(+), 14 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/indices/recovery/MultiFileWriterTests.java diff --git a/server/src/main/java/org/elasticsearch/indices/recovery/MultiFileWriter.java b/server/src/main/java/org/elasticsearch/indices/recovery/MultiFileWriter.java index 954c2b616d24..cb1168caf6e9 100644 --- a/server/src/main/java/org/elasticsearch/indices/recovery/MultiFileWriter.java +++ b/server/src/main/java/org/elasticsearch/indices/recovery/MultiFileWriter.java @@ -37,26 +37,40 @@ public class MultiFileWriter extends AbstractRefCounted implements Releasable { - public MultiFileWriter(Store store, RecoveryState.Index indexState, String tempFilePrefix, Logger logger, Runnable ensureOpen) { - this.store = store; - this.indexState = indexState; - this.tempFilePrefix = tempFilePrefix; - this.logger = logger; - this.ensureOpen = ensureOpen; - } - - private final Runnable ensureOpen; - private final AtomicBoolean closed = new AtomicBoolean(false); - private final Logger logger; private final Store store; private final RecoveryState.Index indexState; private final String tempFilePrefix; + private final Logger logger; + private final Runnable ensureOpen; + private final boolean verifyOutput; + + private final AtomicBoolean closed = new AtomicBoolean(false); private final ConcurrentMap openIndexOutputs = ConcurrentCollections.newConcurrentMap(); private final ConcurrentMap fileChunkWriters = ConcurrentCollections.newConcurrentMap(); final Map tempFileNames = ConcurrentCollections.newConcurrentMap(); + public MultiFileWriter(Store store, RecoveryState.Index indexState, String tempFilePrefix, Logger logger, Runnable ensureOpen) { + this(store, indexState, tempFilePrefix, logger, ensureOpen, true); + } + + public MultiFileWriter( + Store store, + RecoveryState.Index indexState, + String tempFilePrefix, + Logger logger, + Runnable ensureOpen, + boolean verifyOutput + ) { + this.store = store; + this.indexState = indexState; + this.tempFilePrefix = tempFilePrefix; + this.logger = logger; + this.ensureOpen = ensureOpen; + this.verifyOutput = verifyOutput; + } + public void writeFileChunk(StoreFileMetadata fileMetadata, long position, ReleasableBytesReference content, boolean lastChunk) throws IOException { assert Transports.assertNotTransportThread("multi_file_writer"); @@ -69,7 +83,7 @@ public void writeFileChunk(StoreFileMetadata fileMetadata, long position, Releas } } - public void writeFile(StoreFileMetadata fileMetadata, long readSnapshotFileBufferSize, InputStream stream) throws Exception { + public void writeFile(StoreFileMetadata fileMetadata, long readSnapshotFileBufferSize, InputStream stream) throws IOException { ensureOpen.run(); assert Transports.assertNotTransportThread("multi_file_writer"); @@ -81,7 +95,7 @@ public void writeFile(StoreFileMetadata fileMetadata, long readSnapshotFileBuffe tempFileNames.put(tempFileName, fileName); incRef(); - try (IndexOutput indexOutput = store.createVerifyingOutput(tempFileName, fileMetadata, IOContext.DEFAULT)) { + try (IndexOutput indexOutput = createIndexOutput(tempFileName, fileMetadata, IOContext.DEFAULT)) { int bufferSize = Math.toIntExact(Math.min(readSnapshotFileBufferSize, fileMetadata.length())); byte[] buffer = new byte[bufferSize]; int length; @@ -107,7 +121,7 @@ public void writeFile(StoreFileMetadata fileMetadata, long readSnapshotFileBuffe assert Arrays.asList(store.directory().listAll()).contains(tempFileName) : "expected: [" + tempFileName + "] in " + Arrays.toString(store.directory().listAll()); store.directory().sync(Collections.singleton(tempFileName)); - } catch (Exception e) { + } catch (IOException e) { tempFileNames.remove(tempFileName); store.deleteQuiet(tempFileName); indexState.resetRecoveredBytesOfFile(fileName); @@ -117,6 +131,12 @@ public void writeFile(StoreFileMetadata fileMetadata, long readSnapshotFileBuffe } } + private IndexOutput createIndexOutput(String tempFileName, StoreFileMetadata fileMetadata, IOContext context) throws IOException { + return verifyOutput + ? store.createVerifyingOutput(tempFileName, fileMetadata, context) + : store.directory().createOutput(tempFileName, context); + } + /** Get a temporary name for the provided file name. */ String getTempNameForFile(String origFile) { return tempFilePrefix + origFile; diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/MultiFileWriterTests.java b/server/src/test/java/org/elasticsearch/indices/recovery/MultiFileWriterTests.java new file mode 100644 index 000000000000..56fe971ce6d8 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/indices/recovery/MultiFileWriterTests.java @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.indices.recovery; + +import org.apache.lucene.codecs.CodecUtil; +import org.apache.lucene.index.CorruptIndexException; +import org.apache.lucene.store.ByteBuffersDataOutput; +import org.apache.lucene.store.ByteBuffersIndexOutput; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.IOContext; +import org.apache.lucene.tests.util.TestUtil; +import org.elasticsearch.Version; +import org.elasticsearch.index.shard.IndexShard; +import org.elasticsearch.index.shard.IndexShardTestCase; +import org.elasticsearch.index.store.Store; +import org.elasticsearch.index.store.StoreFileMetadata; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +public class MultiFileWriterTests extends IndexShardTestCase { + + private IndexShard indexShard; + private Directory directory; + private Directory directorySpy; + private Store store; + + @Override + public void setUp() throws Exception { + super.setUp(); + indexShard = newShard(true); + directory = newFSDirectory(indexShard.shardPath().resolveIndex()); + directorySpy = spy(directory); + store = createStore(indexShard.shardId(), indexShard.indexSettings(), directorySpy); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + directory.close(); + closeShards(indexShard); + } + + public void testWritesFile() throws IOException { + var fileWriter = createMultiFileWriter(true); + var file = createFile("file"); + + fileWriter.writeFile(file.metadata, 10, new ByteArrayInputStream(file.bytes)); + + verify(directorySpy).createOutput("temp_file", IOContext.DEFAULT); + verify(directorySpy).sync(Collections.singleton("temp_file")); + } + + public void testFailsToWriteFileWithIncorrectChecksum() throws IOException { + var fileWriter = createMultiFileWriter(true); + var file = createFile("file"); + + expectThrows( + CorruptIndexException.class, + () -> fileWriter.writeFile(withWrongChecksum(file.metadata), 10, new ByteArrayInputStream(file.bytes)) + ); + + verify(directorySpy).createOutput("temp_file", IOContext.DEFAULT); + verify(directorySpy).deleteFile("temp_file"); + verify(directorySpy, never()).sync(anyCollection()); + } + + public void testWritesFileWithIncorrectChecksumWithoutVerification() throws IOException { + var fileWriter = createMultiFileWriter(false); + var file = createFile("file"); + + fileWriter.writeFile(withWrongChecksum(file.metadata), 10, new ByteArrayInputStream(file.bytes)); + + verify(directorySpy).createOutput("temp_file", IOContext.DEFAULT); + verify(directorySpy).sync(Collections.singleton("temp_file")); + } + + private MultiFileWriter createMultiFileWriter(boolean verifyOutput) { + return new MultiFileWriter(store, mock(RecoveryState.Index.class), "temp_", logger, mock(Runnable.class), verifyOutput); + } + + private record FileAndMetadata(byte[] bytes, StoreFileMetadata metadata) {} + + private static FileAndMetadata createFile(String name) throws IOException { + var buffer = new ByteBuffersDataOutput(); + var output = new ByteBuffersIndexOutput(buffer, "test", name); + output.writeString(TestUtil.randomRealisticUnicodeString(random(), 10, 1024)); + CodecUtil.writeBEInt(output, CodecUtil.FOOTER_MAGIC); + CodecUtil.writeBEInt(output, 0); + String checksum = Store.digestToString(output.getChecksum()); + CodecUtil.writeBELong(output, output.getChecksum()); + output.close(); + + return new FileAndMetadata(buffer.toArrayCopy(), new StoreFileMetadata(name, buffer.size(), checksum, Version.CURRENT.toString())); + } + + private static StoreFileMetadata withWrongChecksum(StoreFileMetadata metadata) { + var newChecksum = randomValueOtherThan(metadata.checksum(), () -> randomAlphaOfLength(6)); + return new StoreFileMetadata(metadata.name(), metadata.length(), newChecksum, metadata.writtenBy()); + } +} From 562fdc9f071f12781c5801635dde2091ee1f708a Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Wed, 21 Dec 2022 11:59:06 +0100 Subject: [PATCH 325/919] Make clean up files step configurable for peer-recovery of replicas (#92490) Skip the "clean up and verify" step at the end of files based peer-recovery for replicas. --- docs/changelog/92490.yaml | 5 +++++ .../org/elasticsearch/indices/recovery/RecoveryTarget.java | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/92490.yaml diff --git a/docs/changelog/92490.yaml b/docs/changelog/92490.yaml new file mode 100644 index 000000000000..5f6f63f3fc27 --- /dev/null +++ b/docs/changelog/92490.yaml @@ -0,0 +1,5 @@ +pr: 92490 +summary: Make clean up files step configurable for peer-recovery of replicas +area: Recovery +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/indices/recovery/RecoveryTarget.java b/server/src/main/java/org/elasticsearch/indices/recovery/RecoveryTarget.java index aec8f06d5cf8..de94b198c635 100644 --- a/server/src/main/java/org/elasticsearch/indices/recovery/RecoveryTarget.java +++ b/server/src/main/java/org/elasticsearch/indices/recovery/RecoveryTarget.java @@ -495,7 +495,10 @@ public void cleanFiles( final Store store = store(); store.incRef(); try { - store.cleanupAndVerify("recovery CleanFilesRequestHandler", sourceMetadata); + if (DiscoveryNode.isStateless(indexShard.indexSettings().getNodeSettings()) == false + || indexShard.routingEntry().primary()) { + store.cleanupAndVerify("recovery CleanFilesRequestHandler", sourceMetadata); + } final String translogUUID = Translog.createEmptyTranslog( indexShard.shardPath().resolveTranslog(), globalCheckpoint, From b9c0315d242d720da40a41e19187d00c6f3e1909 Mon Sep 17 00:00:00 2001 From: Hendrik Muhs Date: Wed, 21 Dec 2022 12:24:10 +0100 Subject: [PATCH 326/919] [ML] add the ability to include and exclude values in Frequent items (#92414) This PR adds include and excludes to frequent items. This will allow to filter values from the analysis. --- docs/changelog/92414.yaml | 5 + .../frequent-items-aggregation.asciidoc | 6 + .../FrequentItemSetsAggregationBuilder.java | 2 +- .../FrequentItemSetsAggregatorFactory.java | 39 +++-- .../mr/ItemSetMapReduceAggregator.java | 22 +-- .../mr/ItemSetMapReduceValueSource.java | 25 ++- ...equentItemSetsAggregationBuilderTests.java | 31 ++++ .../FrequentItemSetsAggregatorTests.java | 150 ++++++++++++++++-- .../test/ml/frequent_items_agg.yml | 62 ++++++++ 9 files changed, 297 insertions(+), 45 deletions(-) create mode 100644 docs/changelog/92414.yaml diff --git a/docs/changelog/92414.yaml b/docs/changelog/92414.yaml new file mode 100644 index 000000000000..c062122833c4 --- /dev/null +++ b/docs/changelog/92414.yaml @@ -0,0 +1,5 @@ +pr: 92414 +summary: Add the ability to include and exclude values in Frequent items +area: Machine Learning +type: enhancement +issues: [] diff --git a/docs/reference/aggregations/bucket/frequent-items-aggregation.asciidoc b/docs/reference/aggregations/bucket/frequent-items-aggregation.asciidoc index 811f08c98ca0..07bfbfe8e0fe 100644 --- a/docs/reference/aggregations/bucket/frequent-items-aggregation.asciidoc +++ b/docs/reference/aggregations/bucket/frequent-items-aggregation.asciidoc @@ -66,6 +66,12 @@ fields. If the combined cardinality of the analyzed fields are high, then the aggregation might require a significant amount of system resources. +It is possible to filter the values for each field. This can be done using the +`include` and `exclude` parameters which are based on a regular expression string +or arrays of exact terms. This functionality mirrors the features described in the +<> documentation. +Filtered values are removed from the analysis and therefore reduce the runtime. + [discrete] [[frequent-items-minimum-set-size]] ==== Minimum set size diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregationBuilder.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregationBuilder.java index 3fd5e899c75f..627907450faa 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregationBuilder.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregationBuilder.java @@ -65,7 +65,7 @@ public final class FrequentItemSetsAggregationBuilder extends AbstractAggregatio false, // timezone aware false, // filtered (not defined per field, but for all fields below) false, // format - false // includes and excludes + true // includes and excludes ); PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(), (p, n) -> fieldsParser.parse(p, null).build(), FIELDS); PARSER.declareDouble(ConstructingObjectParser.optionalConstructorArg(), MINIMUM_SUPPORT); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregatorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregatorFactory.java index 5842d282d4d3..b17a0b589219 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregatorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregatorFactory.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.ml.aggs.frequentitemsets; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.core.Tuple; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.search.SearchService; import org.elasticsearch.search.aggregations.AggregationExecutionException; @@ -15,6 +16,7 @@ import org.elasticsearch.search.aggregations.AggregatorFactories.Builder; import org.elasticsearch.search.aggregations.AggregatorFactory; import org.elasticsearch.search.aggregations.CardinalityUpperBound; +import org.elasticsearch.search.aggregations.bucket.terms.IncludeExclude; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.CoreValuesSourceType; import org.elasticsearch.search.aggregations.support.MultiValuesSourceFieldConfig; @@ -27,6 +29,8 @@ import java.util.List; import java.util.Map; +import static org.elasticsearch.core.Tuple.tuple; + /** * Factory for frequent items aggregation * @@ -60,7 +64,7 @@ public class FrequentItemSetsAggregatorFactory extends AggregatorFactory { private final double minimumSupport; private final int minimumSetSize; private final int size; - private final QueryBuilder filter; + private final QueryBuilder documentFilter; public FrequentItemSetsAggregatorFactory( String name, @@ -72,32 +76,35 @@ public FrequentItemSetsAggregatorFactory( double minimumSupport, int minimumSetSize, int size, - QueryBuilder filter + QueryBuilder documentFilter ) throws IOException { super(name, context, parent, subFactoriesBuilder, metadata); this.fields = fields; this.minimumSupport = minimumSupport; this.minimumSetSize = minimumSetSize; this.size = size; - this.filter = filter; + this.documentFilter = documentFilter; } @Override protected Aggregator createInternal(Aggregator parent, CardinalityUpperBound cardinality, Map metadata) throws IOException { - List configs = new ArrayList<>(fields.size()); + List> configsAndFilters = new ArrayList<>(fields.size()); for (MultiValuesSourceFieldConfig field : fields) { - configs.add( - ValuesSourceConfig.resolve( - context, - field.getUserValueTypeHint(), - field.getFieldName(), - field.getScript(), - field.getMissing(), - field.getTimeZone(), - field.getFormat(), - CoreValuesSourceType.KEYWORD + configsAndFilters.add( + tuple( + ValuesSourceConfig.resolve( + context, + field.getUserValueTypeHint(), + field.getFieldName(), + field.getScript(), + field.getMissing(), + field.getTimeZone(), + field.getFormat(), + CoreValuesSourceType.KEYWORD + ), + field.getIncludeExclude() ) ); } @@ -113,8 +120,8 @@ protected Aggregator createInternal(Aggregator parent, CardinalityUpperBound car parent, metadata, new EclatMapReducer(FrequentItemSetsAggregationBuilder.NAME, minimumSupport, minimumSetSize, size, context.profiling()), - configs, - filter + configsAndFilters, + documentFilter ) { }; } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/mr/ItemSetMapReduceAggregator.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/mr/ItemSetMapReduceAggregator.java index d54a910c797d..dbe29b648171 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/mr/ItemSetMapReduceAggregator.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/mr/ItemSetMapReduceAggregator.java @@ -17,6 +17,7 @@ import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.LongObjectPagedHashMap; import org.elasticsearch.core.Releasables; +import org.elasticsearch.core.Tuple; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.search.aggregations.AggregationExecutionContext; import org.elasticsearch.search.aggregations.Aggregator; @@ -26,6 +27,7 @@ import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; +import org.elasticsearch.search.aggregations.bucket.terms.IncludeExclude; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; import org.elasticsearch.search.aggregations.support.ValuesSourceRegistry; @@ -47,7 +49,7 @@ public abstract class ItemSetMapReduceAggregator< Result extends ToXContent & Writeable> extends AggregatorBase { private final List extractors; - private final Weight weightFilter; + private final Weight weightDocumentFilter; private final List fields; private final AbstractItemSetMapReducer mapReducer; private final BigArrays bigArraysForMapReduce; @@ -62,8 +64,8 @@ protected ItemSetMapReduceAggregator( Aggregator parent, Map metadata, AbstractItemSetMapReducer mapReducer, - List configs, - QueryBuilder filter + List> configsAndValueFilters, + QueryBuilder documentFilter ) throws IOException { super(name, AggregatorFactories.EMPTY, context, parent, CardinalityUpperBound.NONE, metadata); @@ -72,12 +74,14 @@ protected ItemSetMapReduceAggregator( IndexSearcher contextSearcher = context.searcher(); int id = 0; - this.weightFilter = filter != null - ? contextSearcher.createWeight(contextSearcher.rewrite(context.buildQuery(filter)), ScoreMode.COMPLETE_NO_SCORES, 1f) + this.weightDocumentFilter = documentFilter != null + ? contextSearcher.createWeight(contextSearcher.rewrite(context.buildQuery(documentFilter)), ScoreMode.COMPLETE_NO_SCORES, 1f) : null; - for (ValuesSourceConfig c : configs) { - ItemSetMapReduceValueSource e = context.getValuesSourceRegistry().getAggregator(registryKey, c).build(c, id++); + for (var c : configsAndValueFilters) { + ItemSetMapReduceValueSource e = context.getValuesSourceRegistry() + .getAggregator(registryKey, c.v1()) + .build(c.v1(), id++, c.v2()); if (e.getField().getName() != null) { fields.add(e.getField()); extractors.add(e); @@ -115,10 +119,10 @@ public final InternalAggregation[] buildAggregations(long[] owningBucketOrds) th @Override protected LeafBucketCollector getLeafCollector(AggregationExecutionContext ctx, LeafBucketCollector sub) throws IOException { - final Bits bits = weightFilter != null + final Bits bits = weightDocumentFilter != null ? Lucene.asSequentialAccessBits( ctx.getLeafReaderContext().reader().maxDoc(), - weightFilter.scorerSupplier(ctx.getLeafReaderContext()) + weightDocumentFilter.scorerSupplier(ctx.getLeafReaderContext()) ) : null; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/mr/ItemSetMapReduceValueSource.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/mr/ItemSetMapReduceValueSource.java index e298388514e7..6af981f7ed13 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/mr/ItemSetMapReduceValueSource.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/mr/ItemSetMapReduceValueSource.java @@ -17,6 +17,7 @@ import org.elasticsearch.core.Tuple; import org.elasticsearch.index.fielddata.SortedBinaryDocValues; import org.elasticsearch.search.DocValueFormat; +import org.elasticsearch.search.aggregations.bucket.terms.IncludeExclude; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSource.Bytes; import org.elasticsearch.search.aggregations.support.ValuesSource.Numeric; @@ -35,7 +36,7 @@ public abstract class ItemSetMapReduceValueSource { @FunctionalInterface public interface ValueSourceSupplier { - ItemSetMapReduceValueSource build(ValuesSourceConfig config, int id); + ItemSetMapReduceValueSource build(ValuesSourceConfig config, int id, IncludeExclude includeExclude); } enum ValueFormatter { @@ -120,7 +121,7 @@ public int hashCode() { return Objects.hash(id, valueFormatter, name, format); } - }; + } private final Field field; @@ -128,7 +129,6 @@ public int hashCode() { ItemSetMapReduceValueSource(ValuesSourceConfig config, int id, ValueFormatter valueFormatter) { String fieldName = config.fieldContext() != null ? config.fieldContext().field() : null; - if (Strings.isNullOrEmpty(fieldName)) { throw new IllegalArgumentException("scripts are not supported"); } @@ -142,10 +142,12 @@ Field getField() { public static class KeywordValueSource extends ItemSetMapReduceValueSource { private final ValuesSource.Bytes source; + private final IncludeExclude.StringFilter stringFilter; - public KeywordValueSource(ValuesSourceConfig config, int id) { + public KeywordValueSource(ValuesSourceConfig config, int id, IncludeExclude includeExclude) { super(config, id, ValueFormatter.BYTES_REF); this.source = (Bytes) config.getValuesSource(); + this.stringFilter = includeExclude == null ? null : includeExclude.convertToStringFilter(config.format()); } @Override @@ -157,20 +159,26 @@ public Tuple> collect(LeafReaderContext ctx, int doc) throws List objects = new ArrayList<>(valuesCount); for (int i = 0; i < valuesCount; ++i) { - objects.add(BytesRef.deepCopyOf(values.nextValue())); + BytesRef v = values.nextValue(); + if (stringFilter == null || stringFilter.accept(v)) { + objects.add(BytesRef.deepCopyOf(v)); + } } return new Tuple<>(getField(), objects); } return new Tuple<>(getField(), Collections.emptyList()); } + } public static class NumericValueSource extends ItemSetMapReduceValueSource { private final ValuesSource.Numeric source; + private final IncludeExclude.LongFilter longFilter; - public NumericValueSource(ValuesSourceConfig config, int id) { + public NumericValueSource(ValuesSourceConfig config, int id, IncludeExclude includeExclude) { super(config, id, ValueFormatter.LONG); this.source = (Numeric) config.getValuesSource(); + this.longFilter = includeExclude == null ? null : includeExclude.convertToLongFilter(config.format()); } @Override @@ -182,7 +190,10 @@ public Tuple> collect(LeafReaderContext ctx, int doc) throws List objects = new ArrayList<>(valuesCount); for (int i = 0; i < valuesCount; ++i) { - objects.add(values.nextValue()); + long v = values.nextValue(); + if (longFilter == null || longFilter.accept(v)) { + objects.add(v); + } } return new Tuple<>(getField(), objects); } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregationBuilderTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregationBuilderTests.java index 61b60bcea8cd..e58d14ab378c 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregationBuilderTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregationBuilderTests.java @@ -15,6 +15,7 @@ import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.aggregations.BaseAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.terms.IncludeExclude; import org.elasticsearch.search.aggregations.support.MultiValuesSourceFieldConfig; import org.elasticsearch.test.AbstractXContentSerializingTestCase; import org.elasticsearch.xcontent.NamedXContentRegistry; @@ -27,6 +28,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.TreeSet; import java.util.stream.Collectors; import static org.hamcrest.Matchers.hasSize; @@ -47,6 +49,10 @@ public static FrequentItemSetsAggregationBuilder randomFrequentItemsSetsAggregat field.setMissing(randomAlphaOfLength(5)); } + if (randomBoolean()) { + field.setIncludeExclude(randomIncludeExclude()); + } + return field.build(); }).collect(Collectors.toList()); @@ -186,4 +192,29 @@ public void testValidation() { assertEquals("Aggregator [fi] of type [frequent_items] cannot accept sub-aggregations", e.getMessage()); } + private static IncludeExclude randomIncludeExclude() { + switch (randomInt(7)) { + case 0: + return new IncludeExclude("incl*de", null, null, null); + case 1: + return new IncludeExclude("incl*de", "excl*de", null, null); + case 2: + return new IncludeExclude("incl*de", null, null, new TreeSet<>(Set.of(newBytesRef("exclude")))); + case 3: + return new IncludeExclude(null, "excl*de", null, null); + case 4: + return new IncludeExclude(null, "excl*de", new TreeSet<>(Set.of(newBytesRef("include"))), null); + case 5: + return new IncludeExclude(null, null, new TreeSet<>(Set.of(newBytesRef("include"))), null); + case 6: + return new IncludeExclude( + null, + null, + new TreeSet<>(Set.of(newBytesRef("include"))), + new TreeSet<>(Set.of(newBytesRef("exclude"))) + ); + default: + return new IncludeExclude(null, null, null, new TreeSet<>(Set.of(newBytesRef("exclude")))); + } + } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregatorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregatorTests.java index baa4199032a6..bd45eb895c14 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregatorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregatorTests.java @@ -19,6 +19,7 @@ import org.elasticsearch.common.network.InetAddresses; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.time.DateFormatter; +import org.elasticsearch.core.Tuple; import org.elasticsearch.index.mapper.DateFieldMapper; import org.elasticsearch.index.mapper.IpFieldMapper; import org.elasticsearch.index.mapper.KeywordFieldMapper; @@ -27,6 +28,7 @@ import org.elasticsearch.plugins.SearchPlugin; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregatorTestCase; +import org.elasticsearch.search.aggregations.bucket.terms.IncludeExclude; import org.elasticsearch.search.aggregations.support.CoreValuesSourceType; import org.elasticsearch.search.aggregations.support.MultiValuesSourceFieldConfig; import org.elasticsearch.search.aggregations.support.ValuesSourceType; @@ -39,11 +41,16 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; import java.util.stream.Collectors; +import static org.elasticsearch.core.Tuple.tuple; import static org.hamcrest.Matchers.containsInAnyOrder; public class FrequentItemSetsAggregatorTests extends AggregatorTestCase { @@ -87,7 +94,14 @@ protected AggregationBuilder createAggBuilderForTypeTest(MappedFieldType fieldTy public void testKeywordsArray() throws IOException { List fields = new ArrayList<>(); - fields.add(new MultiValuesSourceFieldConfig.Builder().setFieldName(KEYWORD_FIELD1).build()); + String exclude = randomBoolean() ? randomFrom("item-3", "item-4", "item-5", "item-99") : null; + fields.add( + new MultiValuesSourceFieldConfig.Builder().setFieldName(KEYWORD_FIELD1) + .setIncludeExclude( + exclude != null ? new IncludeExclude(null, null, null, new TreeSet<>(Set.of(new BytesRef(exclude)))) : null + ) + .build() + ); double minimumSupport = randomDoubleBetween(0.13, 0.41, true); int minimumSetSize = randomIntBetween(2, 5); @@ -205,19 +219,62 @@ public void testKeywordsArray() throws IOException { ); }, (InternalItemSetMapReduceAggregation results) -> { assertNotNull(results); - assertResults(expectedResults, results.getMapReduceResult().getFrequentItemSets(), minimumSupport, minimumSetSize, size); + assertResults( + expectedResults, + results.getMapReduceResult().getFrequentItemSets(), + minimumSupport, + minimumSetSize, + size, + exclude, + null + ); }, new AggTestConfig(builder, keywordType).withQuery(query)); } public void testMixedSingleValues() throws IOException { List fields = new ArrayList<>(); - fields.add(new MultiValuesSourceFieldConfig.Builder().setFieldName(KEYWORD_FIELD1).build()); - fields.add(new MultiValuesSourceFieldConfig.Builder().setFieldName(KEYWORD_FIELD2).build()); - fields.add(new MultiValuesSourceFieldConfig.Builder().setFieldName(KEYWORD_FIELD3).build()); + String stringExclude = randomBoolean() ? randomFrom("host-2", "192.168.0.1", "client-2", "127.0.0.1") : null; + Integer intExclude = randomBoolean() ? randomIntBetween(0, 10) : null; + + fields.add( + new MultiValuesSourceFieldConfig.Builder().setFieldName(KEYWORD_FIELD1) + .setIncludeExclude( + stringExclude != null ? new IncludeExclude(null, null, null, new TreeSet<>(Set.of(new BytesRef(stringExclude)))) : null + ) + .build() + ); + fields.add( + new MultiValuesSourceFieldConfig.Builder().setFieldName(KEYWORD_FIELD2) + .setIncludeExclude( + stringExclude != null ? new IncludeExclude(null, null, null, new TreeSet<>(Set.of(new BytesRef(stringExclude)))) : null + ) + .build() + ); + fields.add( + new MultiValuesSourceFieldConfig.Builder().setFieldName(KEYWORD_FIELD3) + .setIncludeExclude( + stringExclude != null ? new IncludeExclude(null, null, null, new TreeSet<>(Set.of(new BytesRef(stringExclude)))) : null + ) + .build() + ); fields.add(new MultiValuesSourceFieldConfig.Builder().setFieldName(FLOAT_FIELD).build()); - fields.add(new MultiValuesSourceFieldConfig.Builder().setFieldName(INT_FIELD).build()); - fields.add(new MultiValuesSourceFieldConfig.Builder().setFieldName(IP_FIELD).build()); + fields.add( + new MultiValuesSourceFieldConfig.Builder().setFieldName(INT_FIELD) + .setIncludeExclude( + intExclude != null + ? new IncludeExclude(null, null, null, new TreeSet<>(Set.of(new BytesRef(String.valueOf(intExclude))))) + : null + ) + .build() + ); + fields.add( + new MultiValuesSourceFieldConfig.Builder().setFieldName(IP_FIELD) + .setIncludeExclude( + stringExclude != null ? new IncludeExclude(null, null, null, new TreeSet<>(Set.of(new BytesRef(stringExclude)))) : null + ) + .build() + ); double minimumSupport = randomDoubleBetween(0.13, 0.51, true); int minimumSetSize = randomIntBetween(2, 6); @@ -382,7 +439,15 @@ public void testMixedSingleValues() throws IOException { ); }, (InternalItemSetMapReduceAggregation results) -> { assertNotNull(results); - assertResults(expectedResults, results.getMapReduceResult().getFrequentItemSets(), minimumSupport, minimumSetSize, size); + assertResults( + expectedResults, + results.getMapReduceResult().getFrequentItemSets(), + minimumSupport, + minimumSetSize, + size, + stringExclude, + intExclude + ); }, new AggTestConfig(builder, keywordType1, keywordType2, keywordType3, intType, floatType, ipType).withQuery(query)); } @@ -390,10 +455,18 @@ public void testMixedSingleValues() throws IOException { public void testSingleValueWithDate() throws IOException { List fields = new ArrayList<>(); + String dateExclude = randomBoolean() ? randomFrom("2022-06-02", "2022-06-03", "1970-01-01") : null; + fields.add(new MultiValuesSourceFieldConfig.Builder().setFieldName(KEYWORD_FIELD1).build()); fields.add(new MultiValuesSourceFieldConfig.Builder().setFieldName(KEYWORD_FIELD2).build()); fields.add(new MultiValuesSourceFieldConfig.Builder().setFieldName(KEYWORD_FIELD3).build()); - fields.add(new MultiValuesSourceFieldConfig.Builder().setFieldName(DATE_FIELD).build()); + fields.add( + new MultiValuesSourceFieldConfig.Builder().setFieldName(DATE_FIELD) + .setIncludeExclude( + dateExclude != null ? new IncludeExclude(null, null, null, new TreeSet<>(Set.of(new BytesRef(dateExclude)))) : null + ) + .build() + ); double minimumSupport = randomDoubleBetween(0.13, 0.51, true); int minimumSetSize = randomIntBetween(2, 6); @@ -571,7 +644,15 @@ public void testSingleValueWithDate() throws IOException { ); }, (InternalItemSetMapReduceAggregation results) -> { assertNotNull(results); - assertResults(expectedResults, results.getMapReduceResult().getFrequentItemSets(), minimumSupport, minimumSetSize, size); + assertResults( + expectedResults, + results.getMapReduceResult().getFrequentItemSets(), + minimumSupport, + minimumSetSize, + size, + dateExclude, + null + ); }, new AggTestConfig(builder, keywordType1, keywordType2, keywordType3, dateType, ipType).withQuery(query)); } @@ -594,11 +675,45 @@ private DateFieldMapper.DateFieldType dateFieldType(String name) { ); } - private void assertResults(List expected, FrequentItemSet[] actual, double minSupport, int minimumSetSize, int size) { + private void assertResults( + List expected, + FrequentItemSet[] actual, + double minSupport, + int minimumSetSize, + int size, + String stringExclude, + Integer intExclude + ) { // sort the expected results descending by doc count expected.get(0).getFields().values().stream().mapToLong(v -> v.stream().count()).sum(); - List filteredExpected = expected.stream() + List filteredExpectedWithDups = expected.stream() + .map( + fi -> new FrequentItemSet( + fi.getFields() + .entrySet() + .stream() + .map( + // filter the string exclude from the list of objects + keyValues -> tuple( + keyValues.getKey(), + keyValues.getValue().stream().filter(v -> v.equals(stringExclude) == false).collect(Collectors.toList()) + ) + ) + .map( + // filter the int exclude + keyValues -> tuple( + keyValues.v1(), + keyValues.v2().stream().filter(v -> v.equals(intExclude) == false).collect(Collectors.toList()) + ) + ) + // after filtering out excludes the list of objects might be empty + .filter(t -> t.v2().size() > 0) + .collect(Collectors.toMap(Tuple::v1, Tuple::v2)), + fi.getDocCount(), + fi.getSupport() + ) + ) .filter(fi -> fi.getSupport() >= minSupport) .filter( fi -> { @@ -638,6 +753,17 @@ private void assertResults(List expected, FrequentItemSet[] act }) .collect(Collectors.toList()); + // after removing excluded items there might be duplicate entries, which need to be collapsed, we do this by very simple hashing + List filteredExpected = new ArrayList<>(); + Set valuesSeen = new HashSet<>(); + for (FrequentItemSet fi : filteredExpectedWithDups) { + if (valuesSeen.add( + fi.getFields().entrySet().stream().mapToInt(v -> Objects.hash(v.getKey(), v.getValue())).reduce(13, (t, s) -> 41 * t + s) + )) { + filteredExpected.add(fi); + } + } + // if size applies, cut the list, however if sets have the same number of items it's unclear which ones are returned int additionalSetsThatShareTheSameDocCount = 0; if (size < filteredExpected.size()) { diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/frequent_items_agg.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/frequent_items_agg.yml index 72d7a08de684..39661d4917d0 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/frequent_items_agg.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/frequent_items_agg.yml @@ -368,6 +368,68 @@ setup: - match: { aggregations.fi.buckets.0.support: 0.4 } - match: { aggregations.fi.buckets.0.key.error_message: ["compressor low pressure"] } +--- +"Test frequent items exclude": + + - do: + search: + index: store + body: > + { + "size": 0, + "aggs": { + "fi": { + "frequent_items": { + "minimum_set_size": 3, + "minimum_support": 0.3, + "fields": [ + {"field": "features"}, + { + "field": "error_message", + "exclude": "engine overheated" + } + ] + } + } + } + } + - length: { aggregations.fi.buckets: 3 } + - match: { aggregations.fi.buckets.0.doc_count: 5 } + - match: { aggregations.fi.buckets.0.support: 0.5 } + - match: { aggregations.fi.buckets.0.key.error_message: ["compressor low pressure"] } + - match: { aggregations.fi.buckets.1.doc_count: 3 } + - match: { aggregations.fi.buckets.1.support: 0.3 } + +--- +"Test frequent items include": + + - do: + search: + index: store + body: > + { + "size": 0, + "aggs": { + "fi": { + "frequent_items": { + "minimum_set_size": 3, + "minimum_support": 0.3, + "fields": [ + {"field": "features"}, + { + "field": "error_message", + "include": "en.*ed" + } + ] + } + } + } + } + - length: { aggregations.fi.buckets: 3 } + - match: { aggregations.fi.buckets.0.doc_count: 4 } + - match: { aggregations.fi.buckets.0.support: 0.4 } + - match: { aggregations.fi.buckets.0.key.error_message: ["engine overheated"] } + --- "Test frequent items unsupported types": - do: From c546a98e2804e4d2c2b2a17ea41f3ba0614834f1 Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Wed, 21 Dec 2022 13:14:27 +0100 Subject: [PATCH 327/919] Make RecoveryPlannerService optional (#92489) This commit contains changes related to recovery planners: - it changes the RecoveryPlannerPlugin to allow more than one plugin to be installed at a time - it makes the instantiation of the RecoveryPlannerService optional, so that plugins can decide to create or not a recovery service but still checks that only 1 recovery planner service is present at a time - it makes the SnapshotBasedRecoveriesPlugin returns an empty service when stateless is enabled - it adjust the computeRecoveryPlan() method to add a boolean parameter indicating if the plan is computed for a primary relocation --- docs/changelog/92489.yaml | 5 +++ .../recovery/RecoverySourceHandler.java | 1 + .../plan/PeerOnlyRecoveryPlannerService.java | 1 + .../recovery/plan/RecoveryPlannerService.java | 1 + .../java/org/elasticsearch/node/Node.java | 28 ++++++++--------- .../plugins/RecoveryPlannerPlugin.java | 8 +++-- .../org/elasticsearch/node/NodeTests.java | 5 +-- .../plugins/PluginIntrospectorTests.java | 4 +-- .../BaseSearchableSnapshotsIntegTestCase.java | 5 +++ ...ableMockSnapshotBasedRecoveriesPlugin.java | 5 ++- .../SnapshotBasedRecoveriesPlugin.java | 18 +++++++++-- .../plan/SnapshotsRecoveryPlannerService.java | 1 + .../SnapshotsRecoveryPlannerServiceTests.java | 31 +++++++++++++------ 13 files changed, 78 insertions(+), 35 deletions(-) create mode 100644 docs/changelog/92489.yaml diff --git a/docs/changelog/92489.yaml b/docs/changelog/92489.yaml new file mode 100644 index 000000000000..e2f3c4cd19e1 --- /dev/null +++ b/docs/changelog/92489.yaml @@ -0,0 +1,5 @@ +pr: 92489 +summary: Make `RecoveryPlannerService` optional +area: Snapshot/Restore +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java b/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java index 02e4d13656a9..e3130fc597ff 100644 --- a/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java +++ b/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java @@ -574,6 +574,7 @@ void phase1(IndexCommit snapshot, long startingSeqNo, IntSupplier translogOps, A translogOps.getAsInt(), getRequest().targetNode().getVersion(), canUseSnapshots, + request.isPrimaryRelocation(), ActionListener.wrap(plan -> recoverFilesFromSourceAndSnapshot(plan, store, stopWatch, listener), listener::onFailure) ); } else { diff --git a/server/src/main/java/org/elasticsearch/indices/recovery/plan/PeerOnlyRecoveryPlannerService.java b/server/src/main/java/org/elasticsearch/indices/recovery/plan/PeerOnlyRecoveryPlannerService.java index 9bff81f5b4ff..f24c612ebcc8 100644 --- a/server/src/main/java/org/elasticsearch/indices/recovery/plan/PeerOnlyRecoveryPlannerService.java +++ b/server/src/main/java/org/elasticsearch/indices/recovery/plan/PeerOnlyRecoveryPlannerService.java @@ -35,6 +35,7 @@ public void computeRecoveryPlan( int translogOps, Version targetVersion, boolean useSnapshots, + boolean primaryRelocation, ActionListener listener ) { ActionListener.completeWith(listener, () -> { diff --git a/server/src/main/java/org/elasticsearch/indices/recovery/plan/RecoveryPlannerService.java b/server/src/main/java/org/elasticsearch/indices/recovery/plan/RecoveryPlannerService.java index 3318e08b3d7a..25f9a143b053 100644 --- a/server/src/main/java/org/elasticsearch/indices/recovery/plan/RecoveryPlannerService.java +++ b/server/src/main/java/org/elasticsearch/indices/recovery/plan/RecoveryPlannerService.java @@ -23,6 +23,7 @@ void computeRecoveryPlan( int translogOps, Version targetVersion, boolean useSnapshots, + boolean primaryRelocation, ActionListener listener ); } diff --git a/server/src/main/java/org/elasticsearch/node/Node.java b/server/src/main/java/org/elasticsearch/node/Node.java index 8b6bb425b28a..adc90fc04716 100644 --- a/server/src/main/java/org/elasticsearch/node/Node.java +++ b/server/src/main/java/org/elasticsearch/node/Node.java @@ -1234,22 +1234,22 @@ private RecoveryPlannerService getRecoveryPlannerService( ClusterService clusterService, RepositoriesService repositoryService ) { - final List recoveryPlannerPlugins = pluginsService.filterPlugins(RecoveryPlannerPlugin.class); - if (recoveryPlannerPlugins.isEmpty()) { + final List recoveryPlannerServices = pluginsService.filterPlugins(RecoveryPlannerPlugin.class) + .stream() + .map( + plugin -> plugin.createRecoveryPlannerService( + new ShardSnapshotsService(client, repositoryService, threadPool, clusterService) + ) + ) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + if (recoveryPlannerServices.isEmpty()) { return new PeerOnlyRecoveryPlannerService(); + } else if (recoveryPlannerServices.size() > 1) { + throw new IllegalStateException("Expected a single RecoveryPlannerService but got: " + recoveryPlannerServices.size()); } - - if (recoveryPlannerPlugins.size() > 1) { - throw new IllegalStateException("A single RecoveryPlannerPlugin was expected but got: " + recoveryPlannerPlugins); - } - - final ShardSnapshotsService shardSnapshotsService = new ShardSnapshotsService( - client, - repositoryService, - threadPool, - clusterService - ); - return recoveryPlannerPlugins.get(0).createRecoveryPlannerService(shardSnapshotsService); + return recoveryPlannerServices.get(0); } private WriteLoadForecaster getWriteLoadForecaster(ThreadPool threadPool, Settings settings, ClusterSettings clusterSettings) { diff --git a/server/src/main/java/org/elasticsearch/plugins/RecoveryPlannerPlugin.java b/server/src/main/java/org/elasticsearch/plugins/RecoveryPlannerPlugin.java index ed0f7a0abcd0..8602d9104dcb 100644 --- a/server/src/main/java/org/elasticsearch/plugins/RecoveryPlannerPlugin.java +++ b/server/src/main/java/org/elasticsearch/plugins/RecoveryPlannerPlugin.java @@ -11,10 +11,12 @@ import org.elasticsearch.indices.recovery.plan.RecoveryPlannerService; import org.elasticsearch.indices.recovery.plan.ShardSnapshotsService; +import java.util.Optional; + /** - * A plugin that allows creating custom {@code RecoveryPlannerService}. Only one plugin of this type - * is allowed to be installed at once. + * A plugin that allows creating custom {@code RecoveryPlannerService}. Only one {@code RecoveryPlannerService} is allowed to be active + * at once. */ public interface RecoveryPlannerPlugin { - RecoveryPlannerService createRecoveryPlannerService(ShardSnapshotsService shardSnapshotsService); + Optional createRecoveryPlannerService(ShardSnapshotsService shardSnapshotsService); } diff --git a/server/src/test/java/org/elasticsearch/node/NodeTests.java b/server/src/test/java/org/elasticsearch/node/NodeTests.java index 0982442dc0d8..645cdf077596 100644 --- a/server/src/test/java/org/elasticsearch/node/NodeTests.java +++ b/server/src/test/java/org/elasticsearch/node/NodeTests.java @@ -55,6 +55,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; @@ -370,8 +371,8 @@ public static class MockRecoveryPlannerPlugin extends Plugin implements Recovery public MockRecoveryPlannerPlugin() {} @Override - public RecoveryPlannerService createRecoveryPlannerService(ShardSnapshotsService shardSnapshotsService) { - return mock(RecoveryPlannerService.class); + public Optional createRecoveryPlannerService(ShardSnapshotsService shardSnapshotsService) { + return Optional.of(mock(RecoveryPlannerService.class)); } } diff --git a/server/src/test/java/org/elasticsearch/plugins/PluginIntrospectorTests.java b/server/src/test/java/org/elasticsearch/plugins/PluginIntrospectorTests.java index c20fdeb98b18..a65b84a46822 100644 --- a/server/src/test/java/org/elasticsearch/plugins/PluginIntrospectorTests.java +++ b/server/src/test/java/org/elasticsearch/plugins/PluginIntrospectorTests.java @@ -151,8 +151,8 @@ public Map getDirectoryFactories() { } @Override - public RecoveryPlannerService createRecoveryPlannerService(ShardSnapshotsService shardSnapshotsService) { - return null; + public Optional createRecoveryPlannerService(ShardSnapshotsService shardSnapshotsService) { + return Optional.empty(); } @Override diff --git a/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/BaseSearchableSnapshotsIntegTestCase.java b/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/BaseSearchableSnapshotsIntegTestCase.java index 154a7ba5e798..51ad152af178 100644 --- a/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/BaseSearchableSnapshotsIntegTestCase.java +++ b/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/BaseSearchableSnapshotsIntegTestCase.java @@ -351,6 +351,11 @@ protected void assertExecutorIsIdle(String executorName) throws Exception { } public static class LicensedSnapshotBasedRecoveriesPlugin extends SnapshotBasedRecoveriesPlugin { + + public LicensedSnapshotBasedRecoveriesPlugin(Settings settings) { + super(settings); + } + @Override public boolean isLicenseEnabled() { return true; diff --git a/x-pack/plugin/snapshot-based-recoveries/src/internalClusterTest/java/org/elasticsearch/xpack/snapshotbasedrecoveries/recovery/ConfigurableMockSnapshotBasedRecoveriesPlugin.java b/x-pack/plugin/snapshot-based-recoveries/src/internalClusterTest/java/org/elasticsearch/xpack/snapshotbasedrecoveries/recovery/ConfigurableMockSnapshotBasedRecoveriesPlugin.java index 9320fa633c0d..2d96416b8164 100644 --- a/x-pack/plugin/snapshot-based-recoveries/src/internalClusterTest/java/org/elasticsearch/xpack/snapshotbasedrecoveries/recovery/ConfigurableMockSnapshotBasedRecoveriesPlugin.java +++ b/x-pack/plugin/snapshot-based-recoveries/src/internalClusterTest/java/org/elasticsearch/xpack/snapshotbasedrecoveries/recovery/ConfigurableMockSnapshotBasedRecoveriesPlugin.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.snapshotbasedrecoveries.recovery; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.CheckedRunnable; import org.elasticsearch.xpack.snapshotbasedrecoveries.SnapshotBasedRecoveriesPlugin; @@ -15,7 +16,9 @@ public class ConfigurableMockSnapshotBasedRecoveriesPlugin extends SnapshotBasedRecoveriesPlugin { private static final AtomicBoolean recoveryFromSnapshotAllowed = new AtomicBoolean(true); - public ConfigurableMockSnapshotBasedRecoveriesPlugin() {} + public ConfigurableMockSnapshotBasedRecoveriesPlugin(Settings settings) { + super(settings); + } @Override public boolean isLicenseEnabled() { diff --git a/x-pack/plugin/snapshot-based-recoveries/src/main/java/org/elasticsearch/xpack/snapshotbasedrecoveries/SnapshotBasedRecoveriesPlugin.java b/x-pack/plugin/snapshot-based-recoveries/src/main/java/org/elasticsearch/xpack/snapshotbasedrecoveries/SnapshotBasedRecoveriesPlugin.java index 8e2c167992d9..fdc4dad7a6e3 100644 --- a/x-pack/plugin/snapshot-based-recoveries/src/main/java/org/elasticsearch/xpack/snapshotbasedrecoveries/SnapshotBasedRecoveriesPlugin.java +++ b/x-pack/plugin/snapshot-based-recoveries/src/main/java/org/elasticsearch/xpack/snapshotbasedrecoveries/SnapshotBasedRecoveriesPlugin.java @@ -7,6 +7,8 @@ package org.elasticsearch.xpack.snapshotbasedrecoveries; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.indices.recovery.plan.RecoveryPlannerService; import org.elasticsearch.indices.recovery.plan.ShardSnapshotsService; import org.elasticsearch.license.License; @@ -16,18 +18,28 @@ import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.snapshotbasedrecoveries.recovery.plan.SnapshotsRecoveryPlannerService; +import java.util.Optional; + public class SnapshotBasedRecoveriesPlugin extends Plugin implements RecoveryPlannerPlugin { + public static final LicensedFeature.Momentary SNAPSHOT_BASED_RECOVERIES_FEATURE = LicensedFeature.momentary( null, "snapshot-based-recoveries", License.OperationMode.ENTERPRISE ); - public SnapshotBasedRecoveriesPlugin() {} + private final Settings settings; + + public SnapshotBasedRecoveriesPlugin(Settings settings) { + this.settings = settings; + } @Override - public RecoveryPlannerService createRecoveryPlannerService(ShardSnapshotsService shardSnapshotsService) { - return new SnapshotsRecoveryPlannerService(shardSnapshotsService, this::isLicenseEnabled); + public Optional createRecoveryPlannerService(ShardSnapshotsService shardSnapshotsService) { + if (DiscoveryNode.isStateless(settings)) { + return Optional.empty(); + } + return Optional.of(new SnapshotsRecoveryPlannerService(shardSnapshotsService, this::isLicenseEnabled)); } // Overridable for tests diff --git a/x-pack/plugin/snapshot-based-recoveries/src/main/java/org/elasticsearch/xpack/snapshotbasedrecoveries/recovery/plan/SnapshotsRecoveryPlannerService.java b/x-pack/plugin/snapshot-based-recoveries/src/main/java/org/elasticsearch/xpack/snapshotbasedrecoveries/recovery/plan/SnapshotsRecoveryPlannerService.java index 61facb238bc9..4609a7256b67 100644 --- a/x-pack/plugin/snapshot-based-recoveries/src/main/java/org/elasticsearch/xpack/snapshotbasedrecoveries/recovery/plan/SnapshotsRecoveryPlannerService.java +++ b/x-pack/plugin/snapshot-based-recoveries/src/main/java/org/elasticsearch/xpack/snapshotbasedrecoveries/recovery/plan/SnapshotsRecoveryPlannerService.java @@ -54,6 +54,7 @@ public void computeRecoveryPlan( int translogOps, Version targetVersion, boolean useSnapshots, + boolean primaryRelocation, ActionListener listener ) { // Fallback to source only recovery if the target node is in an incompatible version diff --git a/x-pack/plugin/snapshot-based-recoveries/src/test/java/org/elasticsearch/xpack/snapshotbasedrecoveries/recovery/plan/SnapshotsRecoveryPlannerServiceTests.java b/x-pack/plugin/snapshot-based-recoveries/src/test/java/org/elasticsearch/xpack/snapshotbasedrecoveries/recovery/plan/SnapshotsRecoveryPlannerServiceTests.java index d0a8387a893d..3870cfb2804e 100644 --- a/x-pack/plugin/snapshot-based-recoveries/src/test/java/org/elasticsearch/xpack/snapshotbasedrecoveries/recovery/plan/SnapshotsRecoveryPlannerServiceTests.java +++ b/x-pack/plugin/snapshot-based-recoveries/src/test/java/org/elasticsearch/xpack/snapshotbasedrecoveries/recovery/plan/SnapshotsRecoveryPlannerServiceTests.java @@ -105,7 +105,8 @@ public void fetchLatestSnapshotsForShard(ShardId shardId, ActionListener true); @@ -460,6 +470,7 @@ private ShardRecoveryPlan computeShardRecoveryPlan( translogOps, version, snapshotRecoveriesEnabled, + primaryRelocation, planFuture ); final ShardRecoveryPlan shardRecoveryPlan = planFuture.get(); From b588b5f1597fdc811be5d292e631a9f320bddc57 Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Wed, 21 Dec 2022 14:25:22 +0100 Subject: [PATCH 328/919] DLS role query validator to validate query while parsing (#92471) DLSRoleQueryValidator validates the role query by parsing it and then recursively visiting all the branches of the query tree. Instead, validation can happen directly while parsing. This way there is no recursion needed to visit the query tree after parsing, and all compound queries are supported out-of-the-box given that each inner query needs to be looked up in the named xcontent registry. --- .../authz/support/DLSRoleQueryValidator.java | 87 +++++++------------ .../support/DLSRoleQueryValidatorTests.java | 57 +++++++++--- 2 files changed, 79 insertions(+), 65 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/support/DLSRoleQueryValidator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/support/DLSRoleQueryValidator.java index 21954e867c0a..7b2e7e1e854b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/support/DLSRoleQueryValidator.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/support/DLSRoleQueryValidator.java @@ -13,13 +13,9 @@ import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.core.Nullable; import org.elasticsearch.index.query.AbstractQueryBuilder; -import org.elasticsearch.index.query.BoolQueryBuilder; -import org.elasticsearch.index.query.BoostingQueryBuilder; -import org.elasticsearch.index.query.ConstantScoreQueryBuilder; import org.elasticsearch.index.query.GeoShapeQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.TermsQueryBuilder; -import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptService; import org.elasticsearch.script.ScriptType; @@ -33,8 +29,7 @@ import org.elasticsearch.xpack.core.security.user.User; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import java.util.Collections; /** * This class helps in evaluating the query field if it is template, @@ -156,10 +151,38 @@ public static QueryBuilder evaluateAndVerifyRoleQuery( @Nullable public static QueryBuilder evaluateAndVerifyRoleQuery(String query, NamedXContentRegistry xContentRegistry) throws IOException { if (query != null) { - try (XContentParser parser = XContentFactory.xContent(query).createParser(parserConfig(xContentRegistry), query)) { - QueryBuilder queryBuilder = AbstractQueryBuilder.parseTopLevelQuery(parser); - verifyRoleQuery(queryBuilder); - return queryBuilder; + NamedXContentRegistry registryWrapper = new NamedXContentRegistry(Collections.emptyList()) { + @Override + public T parseNamedObject(Class categoryClass, String name, XContentParser parser, C context) throws IOException { + T namedObject = xContentRegistry.parseNamedObject(categoryClass, name, parser, context); + if (categoryClass.equals(QueryBuilder.class)) { + if (namedObject instanceof TermsQueryBuilder termsQueryBuilder) { + if (termsQueryBuilder.termsLookup() != null) { + throw new IllegalArgumentException("terms query with terms lookup isn't supported as part of a role query"); + } + } else if (namedObject instanceof GeoShapeQueryBuilder geoShapeQueryBuilder) { + if (geoShapeQueryBuilder.shape() == null) { + throw new IllegalArgumentException( + "geoshape query referring to indexed shapes isn't supported as part of a role query" + ); + } + } + } + return namedObject; + } + }; + try (XContentParser parser = XContentFactory.xContent(query).createParser(parserConfig(registryWrapper), query)) { + return AbstractQueryBuilder.parseTopLevelQuery(parser, queryName -> { + switch (queryName) { + // actually only if percolate query is referring to an existing document then this is problematic, + // a normal percolate query does work. However we can't check that here as this query builder is inside + // another module. So we don't allow the entire percolate query. I don't think users would ever use + // a percolate query as role query, so this restriction shouldn't prohibit anyone from using dls. + case "percolate", "has_child", "has_parent" -> throw new IllegalArgumentException( + queryName + " query isn't supported as part of a role query" + ); + } + }); } } return null; @@ -168,48 +191,4 @@ public static QueryBuilder evaluateAndVerifyRoleQuery(String query, NamedXConten private static XContentParserConfiguration parserConfig(NamedXContentRegistry xContentRegistry) { return XContentParserConfiguration.EMPTY.withRegistry(xContentRegistry).withDeprecationHandler(LoggingDeprecationHandler.INSTANCE); } - - /** - * Checks whether the role query contains queries we know can't be used as DLS role query. - * - * @param queryBuilder {@link QueryBuilder} for given query - */ - // pkg protected for testing - static void verifyRoleQuery(QueryBuilder queryBuilder) { - if (queryBuilder instanceof TermsQueryBuilder termsQueryBuilder) { - if (termsQueryBuilder.termsLookup() != null) { - throw new IllegalArgumentException("terms query with terms lookup isn't supported as part of a role query"); - } - } else if (queryBuilder instanceof GeoShapeQueryBuilder geoShapeQueryBuilder) { - if (geoShapeQueryBuilder.shape() == null) { - throw new IllegalArgumentException("geoshape query referring to indexed shapes isn't supported as part of a role query"); - } - } else if (queryBuilder.getName().equals("percolate")) { - // actually only if percolate query is referring to an existing document then this is problematic, - // a normal percolate query does work. However we can't check that here as this query builder is inside - // another module. So we don't allow the entire percolate query. I don't think users would ever use - // a percolate query as role query, so this restriction shouldn't prohibit anyone from using dls. - throw new IllegalArgumentException("percolate query isn't supported as part of a role query"); - } else if (queryBuilder.getName().equals("has_child")) { - throw new IllegalArgumentException("has_child query isn't supported as part of a role query"); - } else if (queryBuilder.getName().equals("has_parent")) { - throw new IllegalArgumentException("has_parent query isn't supported as part of a role query"); - } else if (queryBuilder instanceof BoolQueryBuilder boolQueryBuilder) { - List clauses = new ArrayList<>(); - clauses.addAll(boolQueryBuilder.filter()); - clauses.addAll(boolQueryBuilder.must()); - clauses.addAll(boolQueryBuilder.mustNot()); - clauses.addAll(boolQueryBuilder.should()); - for (QueryBuilder clause : clauses) { - verifyRoleQuery(clause); - } - } else if (queryBuilder instanceof ConstantScoreQueryBuilder) { - verifyRoleQuery(((ConstantScoreQueryBuilder) queryBuilder).innerQuery()); - } else if (queryBuilder instanceof FunctionScoreQueryBuilder) { - verifyRoleQuery(((FunctionScoreQueryBuilder) queryBuilder).query()); - } else if (queryBuilder instanceof BoostingQueryBuilder) { - verifyRoleQuery(((BoostingQueryBuilder) queryBuilder).negativeQuery()); - verifyRoleQuery(((BoostingQueryBuilder) queryBuilder).positiveQuery()); - } - } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/support/DLSRoleQueryValidatorTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/support/DLSRoleQueryValidatorTests.java index 1ec6847ca0d7..9f8f3703a0e1 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/support/DLSRoleQueryValidatorTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/support/DLSRoleQueryValidatorTests.java @@ -7,7 +7,9 @@ package org.elasticsearch.xpack.core.security.authz.support; import org.apache.lucene.search.join.ScoreMode; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.BoostingQueryBuilder; import org.elasticsearch.index.query.ConstantScoreQueryBuilder; @@ -17,52 +19,86 @@ import org.elasticsearch.index.query.TermsQueryBuilder; import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder; import org.elasticsearch.indices.TermsLookup; +import org.elasticsearch.join.ParentJoinPlugin; import org.elasticsearch.join.query.HasChildQueryBuilder; import org.elasticsearch.join.query.HasParentQueryBuilder; +import org.elasticsearch.search.SearchModule; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xcontent.NamedXContentRegistry; import java.io.IOException; +import java.util.List; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; public class DLSRoleQueryValidatorTests extends ESTestCase { + private static final NamedXContentRegistry XCONTENT_REGISTRY = new NamedXContentRegistry( + new SearchModule(Settings.EMPTY, List.of(new ParentJoinPlugin())).getNamedXContents() + ); + public void testVerifyRoleQuery() throws Exception { QueryBuilder queryBuilder1 = new TermsQueryBuilder("field", "val1", "val2"); - DLSRoleQueryValidator.verifyRoleQuery(queryBuilder1); + DLSRoleQueryValidator.evaluateAndVerifyRoleQuery(Strings.toString(queryBuilder1), XCONTENT_REGISTRY); QueryBuilder queryBuilder2 = new TermsQueryBuilder("field", new TermsLookup("_index", "_id", "_path")); - Exception e = expectThrows(IllegalArgumentException.class, () -> DLSRoleQueryValidator.verifyRoleQuery(queryBuilder2)); + Exception e = expectThrows( + IllegalArgumentException.class, + () -> DLSRoleQueryValidator.evaluateAndVerifyRoleQuery(Strings.toString(queryBuilder2), XCONTENT_REGISTRY) + ); assertThat(e.getMessage(), equalTo("terms query with terms lookup isn't supported as part of a role query")); QueryBuilder queryBuilder3 = new GeoShapeQueryBuilder("field", "_id"); - e = expectThrows(IllegalArgumentException.class, () -> DLSRoleQueryValidator.verifyRoleQuery(queryBuilder3)); + e = expectThrows( + IllegalArgumentException.class, + () -> DLSRoleQueryValidator.evaluateAndVerifyRoleQuery(Strings.toString(queryBuilder3), XCONTENT_REGISTRY) + ); assertThat(e.getMessage(), equalTo("geoshape query referring to indexed shapes isn't supported as part of a role query")); QueryBuilder queryBuilder4 = new HasChildQueryBuilder("_type", new MatchAllQueryBuilder(), ScoreMode.None); - e = expectThrows(IllegalArgumentException.class, () -> DLSRoleQueryValidator.verifyRoleQuery(queryBuilder4)); + e = expectThrows( + IllegalArgumentException.class, + () -> DLSRoleQueryValidator.evaluateAndVerifyRoleQuery(Strings.toString(queryBuilder4), XCONTENT_REGISTRY) + ); assertThat(e.getMessage(), equalTo("has_child query isn't supported as part of a role query")); QueryBuilder queryBuilder5 = new HasParentQueryBuilder("_type", new MatchAllQueryBuilder(), false); - e = expectThrows(IllegalArgumentException.class, () -> DLSRoleQueryValidator.verifyRoleQuery(queryBuilder5)); + e = expectThrows( + IllegalArgumentException.class, + () -> DLSRoleQueryValidator.evaluateAndVerifyRoleQuery(Strings.toString(queryBuilder5), XCONTENT_REGISTRY) + ); assertThat(e.getMessage(), equalTo("has_parent query isn't supported as part of a role query")); QueryBuilder queryBuilder6 = new BoolQueryBuilder().must(new GeoShapeQueryBuilder("field", "_id")); - e = expectThrows(IllegalArgumentException.class, () -> DLSRoleQueryValidator.verifyRoleQuery(queryBuilder6)); - assertThat(e.getMessage(), equalTo("geoshape query referring to indexed shapes isn't supported as part of a role query")); + e = expectThrows( + IllegalArgumentException.class, + () -> DLSRoleQueryValidator.evaluateAndVerifyRoleQuery(Strings.toString(queryBuilder6), XCONTENT_REGISTRY) + ); + assertThat( + e.getCause().getMessage(), + equalTo("geoshape query referring to indexed shapes isn't supported as part of a role query") + ); QueryBuilder queryBuilder7 = new ConstantScoreQueryBuilder(new GeoShapeQueryBuilder("field", "_id")); - e = expectThrows(IllegalArgumentException.class, () -> DLSRoleQueryValidator.verifyRoleQuery(queryBuilder7)); + e = expectThrows( + IllegalArgumentException.class, + () -> DLSRoleQueryValidator.evaluateAndVerifyRoleQuery(Strings.toString(queryBuilder7), XCONTENT_REGISTRY) + ); assertThat(e.getMessage(), equalTo("geoshape query referring to indexed shapes isn't supported as part of a role query")); QueryBuilder queryBuilder8 = new FunctionScoreQueryBuilder(new GeoShapeQueryBuilder("field", "_id")); - e = expectThrows(IllegalArgumentException.class, () -> DLSRoleQueryValidator.verifyRoleQuery(queryBuilder8)); + e = expectThrows( + IllegalArgumentException.class, + () -> DLSRoleQueryValidator.evaluateAndVerifyRoleQuery(Strings.toString(queryBuilder8), XCONTENT_REGISTRY) + ); assertThat(e.getMessage(), equalTo("geoshape query referring to indexed shapes isn't supported as part of a role query")); QueryBuilder queryBuilder9 = new BoostingQueryBuilder(new GeoShapeQueryBuilder("field", "_id"), new MatchAllQueryBuilder()); - e = expectThrows(IllegalArgumentException.class, () -> DLSRoleQueryValidator.verifyRoleQuery(queryBuilder9)); + e = expectThrows( + IllegalArgumentException.class, + () -> DLSRoleQueryValidator.evaluateAndVerifyRoleQuery(Strings.toString(queryBuilder9), XCONTENT_REGISTRY) + ); assertThat(e.getMessage(), equalTo("geoshape query referring to indexed shapes isn't supported as part of a role query")); } @@ -76,5 +112,4 @@ public void testHasStoredScript() throws IOException { is(false) ); } - } From c40f45b87a2699ff922f5fe43b6520feb5338c88 Mon Sep 17 00:00:00 2001 From: Henning Andersen <33268011+henningandersen@users.noreply.github.com> Date: Wed, 21 Dec 2022 14:28:11 +0100 Subject: [PATCH 329/919] Unsafe bootstrap memory optimization (#92493) Unsafe bootstrap could run out of memory due to dumping old and new cluster state. No longer attempt to do this unless in verbose mode. --- docs/changelog/92493.yaml | 5 +++++ .../coordination/UnsafeBootstrapMasterCommand.java | 10 ++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 docs/changelog/92493.yaml diff --git a/docs/changelog/92493.yaml b/docs/changelog/92493.yaml new file mode 100644 index 000000000000..a561defeeae3 --- /dev/null +++ b/docs/changelog/92493.yaml @@ -0,0 +1,5 @@ +pr: 92493 +summary: Unsafe bootstrap memory optimization +area: Cluster Coordination +type: bug +issues: [] diff --git a/server/src/main/java/org/elasticsearch/cluster/coordination/UnsafeBootstrapMasterCommand.java b/server/src/main/java/org/elasticsearch/cluster/coordination/UnsafeBootstrapMasterCommand.java index c4683aaabbae..a094fbee07e1 100644 --- a/server/src/main/java/org/elasticsearch/cluster/coordination/UnsafeBootstrapMasterCommand.java +++ b/server/src/main/java/org/elasticsearch/cluster/coordination/UnsafeBootstrapMasterCommand.java @@ -116,10 +116,12 @@ protected void processDataPaths(Terminal terminal, Path[] dataPaths, OptionSet o final ClusterState newClusterState = ClusterState.builder(oldClusterState).metadata(newMetadata).build(); - terminal.println( - Terminal.Verbosity.VERBOSE, - "[old cluster state = " + oldClusterState + ", new cluster state = " + newClusterState + "]" - ); + if (terminal.isPrintable(Terminal.Verbosity.VERBOSE)) { + terminal.println( + Terminal.Verbosity.VERBOSE, + "[old cluster state = " + oldClusterState + ", new cluster state = " + newClusterState + "]" + ); + } confirm(terminal, CONFIRMATION_MSG); From e1c861d08fafb8bd728e4c6e6e687df000ea149b Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 21 Dec 2022 13:47:44 +0000 Subject: [PATCH 330/919] Reject connection attempts while closing (#92465) Today if there is a constant stream of connection attempts then it's possible for the `ClusterConnectionManager` to wait forever in `close()` for `connectingRefCounter` to be fully released. With this commit we reject connection attempts while closing, avoiding this starvation situation. --- docs/changelog/92465.yaml | 5 + .../transport/ClusterConnectionManager.java | 8 +- .../ClusterConnectionManagerTests.java | 127 ++++++++++++++++++ 3 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 docs/changelog/92465.yaml diff --git a/docs/changelog/92465.yaml b/docs/changelog/92465.yaml new file mode 100644 index 000000000000..5c02ddff1e17 --- /dev/null +++ b/docs/changelog/92465.yaml @@ -0,0 +1,5 @@ +pr: 92465 +summary: Reject connection attempts while closing +area: Network +type: bug +issues: [] diff --git a/server/src/main/java/org/elasticsearch/transport/ClusterConnectionManager.java b/server/src/main/java/org/elasticsearch/transport/ClusterConnectionManager.java index 89a26ff8524f..15a5e0b1260b 100644 --- a/server/src/main/java/org/elasticsearch/transport/ClusterConnectionManager.java +++ b/server/src/main/java/org/elasticsearch/transport/ClusterConnectionManager.java @@ -75,7 +75,7 @@ public void removeListener(TransportConnectionListener listener) { @Override public void openConnection(DiscoveryNode node, ConnectionProfile connectionProfile, ActionListener listener) { ConnectionProfile resolvedProfile = ConnectionProfile.resolveConnectionProfile(connectionProfile, defaultProfile); - if (connectingRefCounter.tryIncRef()) { + if (acquireConnectingRef()) { var success = false; final var release = new RunOnce(connectingRefCounter::decRef); try { @@ -136,7 +136,7 @@ private void connectToNodeOrRetry( return; } - if (connectingRefCounter.tryIncRef() == false) { + if (acquireConnectingRef() == false) { listener.onFailure(new ConnectTransportException(node, "connection manager is closed")); return; } @@ -391,4 +391,8 @@ public ConnectionProfile getConnectionProfile() { return defaultProfile; } + private boolean acquireConnectingRef() { + return closing.get() == false && connectingRefCounter.tryIncRef(); + } + } diff --git a/server/src/test/java/org/elasticsearch/transport/ClusterConnectionManagerTests.java b/server/src/test/java/org/elasticsearch/transport/ClusterConnectionManagerTests.java index ff67abcb171d..46cece92665f 100644 --- a/server/src/test/java/org/elasticsearch/transport/ClusterConnectionManagerTests.java +++ b/server/src/test/java/org/elasticsearch/transport/ClusterConnectionManagerTests.java @@ -19,6 +19,9 @@ import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.util.concurrent.AbstractRunnable; +import org.elasticsearch.common.util.concurrent.ConcurrentCollections; +import org.elasticsearch.common.util.concurrent.RunOnce; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.core.Releasable; import org.elasticsearch.core.Releasables; @@ -42,6 +45,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -49,6 +53,7 @@ import static org.elasticsearch.test.ActionListenerUtils.anyActionListener; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.notNullValue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -346,6 +351,128 @@ public void testConcurrentConnects() throws Exception { } } + public void testConcurrentConnectsDuringClose() throws Exception { + + // This test ensures that closing the connection manager doesn't block forever, even if there's a constant stream of attempts to + // open connections. Note that closing the connection manager _does_ block while there are in-flight connection attempts, and in + // practice each attempt will (eventually) finish, so we're just trying to test that constant open attempts do not cause starvation. + // + // It works by spawning connection-open attempts in several concurrent loops, putting a Runnable to complete each attempt into a + // queue, and then consuming and completing the enqueued runnables in a separate thread. The consuming thread is throttled via a + // Semaphore, from which the main thread steals a permit which ensures that there's always at least one pending connection while the + // close is ongoing even though no connection attempt blocks forever. + + final var pendingConnectionPermits = new Semaphore(0); + final var pendingConnections = ConcurrentCollections.newQueue(); + + // transport#openConnection enqueues a Runnable to complete the connection attempt + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + final var listener = (ActionListener) invocationOnMock.getArguments()[2]; + final var targetNode = (DiscoveryNode) invocationOnMock.getArguments()[0]; + pendingConnections.add(() -> listener.onResponse(new TestConnect(targetNode))); + pendingConnectionPermits.release(); + return null; + }).when(transport).openConnection(any(), eq(connectionProfile), anyActionListener()); + + final ConnectionManager.ConnectionValidator validator = (c, p, l) -> l.onResponse(null); + + // Once we start to see connections being rejected, we give back the stolen permit so that the last connection can complete + final var onConnectException = new RunOnce(pendingConnectionPermits::release); + + // Create a few threads which open connections in a loop. Must be at least 2 so that there's always more connections incoming. + final var connectionLoops = between(2, 4); + final var connectionLoopCountDown = new CountDownLatch(connectionLoops); + final var expectConnectionFailures = new AtomicBoolean(); // unexpected failures would make this test pass vacuously + + class ConnectionLoop extends AbstractRunnable { + private final boolean useConnectToNode = randomBoolean(); + + @Override + public void onFailure(Exception e) { + assert false : e; + } + + @Override + protected void doRun() throws Exception { + final var discoveryNode = new DiscoveryNode("", new TransportAddress(InetAddress.getLoopbackAddress(), 0), Version.CURRENT); + final var listener = new ActionListener() { + @Override + public void onResponse(Releasable releasable) { + releasable.close(); + threadPool.generic().execute(ConnectionLoop.this); + } + + @Override + public void onFailure(Exception e) { + assertTrue(expectConnectionFailures.get()); + assertThat(e, instanceOf(ConnectTransportException.class)); + assertThat(e.getMessage(), containsString("connection manager is closed")); + onConnectException.run(); + connectionLoopCountDown.countDown(); + } + }; + + if (useConnectToNode) { + connectionManager.connectToNode(discoveryNode, connectionProfile, validator, listener); + } else { + connectionManager.openConnection(discoveryNode, connectionProfile, listener.map(c -> c::close)); + } + } + } + + for (int i = 0; i < connectionLoops; i++) { + threadPool.generic().execute(new ConnectionLoop()); + } + + // Create a separate thread to complete pending connection attempts, throttled by the pendingConnectionPermits semaphore + final var completionThread = new Thread(() -> { + while (true) { + try { + assertTrue(pendingConnectionPermits.tryAcquire(10, TimeUnit.SECONDS)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + // There could still be items in the queue when we are interrupted, so drain the queue before exiting: + while (pendingConnectionPermits.tryAcquire()) { + // noinspection ConstantConditions + pendingConnections.poll().run(); + } + return; + } + // noinspection ConstantConditions + pendingConnections.poll().run(); + } + }); + completionThread.start(); + + // Steal a permit so that the consumer lags behind the producers ... + assertTrue(pendingConnectionPermits.tryAcquire(10, TimeUnit.SECONDS)); + // ... and then send a connection attempt through the system to ensure that the lagging has started + Releasables.closeExpectNoException( + PlainActionFuture.get( + fut -> connectionManager.connectToNode( + new DiscoveryNode("", new TransportAddress(InetAddress.getLoopbackAddress(), 0), Version.CURRENT), + connectionProfile, + validator, + fut + ), + 30, + TimeUnit.SECONDS + ) + ); + + // Now close the connection manager + expectConnectionFailures.set(true); + connectionManager.close(); + // Success! The close call returned + + // Clean up and check everything completed properly + assertTrue(connectionLoopCountDown.await(10, TimeUnit.SECONDS)); + completionThread.interrupt(); + completionThread.join(); + assertTrue(pendingConnections.isEmpty()); + } + public void testConcurrentConnectsAndDisconnects() throws Exception { final DiscoveryNode node = new DiscoveryNode("", new TransportAddress(InetAddress.getLoopbackAddress(), 0), Version.CURRENT); doAnswer(invocationOnMock -> { From 51b8f81ec1da01362a6e165f37879c324975b946 Mon Sep 17 00:00:00 2001 From: Hendrik Muhs Date: Wed, 21 Dec 2022 16:13:02 +0100 Subject: [PATCH 331/919] avoid test error when string exclude is not a valid IP (#92501) avoid test error when string exclude is not a valid IP fixes #92500 --- .../frequentitemsets/FrequentItemSetsAggregatorTests.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregatorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregatorTests.java index bd45eb895c14..7ab4de13b297 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregatorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/frequentitemsets/FrequentItemSetsAggregatorTests.java @@ -271,7 +271,11 @@ public void testMixedSingleValues() throws IOException { fields.add( new MultiValuesSourceFieldConfig.Builder().setFieldName(IP_FIELD) .setIncludeExclude( - stringExclude != null ? new IncludeExclude(null, null, null, new TreeSet<>(Set.of(new BytesRef(stringExclude)))) : null + stringExclude != null + ? stringExclude.startsWith("1") // only add exclude if it is an IP + ? new IncludeExclude(null, null, null, new TreeSet<>(Set.of(new BytesRef(stringExclude)))) + : null + : null ) .build() ); From af65e71114866e3509f3b98222361ab95876cb7b Mon Sep 17 00:00:00 2001 From: Madhusudhan Konda Date: Wed, 21 Dec 2022 16:22:35 +0000 Subject: [PATCH 332/919] The exception is inserted in a code block (#90325) * The exception is inserted in a code block * Update docs/reference/mapping/types/text.asciidoc Co-authored-by: Abdon Pijpelink --- docs/reference/mapping/types/text.asciidoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference/mapping/types/text.asciidoc b/docs/reference/mapping/types/text.asciidoc index aec330701a76..acc9c962add9 100644 --- a/docs/reference/mapping/types/text.asciidoc +++ b/docs/reference/mapping/types/text.asciidoc @@ -266,11 +266,11 @@ Will become: `text` fields are searchable by default, but by default are not available for aggregations, sorting, or scripting. If you try to sort, aggregate, or access -values from a script on a `text` field, you will see this exception: +values from a `text` field using a script, you'll see an exception indicating +that field data is disabled by default on text fields. To load field data in +memory, set `fielddata=true` on your field. -Fielddata is disabled on text fields by default. Set `fielddata=true` on -`your_field_name` in order to load fielddata in memory by uninverting the -inverted index. Note that this can however use significant memory. +NOTE: Loading field data in memory can consume significant memory. Field data is the only way to access the analyzed tokens from a full text field in aggregations, sorting, or scripting. For example, a full text field like `New York` From f7e0d477f65440a00f8fc7b4edd5349db6cca2f5 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Wed, 21 Dec 2022 16:25:00 +0000 Subject: [PATCH 333/919] Optimize composite agg with leading global ordinal value source (#92197) When queries are present in a search with a composite agg with a leading source that is of type `GlobalOrdinalValuesSource` there is an optimization we can do. In particular, once the composite queue is full, we know the range of ordinals we are interested in from the source. Thus, we can add a competitive iterator to the `LeafBucketCollector` that skips documents that are out of the competitive range. This commit adds that optimization. In a dataset I have experimented with that has ~31M docs I observed a 5x improvement in a simple search with a range query that matched ~28M docs and with `size = 5` over a keyword field whose cardinality was 200. Co-authored-by: Adrien Grand --- docs/changelog/92197.yaml | 5 + .../ChildrenAggregatorFactory.java | 2 +- .../aggregations/ParentAggregatorFactory.java | 2 +- .../bucket/composite/CompositeAggregator.java | 30 +- .../CompositeValuesCollectorQueue.java | 95 +++++- .../composite/GlobalOrdinalValuesSource.java | 254 +++++++++++++++ .../composite/TermsValuesSourceBuilder.java | 7 + .../bucket/terms/TermsAggregatorFactory.java | 2 +- .../metrics/CardinalityAggregatorFactory.java | 2 +- .../aggregations/support/ValuesSource.java | 4 +- .../ProfilingLeafBucketCollector.java | 6 + .../profile/query/ProfileCollector.java | 6 + .../query/EarlyTerminatingCollector.java | 11 + .../composite/CompositeAggregatorTests.java | 308 ++++++++++++++++++ .../CompositeValuesCollectorQueueTests.java | 18 +- .../SingleDimensionValuesSourceTests.java | 4 + 16 files changed, 741 insertions(+), 15 deletions(-) create mode 100644 docs/changelog/92197.yaml diff --git a/docs/changelog/92197.yaml b/docs/changelog/92197.yaml new file mode 100644 index 000000000000..a28740217277 --- /dev/null +++ b/docs/changelog/92197.yaml @@ -0,0 +1,5 @@ +pr: 92197 +summary: Optimize composite agg with leading global ordinal value source +area: Aggregations +type: enhancement +issues: [] diff --git a/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/ChildrenAggregatorFactory.java b/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/ChildrenAggregatorFactory.java index 83cc38636b10..fc797be04451 100644 --- a/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/ChildrenAggregatorFactory.java +++ b/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/ChildrenAggregatorFactory.java @@ -68,7 +68,7 @@ protected Aggregator doCreateInternal(Aggregator parent, CardinalityUpperBound c ); } WithOrdinals valuesSource = (WithOrdinals) rawValuesSource; - long maxOrd = valuesSource.globalMaxOrd(context.searcher()); + long maxOrd = valuesSource.globalMaxOrd(context.searcher().getIndexReader()); return new ParentToChildrenAggregator( name, factories, diff --git a/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/ParentAggregatorFactory.java b/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/ParentAggregatorFactory.java index 5ff008832622..f531ac67b98f 100644 --- a/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/ParentAggregatorFactory.java +++ b/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/ParentAggregatorFactory.java @@ -68,7 +68,7 @@ protected Aggregator doCreateInternal(Aggregator children, CardinalityUpperBound ); } WithOrdinals valuesSource = (WithOrdinals) rawValuesSource; - long maxOrd = valuesSource.globalMaxOrd(context.searcher()); + long maxOrd = valuesSource.globalMaxOrd(context.searcher().getIndexReader()); return new ChildrenToParentAggregator( name, factories, diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregator.java index 7d01e7598537..83fc95a43e17 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregator.java @@ -34,6 +34,7 @@ import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.Rounding; import org.elasticsearch.core.Releasables; +import org.elasticsearch.core.Strings; import org.elasticsearch.index.IndexSortConfig; import org.elasticsearch.lucene.queries.SearchAfterSortedDocQuery; import org.elasticsearch.search.DocValueFormat; @@ -60,11 +61,13 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.function.BiConsumer; import java.util.function.LongUnaryOperator; import static org.elasticsearch.search.aggregations.MultiBucketConsumerService.MAX_BUCKET_SETTING; public final class CompositeAggregator extends BucketsAggregator implements SizedBucketAggregator { + private final int size; private final List sourceNames; private final int[] reverseMuls; @@ -130,7 +133,7 @@ public final class CompositeAggregator extends BucketsAggregator implements Size } } this.innerSizedBucketAggregators = dateHistogramValuesSources.toArray(new DateHistogramValuesSource[0]); - this.queue = new CompositeValuesCollectorQueue(aggCtx.bigArrays(), sources, size); + this.queue = new CompositeValuesCollectorQueue(aggCtx.bigArrays(), sources, size, aggCtx.searcher().getIndexReader()); if (rawAfterKey != null) { try { this.queue.setAfterKey(rawAfterKey); @@ -155,6 +158,14 @@ protected void doClose() { } } + @Override + public ScoreMode scoreMode() { + if (queue.mayDynamicallyPrune()) { + return super.scoreMode().needsScores() ? ScoreMode.TOP_DOCS_WITH_SCORES : ScoreMode.TOP_DOCS; + } + return super.scoreMode(); + } + @Override protected void doPreCollection() throws IOException { deferredCollectors = MultiBucketCollector.wrap(false, Arrays.asList(subAggregators)); @@ -492,6 +503,15 @@ public void collect(int doc, long zeroBucket) throws IOException { assert zeroBucket == 0L; inner.collect(doc); } + + @Override + public DocIdSetIterator competitiveIterator() throws IOException { + if (queue.mayDynamicallyPrune()) { + return inner.competitiveIterator(); + } else { + return null; + } + } }; } } @@ -607,6 +627,14 @@ public double bucketSize(Rounding.DateTimeUnit unit) { return innerSizedBucketAggregators[0].bucketSize(unit); } + @Override + public void collectDebugInfo(BiConsumer add) { + super.collectDebugInfo(add); + if (sources[0]instanceof GlobalOrdinalValuesSource globalOrdinalValuesSource) { + globalOrdinalValuesSource.collectDebugInfo(Strings.format("sources.%s", sourceConfigs[0].name()), add); + } + } + private static class Entry { final AggregationExecutionContext aggCtx; final DocIdSet docIdSet; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesCollectorQueue.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesCollectorQueue.java index ca09ef61dcad..7057b1dd55c8 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesCollectorQueue.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesCollectorQueue.java @@ -8,6 +8,7 @@ package org.elasticsearch.search.aggregations.bucket.composite; +import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.search.CollectionTerminatedException; import org.apache.lucene.util.PriorityQueue; @@ -48,6 +49,11 @@ public int hashCode() { } } + @FunctionalInterface + private interface CompetitiveBoundsChangedListener { + void boundsChanged(int topSlot) throws IOException; + } + // the slot for the current candidate private static final int CANDIDATE_SLOT = Integer.MAX_VALUE; @@ -55,6 +61,7 @@ public int hashCode() { private final int maxSize; private final Map map; private final SingleDimensionValuesSource[] arrays; + private final CompetitiveBoundsChangedListener competitiveBoundsChangedListener; private LongArray docCounts; private boolean afterKeyIsSet = false; @@ -62,18 +69,83 @@ public int hashCode() { /** * Constructs a composite queue with the specified size and sources. * - * @param sources The list of {@link CompositeValuesSourceConfig} to build the composite buckets. - * @param size The number of composite buckets to keep. + * @param sources The list of {@link CompositeValuesSourceConfig} to build the composite buckets. + * @param size The number of composite buckets to keep. + * @param indexReader */ - CompositeValuesCollectorQueue(BigArrays bigArrays, SingleDimensionValuesSource[] sources, int size) { + CompositeValuesCollectorQueue(BigArrays bigArrays, SingleDimensionValuesSource[] sources, int size, IndexReader indexReader) { super(size); this.bigArrays = bigArrays; this.maxSize = size; this.arrays = sources; + + // If the leading source is a GlobalOrdinalValuesSource we can apply an optimization which requires + // tracking the highest competitive value. + if (arrays[0]instanceof GlobalOrdinalValuesSource globalOrdinalValuesSource) { + if (shouldApplyGlobalOrdinalDynamicPruningForLeadingSource(sources, size, indexReader)) { + competitiveBoundsChangedListener = topSlot -> globalOrdinalValuesSource.updateHighestCompetitiveValue(topSlot); + } else { + competitiveBoundsChangedListener = null; + } + } else { + competitiveBoundsChangedListener = null; + } + this.map = Maps.newMapWithExpectedSize(size); this.docCounts = bigArrays.newLongArray(1, false); } + private static boolean shouldApplyGlobalOrdinalDynamicPruningForLeadingSource( + SingleDimensionValuesSource[] sources, + int size, + IndexReader indexReader + ) { + if (sources.length == 0) { + return false; + } + if (sources[0]instanceof GlobalOrdinalValuesSource firstSource) { + if (firstSource.mayDynamicallyPrune(indexReader) == false) { + return false; + } + + long approximateTotalNumberOfBuckets = firstSource.getUniqueValueCount(); + if (sources.length > 1) { + // When there are multiple sources, it's hard to guess how many + // unique buckets there might be. Let's be conservative and + // assume that other sources increase the number of buckets by + // 3x. + approximateTotalNumberOfBuckets *= 3L; + } + // If the size is not significantly less than the total number of + // buckets then dynamic pruning can't help much. + if (size >= approximateTotalNumberOfBuckets / 8) { + return false; + } + + // Try to estimate the width of the ordinal range that might be + // returned on each page. Since not all ordinals might match the + // query, we're increasing `size` by 25%. + long rangeWidthPerPage = size + (size / 4); + if (sources.length > 1) { + // Again assume other sources bump the number of buckets by 3x + rangeWidthPerPage /= 3; + } + if (rangeWidthPerPage > GlobalOrdinalValuesSource.MAX_TERMS_FOR_DYNAMIC_PRUNING) { + return false; + } + return true; + } + return false; + } + + /** + * Return true if this queue produces a {@link LeafBucketCollector} that may + * dynamically prune hits that are not competitive. + */ + public boolean mayDynamicallyPrune() { + return competitiveBoundsChangedListener != null; + } + /** * Sets after key * @param afterKey composite key @@ -221,7 +293,14 @@ CompositeKey toCompositeKey(int slot) throws IOException { * The provided collector in is called on each composite bucket. */ LeafBucketCollector getLeafCollector(LeafReaderContext context, LeafBucketCollector in) throws IOException { - return getLeafCollector(null, context, in); + LeafBucketCollector leafBucketCollector = getLeafCollector(null, context, in); + + // As we are starting to collect from a new segment we need to update the topChangedListener if present + // and if the queue is full. + if (competitiveBoundsChangedListener != null && size() >= maxSize) { + competitiveBoundsChangedListener.boundsChanged(top()); + } + return leafBucketCollector; } /** @@ -249,7 +328,7 @@ LeafBucketCollector getLeafCollector(Comparable forceLeadSourceValue, LeafRea * Check if the current candidate should be added in the queue. * @return true if the candidate is competitive (added or already in the queue). */ - boolean addIfCompetitive(long inc) { + boolean addIfCompetitive(long inc) throws IOException { return addIfCompetitive(0, inc); } @@ -263,7 +342,7 @@ boolean addIfCompetitive(long inc) { * * @throws CollectionTerminatedException if the current collection can be terminated early due to index sorting. */ - boolean addIfCompetitive(int indexSortSourcePrefix, long inc) { + boolean addIfCompetitive(int indexSortSourcePrefix, long inc) throws IOException { // checks if the candidate key is competitive Integer topSlot = compareCurrent(); if (topSlot != null) { @@ -312,6 +391,10 @@ boolean addIfCompetitive(int indexSortSourcePrefix, long inc) { copyCurrent(newSlot, inc); map.put(new Slot(newSlot), newSlot); add(newSlot); + + if (competitiveBoundsChangedListener != null && size() >= maxSize) { + competitiveBoundsChangedListener.boundsChanged(top()); + } return true; } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/GlobalOrdinalValuesSource.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/GlobalOrdinalValuesSource.java index 30009bad1acc..855b45654631 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/GlobalOrdinalValuesSource.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/GlobalOrdinalValuesSource.java @@ -10,20 +10,33 @@ import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.PostingsEnum; import org.apache.lucene.index.SortedSetDocValues; +import org.apache.lucene.index.Terms; +import org.apache.lucene.index.TermsEnum; +import org.apache.lucene.search.DocIdSetIterator; +import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.Query; import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.PriorityQueue; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.LongArray; import org.elasticsearch.core.CheckedFunction; import org.elasticsearch.core.Releasables; +import org.elasticsearch.core.Strings; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.StringFieldType; +import org.elasticsearch.logging.LogManager; +import org.elasticsearch.logging.Logger; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.aggregations.LeafBucketCollector; import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.ArrayDeque; +import java.util.List; +import java.util.function.BiConsumer; import static org.apache.lucene.index.SortedSetDocValues.NO_MORE_ORDS; @@ -31,13 +44,23 @@ * A {@link SingleDimensionValuesSource} for global ordinals. */ class GlobalOrdinalValuesSource extends SingleDimensionValuesSource { + + private static final Logger logger = LogManager.getLogger(GlobalOrdinalValuesSource.class); + + public static final int MAX_TERMS_FOR_DYNAMIC_PRUNING = 128; + public static final long MISSING_VALUE_FLAG = -1L; + private final long uniqueValueCount; private final CheckedFunction docValuesFunc; private LongArray values; private SortedSetDocValues lookup; private long currentValue; private Long afterValueGlobalOrd; + private Long highestCompetitiveValueGlobalOrd; private boolean isTopValueInsertionPoint; + private CompetitiveIterator currentCompetitiveIterator; + private int segmentsDynamicPruningUsed; + private int totalSegments; private long lastLookupOrd = -1; private BytesRef lastLookupValue; @@ -45,6 +68,7 @@ class GlobalOrdinalValuesSource extends SingleDimensionValuesSource { GlobalOrdinalValuesSource( BigArrays bigArrays, MappedFieldType type, + long uniqueValueCount, CheckedFunction docValuesFunc, DocValueFormat format, boolean missingBucket, @@ -53,10 +77,58 @@ class GlobalOrdinalValuesSource extends SingleDimensionValuesSource { int reverseMul ) { super(bigArrays, format, type, missingBucket, missingOrder, size, reverseMul); + this.uniqueValueCount = uniqueValueCount; this.docValuesFunc = docValuesFunc; this.values = bigArrays.newLongArray(Math.min(size, 100), false); } + /** + * Return the number of unique values of this source. Note that some unique + * values might not produce buckets if the query doesn't match documents + * that contain these values. + */ + long getUniqueValueCount() { + return uniqueValueCount; + } + + /** + * Return whether the source can be used for dynamic pruning. + */ + boolean mayDynamicallyPrune(IndexReader indexReader) { + // If missing bucket is requested we have no way to tell lucene to efficiently match + // buckets in a range AND missing values. + if (missingBucket) { + return false; + } + + // If we do not know the field type, the field is not indexed (e.g. runtime field) or the field + // has no doc values we should not apply the optimization. + if (fieldType == null || fieldType.isIndexed() == false || fieldType.hasDocValues() == false) { + return false; + } + + // We also need to check if there are terms for the field in question. + // Some field types do not despite being global ordinals (e.g. IP addresses). + return hasTerms(indexReader); + } + + private boolean hasTerms(IndexReader indexReader) { + assert fieldType != null; + List leaves = indexReader.leaves(); + for (LeafReaderContext leaf : leaves) { + Terms terms; + try { + terms = leaf.reader().terms(fieldType.name()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + if (terms != null) { + return true; + } + } + return false; + } + @Override void copyCurrent(int slot) { values = bigArrays.grow(values, slot + 1); @@ -133,11 +205,21 @@ BytesRef toComparable(int slot) throws IOException { @Override LeafBucketCollector getLeafCollector(LeafReaderContext context, LeafBucketCollector next) throws IOException { + totalSegments++; final SortedSetDocValues dvs = docValuesFunc.apply(context); if (lookup == null) { initLookup(dvs); } + + // We create a competitive iterator that allows us to optimize by narrowing down the search + // to terms that are within the range based on what the composite queue is tracking. + // For example, if the composite agg size is 5, and we have seen terms with ordinals [1, 4, 5, 10, 11], + // we know that we never need to look at terms with and ordinal higher than 11. + final CompetitiveIterator competitiveIterator = fieldType == null ? null : new CompetitiveIterator(context, fieldType.name()); + currentCompetitiveIterator = competitiveIterator; + return new LeafBucketCollector() { + @Override public void collect(int doc, long bucket) throws IOException { if (dvs.advanceExact(doc)) { @@ -151,6 +233,11 @@ public void collect(int doc, long bucket) throws IOException { next.collect(doc, bucket); } } + + @Override + public DocIdSetIterator competitiveIterator() { + return competitiveIterator; + } }; } @@ -160,6 +247,7 @@ LeafBucketCollector getLeafCollector(Comparable value, LeafReaderConte if (value.getClass() != BytesRef.class) { throw new IllegalArgumentException("Expected BytesRef, got " + value.getClass()); } + totalSegments++; BytesRef term = (BytesRef) value; final SortedSetDocValues dvs = docValuesFunc.apply(context); if (lookup == null) { @@ -214,4 +302,170 @@ private void initLookup(SortedSetDocValues dvs) throws IOException { } } } + + public void updateHighestCompetitiveValue(int slot) throws IOException { + highestCompetitiveValueGlobalOrd = values.get(slot); + logger.trace("Highest observed set to [{}]", highestCompetitiveValueGlobalOrd); + final CompetitiveIterator competitiveIterator = currentCompetitiveIterator; + if (competitiveIterator != null) { + competitiveIterator.updateBounds(); + } + } + + void collectDebugInfo(String namespace, BiConsumer add) { + add.accept(Strings.format("%s.segments_collected", namespace), totalSegments); + add.accept(Strings.format("%s.segments_dynamic_pruning_used", namespace), segmentsDynamicPruningUsed); + } + + private record PostingsEnumAndOrd(PostingsEnum postings, long ord) {} + + private class CompetitiveIterator extends DocIdSetIterator { + + private final LeafReaderContext context; + private final int maxDoc; + private final String field; + private int doc = -1; + private ArrayDeque postings; + private DocIdSetIterator docsWithField; + private PriorityQueue disjunction; + + CompetitiveIterator(LeafReaderContext context, String field) { + this.context = context; + this.maxDoc = context.reader().maxDoc(); + this.field = field; + } + + @Override + public int docID() { + return doc; + } + + @Override + public int nextDoc() throws IOException { + return advance(docID() + 1); + } + + @Override + public int advance(int target) throws IOException { + if (target >= maxDoc) { + return doc = NO_MORE_DOCS; + } else if (disjunction == null) { + if (docsWithField != null) { + return doc = docsWithField.advance(target); + } else { + // We haven't started skipping yet + return doc = target; + } + } else { + PostingsEnumAndOrd top = disjunction.top(); + if (top == null) { + // priority queue is empty, none of the remaining documents are competitive + return doc = NO_MORE_DOCS; + } + while (top.postings.docID() < target) { + top.postings.advance(target); + top = disjunction.updateTop(); + } + return doc = top.postings.docID(); + } + } + + @Override + public long cost() { + return context.reader().maxDoc(); + } + + /** + * Update this iterator to only match postings whose term has an ordinal between {@code minOrd} + * included and {@code maxOrd} included. + */ + private void update(long minOrd, long maxOrd) throws IOException { + final int maxTerms = Math.min(MAX_TERMS_FOR_DYNAMIC_PRUNING, IndexSearcher.getMaxClauseCount()); + final long size = Math.max(0, maxOrd - minOrd + 1); + if (size > maxTerms) { + if (docsWithField == null) { + docsWithField = docValuesFunc.apply(context); + } + } else if (postings == null) { + init(minOrd, maxOrd); + } else { + boolean removed = false; + // Zero or more ords got removed + while (postings.isEmpty() == false && postings.getFirst().ord < minOrd) { + removed = true; + postings.removeFirst(); + } + while (postings.isEmpty() == false && postings.getLast().ord > maxOrd) { + removed = true; + postings.removeLast(); + } + if (removed) { + disjunction.clear(); + disjunction.addAll(postings); + } + } + } + + /** + * For the first time, this iterator is allowed to skip documents. It needs to pull {@link + * PostingsEnum}s from the terms dictionary of the inverted index and create a priority queue + * out of them. + */ + private void init(long minOrd, long maxOrd) throws IOException { + segmentsDynamicPruningUsed++; + + final int size = (int) Math.max(0, maxOrd - minOrd + 1); + postings = new ArrayDeque<>(size); + if (size > 0) { + Terms terms = context.reader().terms(field); + if (terms != null) { + BytesRef minTerm = BytesRef.deepCopyOf(lookup.lookupOrd(minOrd)); + TermsEnum termsEnum = terms.iterator(); + TermsEnum.SeekStatus seekStatus = termsEnum.seekCeil(minTerm); + if (seekStatus != TermsEnum.SeekStatus.END) { + BytesRef maxTerm = BytesRef.deepCopyOf(lookup.lookupOrd(maxOrd)); + + TermsEnum globalTermsEnum = lookup.termsEnum(); + globalTermsEnum.seekExact(minOrd); + + for (BytesRef term = termsEnum.term(); term != null && term.compareTo(maxTerm) <= 0; term = termsEnum.next()) { + // Compute the global ordinal of this term by advancing the global terms enum to the same term and retrieving + // the term ordinal. This is cheaper than calling lookupTerm for every term. + while (globalTermsEnum.term().compareTo(term) < 0) { + BytesRef nextGlobalTerm = globalTermsEnum.next(); + assert nextGlobalTerm != null; + } + assert globalTermsEnum.term().equals(term); + final long globalOrd = globalTermsEnum.ord(); + postings.add(new PostingsEnumAndOrd(termsEnum.postings(null, PostingsEnum.NONE), globalOrd)); + } + } + } + } + disjunction = new PriorityQueue<>(size) { + @Override + protected boolean lessThan(PostingsEnumAndOrd a, PostingsEnumAndOrd b) { + return a.postings.docID() < b.postings.docID(); + } + }; + disjunction.addAll(postings); + } + + public void updateBounds() throws IOException { + if (highestCompetitiveValueGlobalOrd == null) { + return; + } + + // TODO If this is the only source, we know we are done with the buckets of the after_key. + // We could optimize even further by skipping to the next global ordinal after the after_key. + + long lowOrd; + if (afterValueGlobalOrd != null && afterValueGlobalOrd != MISSING_VALUE_FLAG) { + lowOrd = afterValueGlobalOrd; + } else { + lowOrd = reverseMul == 1 ? 0 : lookup.getValueCount() - 1; + } + update(Math.min(highestCompetitiveValueGlobalOrd, lowOrd), Math.max(highestCompetitiveValueGlobalOrd, lowOrd)); + } + } } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/TermsValuesSourceBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/TermsValuesSourceBuilder.java index dcbaed78ba06..e5e89d94c803 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/TermsValuesSourceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/TermsValuesSourceBuilder.java @@ -168,9 +168,16 @@ static void register(ValuesSourceRegistry.Builder builder) { if (valuesSourceConfig.hasOrdinals() && reader instanceof DirectoryReader) { ValuesSource.Bytes.WithOrdinals vs = (ValuesSource.Bytes.WithOrdinals) compositeValuesSourceConfig.valuesSource(); + long maxOrd; + try { + maxOrd = vs.globalMaxOrd(reader); + } catch (IOException e) { + throw new UnsupportedOperationException(e); + } return new GlobalOrdinalValuesSource( bigArrays, compositeValuesSourceConfig.fieldType(), + maxOrd, vs::globalOrdinalsValues, compositeValuesSourceConfig.format(), compositeValuesSourceConfig.missingBucket(), diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorFactory.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorFactory.java index 904d8b72d70d..5bea0f14db0c 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorFactory.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorFactory.java @@ -354,7 +354,7 @@ static SubAggCollectionMode pickSubAggColectMode(AggregatorFactories factories, */ private static long getMaxOrd(ValuesSource source, IndexSearcher searcher) throws IOException { if (source instanceof ValuesSource.Bytes.WithOrdinals valueSourceWithOrdinals) { - return valueSourceWithOrdinals.globalMaxOrd(searcher); + return valueSourceWithOrdinals.globalMaxOrd(searcher.getIndexReader()); } else { return -1; } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/CardinalityAggregatorFactory.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/CardinalityAggregatorFactory.java index 4481254aba36..1b1ab107c73c 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/CardinalityAggregatorFactory.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/CardinalityAggregatorFactory.java @@ -157,7 +157,7 @@ public static void registerAggregators(ValuesSourceRegistry.Builder builder) { if (valuesSourceConfig.hasValues()) { if (valuesSourceConfig.getValuesSource()instanceof final ValuesSource.Bytes.WithOrdinals source) { if (executionMode.useGlobalOrdinals(context, source, precision)) { - final long maxOrd = source.globalMaxOrd(context.searcher()); + final long maxOrd = source.globalMaxOrd(context.searcher().getIndexReader()); return new GlobalOrdCardinalityAggregator( name, source, diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSource.java b/server/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSource.java index 21f7d91120cb..ca9fa237f7fc 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSource.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSource.java @@ -14,7 +14,6 @@ import org.apache.lucene.index.OrdinalMap; import org.apache.lucene.index.SortedNumericDocValues; import org.apache.lucene.index.SortedSetDocValues; -import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Scorable; import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.Rounding; @@ -249,8 +248,7 @@ public boolean hasOrdinals() { * Get the maximum global ordinal. Requires {@link #globalOrdinalsValues} * so see the note about its performance. */ - public long globalMaxOrd(IndexSearcher indexSearcher) throws IOException { - IndexReader indexReader = indexSearcher.getIndexReader(); + public long globalMaxOrd(IndexReader indexReader) throws IOException { if (indexReader.leaves().isEmpty()) { return 0; } else { diff --git a/server/src/main/java/org/elasticsearch/search/profile/aggregation/ProfilingLeafBucketCollector.java b/server/src/main/java/org/elasticsearch/search/profile/aggregation/ProfilingLeafBucketCollector.java index 1e8d8ee1cd5d..8e84a19d1daf 100644 --- a/server/src/main/java/org/elasticsearch/search/profile/aggregation/ProfilingLeafBucketCollector.java +++ b/server/src/main/java/org/elasticsearch/search/profile/aggregation/ProfilingLeafBucketCollector.java @@ -8,6 +8,7 @@ package org.elasticsearch.search.profile.aggregation; +import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.search.Scorable; import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.profile.Timer; @@ -34,6 +35,11 @@ public void collect(int doc, long bucket) throws IOException { } } + @Override + public DocIdSetIterator competitiveIterator() throws IOException { + return delegate.competitiveIterator(); + } + @Override public void setScorer(Scorable scorer) throws IOException { delegate.setScorer(scorer); diff --git a/server/src/main/java/org/elasticsearch/search/profile/query/ProfileCollector.java b/server/src/main/java/org/elasticsearch/search/profile/query/ProfileCollector.java index 9d025d99e117..84728499db0f 100644 --- a/server/src/main/java/org/elasticsearch/search/profile/query/ProfileCollector.java +++ b/server/src/main/java/org/elasticsearch/search/profile/query/ProfileCollector.java @@ -10,6 +10,7 @@ import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.search.Collector; +import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.search.FilterCollector; import org.apache.lucene.search.FilterLeafCollector; import org.apache.lucene.search.LeafCollector; @@ -64,6 +65,11 @@ public void collect(int doc) throws IOException { } } + @Override + public DocIdSetIterator competitiveIterator() throws IOException { + return in.competitiveIterator(); + } + @Override public void setScorer(Scorable scorer) throws IOException { final long start = System.nanoTime(); diff --git a/server/src/main/java/org/elasticsearch/search/query/EarlyTerminatingCollector.java b/server/src/main/java/org/elasticsearch/search/query/EarlyTerminatingCollector.java index 85ab5b14bb0f..bc4bc079aa4b 100644 --- a/server/src/main/java/org/elasticsearch/search/query/EarlyTerminatingCollector.java +++ b/server/src/main/java/org/elasticsearch/search/query/EarlyTerminatingCollector.java @@ -14,6 +14,7 @@ import org.apache.lucene.search.FilterCollector; import org.apache.lucene.search.FilterLeafCollector; import org.apache.lucene.search.LeafCollector; +import org.apache.lucene.search.ScoreMode; import java.io.IOException; @@ -45,6 +46,16 @@ static final class EarlyTerminationException extends RuntimeException { this.forceTermination = forceTermination; } + @Override + public ScoreMode scoreMode() { + // Let the query know that this collector doesn't intend to collect all hits. + ScoreMode scoreMode = super.scoreMode(); + if (scoreMode.isExhaustive()) { + scoreMode = scoreMode.needsScores() ? ScoreMode.TOP_DOCS_WITH_SCORES : ScoreMode.TOP_DOCS; + } + return scoreMode; + } + @Override public LeafCollector getLeafCollector(LeafReaderContext context) throws IOException { if (numCollected >= maxCountHits) { diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregatorTests.java index a4d1d69c5925..16b7d30e9ab9 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregatorTests.java @@ -44,6 +44,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.time.DateFormatters; +import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.mapper.DateFieldMapper; @@ -113,10 +114,13 @@ import java.util.stream.Collectors; import static org.elasticsearch.search.aggregations.bucket.nested.NestedAggregatorTests.nestedObject; +import static org.elasticsearch.test.MapMatcher.assertMap; +import static org.elasticsearch.test.MapMatcher.matchesMap; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; @@ -3255,6 +3259,310 @@ public void testCompositeWithSamplingAndOneSmallBucket() throws Exception { ); } + public void testWithKeywordGivenNoIndexSortingAndDynamicPruningIsNotApplicableDueToMissingBucket() throws Exception { + final CompositeAggregationBuilder aggregationBuilder = new CompositeAggregationBuilder( + "name", + List.of(new TermsValuesSourceBuilder("leading").field("keyword").missingBucket(true)) + ).size(2); + final MappedFieldType keywordMapping = new KeywordFieldMapper.KeywordFieldType("keyword"); + final MappedFieldType fooMapping = new KeywordFieldMapper.KeywordFieldType("foo"); + + CheckedConsumer buildIndex = iw -> { + for (int i = 1; i <= 100; i++) { + addDocWithKeywordFields(iw, "keyword", "a_" + i, "foo", "bar"); + } + }; + + debugTestCase( + aggregationBuilder, + new TermQuery(new Term("foo", "bar")), + buildIndex, + (InternalComposite result, Class impl, Map> debug) -> { + assertThat(result.getBuckets(), hasSize(2)); + assertEquals(CompositeAggregator.class, impl); + assertEquals("{leading=a_10}", result.afterKey().toString()); + assertEquals("{leading=a_1}", result.getBuckets().get(0).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(0).getDocCount()); + assertEquals("{leading=a_10}", result.getBuckets().get(1).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(1).getDocCount()); + assertMap( + debug, + matchesMap().entry( + "name", + matchesMap().entry("sources.leading.segments_dynamic_pruning_used", equalTo(0)) + .entry("sources.leading.segments_collected", greaterThanOrEqualTo(1)) + ) + ); + }, + keywordMapping, + fooMapping + ); + } + + public void testWithKeywordGivenNoIndexSortingAndDynamicPruningIsNotApplicableDueToSize() throws Exception { + final CompositeAggregationBuilder aggregationBuilder = new CompositeAggregationBuilder( + "name", + List.of(new TermsValuesSourceBuilder("leading").field("keyword").missingBucket(true)) + ).size(13); // We need a size that is more than 1/8 of the total count. + final MappedFieldType keywordMapping = new KeywordFieldMapper.KeywordFieldType("keyword"); + final MappedFieldType fooMapping = new KeywordFieldMapper.KeywordFieldType("foo"); + + CheckedConsumer buildIndex = iw -> { + for (int i = 1; i <= 100; i++) { + addDocWithKeywordFields(iw, "keyword", "a_" + i, "foo", "bar"); + } + }; + + debugTestCase( + aggregationBuilder, + new TermQuery(new Term("foo", "bar")), + buildIndex, + (InternalComposite result, Class impl, Map> debug) -> { + assertThat(result.getBuckets(), hasSize(13)); + assertEquals(CompositeAggregator.class, impl); + assertMap( + debug, + matchesMap().entry( + "name", + matchesMap().entry("sources.leading.segments_dynamic_pruning_used", equalTo(0)) + .entry("sources.leading.segments_collected", greaterThanOrEqualTo(1)) + ) + ); + }, + keywordMapping, + fooMapping + ); + } + + public void testWithKeywordGivenNoIndexSortingAndDynamicPruningIsApplicableAndAscendingOrder() throws Exception { + CompositeAggregationBuilder aggregationBuilder = new CompositeAggregationBuilder( + "name", + List.of(new TermsValuesSourceBuilder("leading").field("keyword")) + ).size(2); + final MappedFieldType keywordMapping = new KeywordFieldMapper.KeywordFieldType("keyword"); + final MappedFieldType fooMapping = new KeywordFieldMapper.KeywordFieldType("foo"); + + CheckedConsumer buildIndex = iw -> { + for (int i = 1; i <= 100; i++) { + addDocWithKeywordFields(iw, "keyword", "a_" + i, "foo", "bar"); + } + }; + + debugTestCase( + aggregationBuilder, + new TermQuery(new Term("foo", "bar")), + buildIndex, + (InternalComposite result, Class impl, Map> debug) -> { + assertThat(result.getBuckets(), hasSize(2)); + assertEquals(CompositeAggregator.class, impl); + assertEquals("{leading=a_10}", result.afterKey().toString()); + assertEquals("{leading=a_1}", result.getBuckets().get(0).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(0).getDocCount()); + assertEquals("{leading=a_10}", result.getBuckets().get(1).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(1).getDocCount()); + assertMap( + debug, + matchesMap().entry( + "name", + matchesMap().entry("sources.leading.segments_dynamic_pruning_used", greaterThanOrEqualTo(1)) + .entry("sources.leading.segments_collected", greaterThanOrEqualTo(1)) + ) + ); + }, + keywordMapping, + fooMapping + ); + + aggregationBuilder = new CompositeAggregationBuilder("name", List.of(new TermsValuesSourceBuilder("leading").field("keyword"))) + .size(2) + .aggregateAfter(Collections.singletonMap("leading", "a_10")); + debugTestCase( + aggregationBuilder, + new TermQuery(new Term("foo", "bar")), + buildIndex, + (InternalComposite result, Class impl, Map> debug) -> { + assertThat(result.getBuckets(), hasSize(2)); + assertEquals(CompositeAggregator.class, impl); + assertEquals("{leading=a_11}", result.afterKey().toString()); + assertEquals("{leading=a_100}", result.getBuckets().get(0).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(0).getDocCount()); + assertEquals("{leading=a_11}", result.getBuckets().get(1).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(1).getDocCount()); + assertMap( + debug, + matchesMap().entry( + "name", + matchesMap().entry("sources.leading.segments_dynamic_pruning_used", greaterThanOrEqualTo(1)) + .entry("sources.leading.segments_collected", greaterThanOrEqualTo(1)) + ) + ); + }, + keywordMapping, + fooMapping + ); + } + + public void testWithKeywordGivenNoIndexSortingAndDynamicPruningIsApplicableAndDescendingOrder() throws Exception { + CompositeAggregationBuilder aggregationBuilder = new CompositeAggregationBuilder( + "name", + List.of(new TermsValuesSourceBuilder("leading").field("keyword").order(SortOrder.DESC)) + ).size(2); + final MappedFieldType keywordMapping = new KeywordFieldMapper.KeywordFieldType("keyword"); + final MappedFieldType fooMapping = new KeywordFieldMapper.KeywordFieldType("foo"); + + CheckedConsumer buildIndex = iw -> { + for (int i = 1; i <= 100; i++) { + addDocWithKeywordFields(iw, "keyword", "a_" + i, "foo", "bar"); + } + }; + + debugTestCase( + aggregationBuilder, + new TermQuery(new Term("foo", "bar")), + buildIndex, + (InternalComposite result, Class impl, Map> debug) -> { + assertThat(result.getBuckets(), hasSize(2)); + assertEquals(CompositeAggregator.class, impl); + assertEquals("{leading=a_98}", result.afterKey().toString()); + assertEquals("{leading=a_99}", result.getBuckets().get(0).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(0).getDocCount()); + assertEquals("{leading=a_98}", result.getBuckets().get(1).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(1).getDocCount()); + assertMap( + debug, + matchesMap().entry( + "name", + matchesMap().entry("sources.leading.segments_dynamic_pruning_used", greaterThanOrEqualTo(1)) + .entry("sources.leading.segments_collected", greaterThanOrEqualTo(1)) + ) + ); + }, + keywordMapping, + fooMapping + ); + + aggregationBuilder = new CompositeAggregationBuilder( + "name", + List.of(new TermsValuesSourceBuilder("leading").field("keyword").order(SortOrder.DESC)) + ).size(2).aggregateAfter(Collections.singletonMap("leading", "a_98")); + debugTestCase( + aggregationBuilder, + new TermQuery(new Term("foo", "bar")), + buildIndex, + (InternalComposite result, Class impl, Map> debug) -> { + assertThat(result.getBuckets(), hasSize(2)); + assertEquals(CompositeAggregator.class, impl); + assertEquals("{leading=a_96}", result.afterKey().toString()); + assertEquals("{leading=a_97}", result.getBuckets().get(0).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(0).getDocCount()); + assertEquals("{leading=a_96}", result.getBuckets().get(1).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(1).getDocCount()); + assertMap( + debug, + matchesMap().entry( + "name", + matchesMap().entry("sources.leading.segments_dynamic_pruning_used", greaterThanOrEqualTo(1)) + .entry("sources.leading.segments_collected", greaterThanOrEqualTo(1)) + ) + ); + }, + keywordMapping, + fooMapping + ); + } + + public void testWithKeywordGivenSecondarySourceAndDynamicPruningIsApplicable() throws Exception { + CompositeAggregationBuilder aggregationBuilder = new CompositeAggregationBuilder( + "name", + List.of( + new TermsValuesSourceBuilder("leading_keyword").field("leading_keyword"), + new TermsValuesSourceBuilder("secondary_keyword").field("secondary_keyword") + ) + ).size(2); + final MappedFieldType leadingKeywordMapping = new KeywordFieldMapper.KeywordFieldType("leading_keyword"); + final MappedFieldType secondaryKeywordMapping = new KeywordFieldMapper.KeywordFieldType("secondary_keyword"); + final MappedFieldType fooMapping = new KeywordFieldMapper.KeywordFieldType("foo"); + + CheckedConsumer buildIndex = iw -> { + for (int i = 1; i <= 100; i++) { + addDocWithKeywordFields(iw, "leading_keyword", "a_" + i, "secondary_keyword", "alpha", "foo", "bar"); + addDocWithKeywordFields(iw, "leading_keyword", "a_" + i, "secondary_keyword", "beta", "foo", "bar"); + } + }; + + debugTestCase( + aggregationBuilder, + new TermQuery(new Term("foo", "bar")), + buildIndex, + (InternalComposite result, Class impl, Map> debug) -> { + assertThat(result.getBuckets(), hasSize(2)); + assertEquals(CompositeAggregator.class, impl); + assertEquals("{leading_keyword=a_1, secondary_keyword=beta}", result.afterKey().toString()); + assertEquals("{leading_keyword=a_1, secondary_keyword=alpha}", result.getBuckets().get(0).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(0).getDocCount()); + assertEquals("{leading_keyword=a_1, secondary_keyword=beta}", result.getBuckets().get(1).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(1).getDocCount()); + assertMap( + debug, + matchesMap().entry( + "name", + matchesMap().entry("sources.leading_keyword.segments_dynamic_pruning_used", greaterThanOrEqualTo(1)) + .entry("sources.leading_keyword.segments_collected", greaterThanOrEqualTo(1)) + ) + ); + }, + leadingKeywordMapping, + secondaryKeywordMapping, + fooMapping + ); + + aggregationBuilder = new CompositeAggregationBuilder( + "name", + List.of( + new TermsValuesSourceBuilder("leading_keyword").field("leading_keyword"), + new TermsValuesSourceBuilder("secondary_keyword").field("secondary_keyword") + ) + ).size(2).aggregateAfter(Map.of("leading_keyword", "a_1", "secondary_keyword", "beta")); + debugTestCase( + aggregationBuilder, + new TermQuery(new Term("foo", "bar")), + buildIndex, + (InternalComposite result, Class impl, Map> debug) -> { + assertThat(result.getBuckets(), hasSize(2)); + assertEquals(CompositeAggregator.class, impl); + assertEquals("{leading_keyword=a_10, secondary_keyword=beta}", result.afterKey().toString()); + assertEquals("{leading_keyword=a_10, secondary_keyword=alpha}", result.getBuckets().get(0).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(0).getDocCount()); + assertEquals("{leading_keyword=a_10, secondary_keyword=beta}", result.getBuckets().get(1).getKeyAsString()); + assertEquals(1L, result.getBuckets().get(1).getDocCount()); + assertMap( + debug, + matchesMap().entry( + "name", + matchesMap().entry("sources.leading_keyword.segments_dynamic_pruning_used", greaterThanOrEqualTo(1)) + .entry("sources.leading_keyword.segments_collected", greaterThanOrEqualTo(1)) + ) + ); + }, + leadingKeywordMapping, + secondaryKeywordMapping, + fooMapping + ); + } + + private static void addDocWithKeywordFields(RandomIndexWriter iw, String... fieldValuePairs) throws IOException { + assertThat(fieldValuePairs.length, greaterThan(0)); + assertThat(fieldValuePairs.length % 2, equalTo(0)); + List fields = new ArrayList<>(); + for (int i = 0; i < fieldValuePairs.length; i = i + 2) { + String field = fieldValuePairs[i]; + String value = fieldValuePairs[i + 1]; + fields.add(new StringField(field, value, Field.Store.NO)); + fields.add(new SortedSetDocValuesField(field, new BytesRef(value))); + } + iw.addDocument(fields); + } + private void testSearchCase( List queries, List>> dataset, diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesCollectorQueueTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesCollectorQueueTests.java index c7a396a23965..c78caced15a4 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesCollectorQueueTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeValuesCollectorQueueTests.java @@ -17,6 +17,7 @@ import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.DocValues; import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexReaderContext; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.search.CollectionTerminatedException; @@ -39,6 +40,7 @@ import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.aggregations.AggregatorTestCase; import org.elasticsearch.search.aggregations.LeafBucketCollector; +import org.junit.Before; import java.io.IOException; import java.util.ArrayList; @@ -51,6 +53,8 @@ import static org.elasticsearch.index.mapper.NumberFieldMapper.NumberType.LONG; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class CompositeValuesCollectorQueueTests extends AggregatorTestCase { static class ClassAndName { @@ -63,6 +67,16 @@ static class ClassAndName { } } + private IndexReader indexReader; + + @Before + public void setUpMocks() { + indexReader = mock(IndexReader.class); + IndexReaderContext indexReaderContext = mock(IndexReaderContext.class); + when(indexReaderContext.leaves()).thenReturn(List.of()); + when(indexReader.getContext()).thenReturn(indexReaderContext); + } + public void testRandomLong() throws IOException { testRandomCase(new ClassAndName(createNumber("long", LONG), Long.class)); } @@ -275,6 +289,7 @@ private void testRandomCase(boolean forceMerge, boolean missingBucket, int index sources[i] = new GlobalOrdinalValuesSource( bigArrays, fieldType, + DocValues.getSortedSet(reader.leaves().get(0).reader(), fieldType.name()).getValueCount(), context -> DocValues.getSortedSet(context.reader(), fieldType.name()), DocValueFormat.RAW, missingBucket, @@ -308,7 +323,8 @@ private void testRandomCase(boolean forceMerge, boolean missingBucket, int index final CompositeValuesCollectorQueue queue = new CompositeValuesCollectorQueue( BigArrays.NON_RECYCLING_INSTANCE, sources, - size + size, + indexReader ); if (last != null) { queue.setAfterKey(last); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/SingleDimensionValuesSourceTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/SingleDimensionValuesSourceTests.java index 56b430c4ee66..4f5700cff91d 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/SingleDimensionValuesSourceTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/composite/SingleDimensionValuesSourceTests.java @@ -96,6 +96,7 @@ public void testGlobalOrdinalsSorted() { GlobalOrdinalValuesSource source = new GlobalOrdinalValuesSource( BigArrays.NON_RECYCLING_INSTANCE, keyword, + 0L, context -> null, DocValueFormat.RAW, false, @@ -113,6 +114,7 @@ public void testGlobalOrdinalsSorted() { source = new GlobalOrdinalValuesSource( BigArrays.NON_RECYCLING_INSTANCE, keyword, + 0L, context -> null, DocValueFormat.RAW, true, @@ -127,6 +129,7 @@ public void testGlobalOrdinalsSorted() { source = new GlobalOrdinalValuesSource( BigArrays.NON_RECYCLING_INSTANCE, keyword, + 0L, context -> null, DocValueFormat.RAW, false, @@ -141,6 +144,7 @@ public void testGlobalOrdinalsSorted() { source = new GlobalOrdinalValuesSource( BigArrays.NON_RECYCLING_INSTANCE, ip, + 0L, context -> null, DocValueFormat.RAW, false, From 83d3f7f6f9ac5bdcc30d06d04b11ab0f899bc81c Mon Sep 17 00:00:00 2001 From: David Roberts Date: Wed, 21 Dec 2022 16:26:44 +0000 Subject: [PATCH 334/919] More kibana_system changes to support Fleet transform install (#92462) Changes to support elastic/kibana#142920. Transform destination indices now have a version number appended to their names, so the patterns used to set up the index privileges need to be adjusted accordingly. --- .../authz/store/ReservedRolesStore.java | 10 +-- .../authz/store/ReservedRolesStoreTests.java | 61 ++++++++++--------- 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java index d8889003f366..8512d29f3901 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java @@ -768,7 +768,7 @@ public static RoleDescriptor kibanaSystemRoleDescriptor(String name) { "logs-*", "synthetics-*", "traces-*", - "/metrics-.*&~(metrics-endpoint\\.metadata_current_default)/", + "/metrics-.*&~(metrics-endpoint\\.metadata_current_default.*)/", ".logs-endpoint.action.responses-*", ".logs-endpoint.diagnostic.collection-*", ".logs-endpoint.actions-*", @@ -820,9 +820,9 @@ public static RoleDescriptor kibanaSystemRoleDescriptor(String name) { .build(), RoleDescriptor.IndicesPrivileges.builder() .indices( - "metrics-endpoint.metadata_current_default", - ".metrics-endpoint.metadata_current_default", - ".metrics-endpoint.metadata_united_default" + "metrics-endpoint.metadata_current_default*", + ".metrics-endpoint.metadata_current_default*", + ".metrics-endpoint.metadata_united_default*" ) .privileges("create_index", "delete_index", "read", "index", IndicesAliasesAction.NAME, UpdateSettingsAction.NAME) .build(), @@ -845,7 +845,7 @@ public static RoleDescriptor kibanaSystemRoleDescriptor(String name) { .privileges("read", "view_index_metadata") .build(), RoleDescriptor.IndicesPrivileges.builder() - .indices("logs-cloud_security_posture.findings_latest-default", "logs-cloud_security_posture.scores-default") + .indices("logs-cloud_security_posture.findings_latest-default*", "logs-cloud_security_posture.scores-default*") .privileges("create_index", "read", "index", "delete", IndicesAliasesAction.NAME, UpdateSettingsAction.NAME) .build() }, null, diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java index 71554880cd8c..e3ac7164e956 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java @@ -997,7 +997,10 @@ public void testKibanaSystemRole() { Arrays.asList( "metrics-endpoint.metadata_current_default", ".metrics-endpoint.metadata_current_default", - ".metrics-endpoint.metadata_united_default" + ".metrics-endpoint.metadata_united_default", + "metrics-endpoint.metadata_current_default-" + Version.CURRENT, + ".metrics-endpoint.metadata_current_default-" + Version.CURRENT, + ".metrics-endpoint.metadata_united_default-" + Version.CURRENT ).forEach(indexName -> { logger.info("index name [{}]", indexName); final IndexAbstraction indexAbstraction = mockIndexAbstraction(indexName); @@ -1079,33 +1082,35 @@ public void testKibanaSystemRole() { assertThat(kibanaRole.indices().allowedIndicesMatcher(RolloverAction.NAME).test(indexAbstraction), is(true)); }); - Arrays.asList("logs-cloud_security_posture.findings_latest-default", "logs-cloud_security_posture.scores-default") - .forEach(indexName -> { - logger.info("index name [{}]", indexName); - final IndexAbstraction indexAbstraction = mockIndexAbstraction(indexName); - // Allow indexing - assertThat(kibanaRole.indices().allowedIndicesMatcher(SearchAction.NAME).test(indexAbstraction), is(true)); - assertThat(kibanaRole.indices().allowedIndicesMatcher(GetAction.NAME).test(indexAbstraction), is(true)); - assertThat(kibanaRole.indices().allowedIndicesMatcher(IndexAction.NAME).test(indexAbstraction), is(true)); - assertThat(kibanaRole.indices().allowedIndicesMatcher(UpdateAction.NAME).test(indexAbstraction), is(true)); - assertThat(kibanaRole.indices().allowedIndicesMatcher(BulkAction.NAME).test(indexAbstraction), is(true)); - // Allow create and delete index, modifying aliases, and updating index settings - assertThat(kibanaRole.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(indexAbstraction), is(true)); - assertThat(kibanaRole.indices().allowedIndicesMatcher(AutoCreateAction.NAME).test(indexAbstraction), is(true)); - assertThat(kibanaRole.indices().allowedIndicesMatcher(CreateDataStreamAction.NAME).test(indexAbstraction), is(true)); - assertThat(kibanaRole.indices().allowedIndicesMatcher(GetAliasesAction.NAME).test(indexAbstraction), is(true)); - assertThat(kibanaRole.indices().allowedIndicesMatcher(IndicesAliasesAction.NAME).test(indexAbstraction), is(true)); - assertThat(kibanaRole.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(indexAbstraction), is(true)); - - // Implied by the overall view_index_metadata and monitor privilege - assertViewIndexMetadata(kibanaRole, indexName); - assertThat( - kibanaRole.indices() - .allowedIndicesMatcher("indices:monitor/" + randomAlphaOfLengthBetween(3, 8)) - .test(indexAbstraction), - is(true) - ); - }); + Arrays.asList( + "logs-cloud_security_posture.findings_latest-default", + "logs-cloud_security_posture.scores-default", + "logs-cloud_security_posture.findings_latest-default-" + Version.CURRENT, + "logs-cloud_security_posture.scores-default-" + Version.CURRENT + ).forEach(indexName -> { + logger.info("index name [{}]", indexName); + final IndexAbstraction indexAbstraction = mockIndexAbstraction(indexName); + // Allow indexing + assertThat(kibanaRole.indices().allowedIndicesMatcher(SearchAction.NAME).test(indexAbstraction), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(GetAction.NAME).test(indexAbstraction), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(IndexAction.NAME).test(indexAbstraction), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(UpdateAction.NAME).test(indexAbstraction), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(BulkAction.NAME).test(indexAbstraction), is(true)); + // Allow create and delete index, modifying aliases, and updating index settings + assertThat(kibanaRole.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(indexAbstraction), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(AutoCreateAction.NAME).test(indexAbstraction), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(CreateDataStreamAction.NAME).test(indexAbstraction), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(GetAliasesAction.NAME).test(indexAbstraction), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(IndicesAliasesAction.NAME).test(indexAbstraction), is(true)); + assertThat(kibanaRole.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(indexAbstraction), is(true)); + + // Implied by the overall view_index_metadata and monitor privilege + assertViewIndexMetadata(kibanaRole, indexName); + assertThat( + kibanaRole.indices().allowedIndicesMatcher("indices:monitor/" + randomAlphaOfLengthBetween(3, 8)).test(indexAbstraction), + is(true) + ); + }); // Ensure privileges necessary for ILM policies in APM & Endpoint packages Arrays.asList( From c5ed72e70dab8091e61920d1340e507db3863c21 Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Wed, 21 Dec 2022 19:12:21 +0000 Subject: [PATCH 335/919] [Tests] Timeout 1s when creating a red index (#92486) --- .../resources/rest-api-spec/test/health/40_diagnosis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/40_diagnosis.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/40_diagnosis.yml index 12b38224cbcb..4c87a0ae2786 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/40_diagnosis.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/40_diagnosis.yml @@ -7,6 +7,8 @@ - do: indices.create: index: red_index + master_timeout: 1s + timeout: 1s body: settings: number_of_shards: 1 From 0421a21a6e4354f7701dba2d9c5b2a8043a79061 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Wed, 21 Dec 2022 13:49:15 -0800 Subject: [PATCH 336/919] Use default text for preview warning with tcp readiness port (#92508) The docs for the TCP readiness port have a preview warning, with custom text. However, this doesn't render correctly in the docs. Rather than figure out why the custom text doesn't render, this commit simply removes the custom text in favor of the default preview text. --- docs/reference/setup/advanced-configuration.asciidoc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/reference/setup/advanced-configuration.asciidoc b/docs/reference/setup/advanced-configuration.asciidoc index 0fc562db2f2f..2a7ccc56742d 100644 --- a/docs/reference/setup/advanced-configuration.asciidoc +++ b/docs/reference/setup/advanced-configuration.asciidoc @@ -158,9 +158,7 @@ using the service manager. See <>. [[readiness-tcp-port]] ===== Enable the Elasticsearch TCP readiness port -preview::["This functionality is in technical preview and may be changed or removed in a future release. -It is intended for internal, experimental use. Features in technical preview are not subject to the support -SLA of official GA features."] +preview::[] If configured, a node can open a TCP port when the node is in a ready state. A node is deemed ready when it has successfully joined a cluster. In a single node configuration, the node is From c2eda511de8c793cec480a1a25683b40c0088f66 Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Wed, 21 Dec 2022 15:33:46 -0800 Subject: [PATCH 337/919] Add JUnit rule based integration test cluster orchestration framework (#92379) This commit adds a new test framework for configuring and orchestrating test clusters for both Java and YAML REST testing. This will eventually replace the existing "test-clusters" Gradle plugin and the build-time cluster orchestration. --- build-tools-internal/build.gradle | 14 +- .../AbstractRestResourcesFuncTest.groovy | 1 + ...gConventionsPrecommitPluginFuncTest.groovy | 18 +- ...cyYamlRestCompatTestPluginFuncTest.groovy} | 10 +- ...> LegacyYamlRestTestPluginFuncTest.groovy} | 8 +- .../gradle/internal/doc/DocsTestPlugin.groovy | 2 +- .../internal/ResolveAllDependencies.java | 2 +- .../TestingConventionsPrecommitPlugin.java | 18 +- ...gin.java => LegacyRestTestBasePlugin.java} | 32 +- .../test/StandaloneRestTestPlugin.java | 10 +- .../internal/test/TestWithSslPlugin.java | 6 +- .../test/rest/InternalJavaRestTestPlugin.java | 7 +- .../test/rest/InternalYamlRestTestPlugin.java | 6 +- .../test/rest/LegacyJavaRestTestPlugin.java | 48 ++ .../test/rest/LegacyYamlRestTestPlugin.java | 76 +++ .../test/rest/RestTestBasePlugin.java | 227 ++++++++ .../internal/test/rest/RestTestUtil.java | 22 +- .../AbstractYamlRestCompatTestPlugin.java} | 35 +- .../LegacyYamlRestCompatTestPlugin.java | 44 ++ .../compat/RestCompatTestTransformTask.java | 2 +- .../compat/YamlRestCompatTestPlugin.java | 42 ++ .../StandaloneRestIntegTestTask.java | 5 - .../archives/integ-test-zip/build.gradle | 2 +- distribution/build.gradle | 12 +- distribution/docker/build.gradle | 2 +- modules/aggregations/build.gradle | 4 +- modules/aggs-matrix-stats/build.gradle | 4 +- modules/analysis-common/build.gradle | 4 +- modules/data-streams/build.gradle | 6 +- modules/ingest-attachment/build.gradle | 4 +- modules/ingest-common/build.gradle | 4 +- modules/ingest-geoip/build.gradle | 4 +- .../qa/file-based-update/build.gradle | 2 +- modules/ingest-user-agent/build.gradle | 4 +- modules/kibana/build.gradle | 2 +- modules/lang-expression/build.gradle | 4 +- modules/lang-mustache/build.gradle | 6 +- modules/lang-painless/build.gradle | 4 +- modules/mapper-extras/build.gradle | 4 +- modules/parent-join/build.gradle | 4 +- modules/percolator/build.gradle | 4 +- modules/rank-eval/build.gradle | 4 +- modules/reindex/build.gradle | 6 +- modules/repository-azure/build.gradle | 2 +- modules/repository-gcs/build.gradle | 8 +- modules/repository-s3/build.gradle | 10 +- modules/repository-url/build.gradle | 4 +- modules/runtime-fields-common/build.gradle | 4 +- modules/transport-netty4/build.gradle | 10 +- plugins/analysis-icu/build.gradle | 4 +- plugins/analysis-kuromoji/build.gradle | 4 +- plugins/analysis-nori/build.gradle | 4 +- plugins/analysis-phonetic/build.gradle | 4 +- plugins/analysis-smartcn/build.gradle | 4 +- plugins/analysis-stempel/build.gradle | 4 +- plugins/analysis-ukrainian/build.gradle | 4 +- plugins/discovery-azure-classic/build.gradle | 2 +- plugins/discovery-ec2/build.gradle | 2 +- .../discovery-ec2/qa/amazon-ec2/build.gradle | 6 +- plugins/discovery-gce/build.gradle | 2 +- plugins/discovery-gce/qa/gce/build.gradle | 2 +- plugins/mapper-annotated-text/build.gradle | 4 +- plugins/mapper-murmur3/build.gradle | 4 +- .../size/MapperSizeClientYamlTestSuiteIT.java | 10 + plugins/repository-hdfs/build.gradle | 4 +- plugins/store-smb/build.gradle | 2 +- qa/ccs-unavailable-clusters/build.gradle | 2 +- qa/smoke-test-http/build.gradle | 2 +- qa/smoke-test-ingest-disabled/build.gradle | 2 +- .../build.gradle | 4 +- qa/smoke-test-multinode/build.gradle | 2 +- qa/smoke-test-plugins/build.gradle | 2 +- qa/system-indices/build.gradle | 2 +- qa/unconfigured-node-name/build.gradle | 2 +- rest-api-spec/build.gradle | 5 +- .../test/rest/ClientYamlTestSuiteIT.java | 14 + settings.gradle | 5 +- test/external-modules/build.gradle | 2 +- .../die-with-dignity/build.gradle | 2 +- test/test-clusters/build.gradle | 15 + .../test/cluster/ClusterFactory.java | 14 + .../test/cluster/ClusterHandle.java | 45 ++ .../test/cluster/ClusterSpec.java | 11 + .../test/cluster/ElasticsearchCluster.java | 35 ++ .../test/cluster/EnvironmentProvider.java | 30 ++ .../test/cluster/FeatureFlag.java | 29 + .../test/cluster/MutableSettingsProvider.java | 31 ++ .../test/cluster/SettingsProvider.java | 30 ++ .../local/AbstractLocalSpecBuilder.java | 159 ++++++ .../local/DefaultEnvironmentProvider.java | 39 ++ .../local/DefaultLocalClusterSpecBuilder.java | 152 ++++++ .../local/DefaultSettingsProvider.java | 74 +++ .../local/LocalClusterConfigProvider.java | 14 + .../cluster/local/LocalClusterFactory.java | 503 ++++++++++++++++++ .../cluster/local/LocalClusterHandle.java | 155 ++++++ .../test/cluster/local/LocalClusterSpec.java | 190 +++++++ .../local/LocalClusterSpecBuilder.java | 58 ++ .../local/LocalElasticsearchCluster.java | 81 +++ .../cluster/local/LocalNodeSpecBuilder.java | 17 + .../test/cluster/local/LocalSpecBuilder.java | 57 ++ .../cluster/local/WaitForHttpResource.java | 219 ++++++++ .../DefaultDistributionDescriptor.java | 57 ++ .../distribution/DistributionDescriptor.java | 23 + .../distribution/DistributionResolver.java | 16 + .../local/distribution/DistributionType.java | 24 + .../LocalDistributionResolver.java | 68 +++ .../SnapshotDistributionResolver.java | 19 + .../test/cluster/local/model/User.java | 41 ++ .../test/cluster/util/ExceptionUtils.java | 22 + .../test/cluster/util/IOUtils.java | 199 +++++++ .../elasticsearch/test/cluster/util/OS.java | 79 +++ .../elasticsearch/test/cluster/util/Pair.java | 38 ++ .../test/cluster/util/ProcessReaper.java | 153 ++++++ .../test/cluster/util/ProcessUtils.java | 177 ++++++ .../test/cluster/util/Retry.java | 59 ++ .../test/cluster/util/Version.java | 177 ++++++ .../util/resource/ClasspathTextResource.java | 55 ++ .../util/resource/FileTextResource.java | 32 ++ .../util/resource/StringTextResource.java | 22 + .../cluster/util/resource/TextResource.java | 27 + .../src/main/resources/default_test_roles.yml | 13 + test/yaml-rest-runner/build.gradle | 4 +- .../plugin/async-search/qa/rest/build.gradle | 4 +- .../async-search/qa/security/build.gradle | 2 +- .../plugin/autoscaling/qa/rest/build.gradle | 4 +- x-pack/plugin/build.gradle | 6 +- x-pack/plugin/ccr/qa/rest/build.gradle | 4 +- x-pack/plugin/core/build.gradle | 6 +- .../qa/early-deprecation-rest/build.gradle | 2 +- .../plugin/deprecation/qa/rest/build.gradle | 2 +- .../rest-with-advanced-security/build.gradle | 2 +- .../enrich/qa/rest-with-security/build.gradle | 2 +- x-pack/plugin/enrich/qa/rest/build.gradle | 6 +- x-pack/plugin/eql/qa/correctness/build.gradle | 2 +- x-pack/plugin/eql/qa/mixed-node/build.gradle | 2 +- .../multi-cluster-with-security/build.gradle | 2 +- x-pack/plugin/eql/qa/rest/build.gradle | 6 +- x-pack/plugin/eql/qa/security/build.gradle | 2 +- x-pack/plugin/fleet/build.gradle | 2 +- x-pack/plugin/fleet/qa/rest/build.gradle | 2 +- .../graph/qa/with-security/build.gradle | 2 +- .../qa/idp-rest-tests/build.gradle | 2 +- x-pack/plugin/ilm/qa/multi-node/build.gradle | 2 +- x-pack/plugin/ilm/qa/rest/build.gradle | 4 +- .../plugin/ilm/qa/with-security/build.gradle | 2 +- x-pack/plugin/logstash/build.gradle | 2 +- .../mapper-constant-keyword/build.gradle | 2 +- .../plugin/mapper-unsigned-long/build.gradle | 4 +- x-pack/plugin/mapper-version/build.gradle | 4 +- .../ml/qa/basic-multi-node/build.gradle | 2 +- x-pack/plugin/ml/qa/disabled/build.gradle | 2 +- .../ml/qa/ml-with-security/build.gradle | 2 +- .../qa/native-multi-node-tests/build.gradle | 2 +- .../ml/qa/semantic-search-tests/build.gradle | 2 +- .../ml/qa/single-node-tests/build.gradle | 2 +- .../qa/azure/build.gradle | 2 +- .../qa/gcs/build.gradle | 2 +- .../qa/s3/build.gradle | 2 +- x-pack/plugin/rollup/qa/rest/build.gradle | 4 +- .../qa/azure/build.gradle | 2 +- .../searchable-snapshots/qa/gcs/build.gradle | 2 +- .../searchable-snapshots/qa/hdfs/build.gradle | 2 +- .../qa/minio/build.gradle | 2 +- .../searchable-snapshots/qa/rest/build.gradle | 6 +- .../searchable-snapshots/qa/s3/build.gradle | 2 +- .../searchable-snapshots/qa/url/build.gradle | 2 +- .../qa/basic-enable-security/build.gradle | 2 +- .../plugin/security/qa/jwt-realm/build.gradle | 2 +- .../qa/operator-privileges-tests/build.gradle | 2 +- .../plugin/security/qa/profile/build.gradle | 2 +- .../security/qa/security-basic/build.gradle | 2 +- .../qa/security-disabled/build.gradle | 2 +- .../security/qa/security-trial/build.gradle | 2 +- .../security/qa/service-account/build.gradle | 2 +- .../qa/smoke-test-all-realms/build.gradle | 2 +- .../plugin/security/qa/tls-basic/build.gradle | 2 +- .../shutdown/qa/multi-node/build.gradle | 2 +- .../qa/azure/build.gradle | 2 +- .../qa/fs/build.gradle | 2 +- .../qa/gcs/build.gradle | 2 +- .../qa/license-enforcing/build.gradle | 2 +- .../qa/s3/build.gradle | 2 +- .../qa/azure/build.gradle | 2 +- .../qa/gcs/build.gradle | 2 +- .../qa/hdfs/build.gradle | 2 +- .../qa/minio/build.gradle | 2 +- .../qa/rest/build.gradle | 2 +- .../snapshot-repo-test-kit/qa/s3/build.gradle | 2 +- x-pack/plugin/sql/qa/jdbc/build.gradle | 2 +- x-pack/plugin/sql/qa/mixed-node/build.gradle | 2 +- x-pack/plugin/sql/qa/server/build.gradle | 2 +- .../multi-cluster-with-security/build.gradle | 2 +- x-pack/plugin/stack/qa/rest/build.gradle | 4 +- .../text-structure-with-security/build.gradle | 2 +- .../qa/multi-node-tests/build.gradle | 2 +- .../qa/single-node-tests/build.gradle | 2 +- x-pack/plugin/vector-tile/build.gradle | 2 +- .../vector-tile/qa/multi-cluster/build.gradle | 2 +- x-pack/plugin/watcher/qa/rest/build.gradle | 6 +- .../watcher/qa/with-monitoring/build.gradle | 2 +- .../watcher/qa/with-security/build.gradle | 6 +- x-pack/plugin/wildcard/build.gradle | 2 +- .../build.gradle | 2 +- x-pack/qa/kerberos-tests/build.gradle | 2 +- x-pack/qa/mixed-tier-cluster/build.gradle | 2 +- x-pack/qa/multi-node/build.gradle | 13 +- .../GlobalCheckpointSyncActionIT.java | 19 + .../elasticsearch/multi_node/RollupIT.java | 17 + .../javaRestTest/resources}/roles.yml | 0 x-pack/qa/oidc-op-tests/build.gradle | 2 +- .../password-protected-keystore/build.gradle | 2 +- .../reindex-tests-with-security/build.gradle | 2 +- x-pack/qa/runtime-fields/build.gradle | 2 +- .../runtime-fields/with-security/build.gradle | 2 +- x-pack/qa/saml-idp-tests/build.gradle | 2 +- .../build.gradle | 2 +- .../build.gradle | 2 +- x-pack/qa/smoke-test-plugins-ssl/build.gradle | 2 +- x-pack/qa/smoke-test-plugins/build.gradle | 2 +- .../build.gradle | 2 +- x-pack/qa/third-party/jira/build.gradle | 2 +- x-pack/qa/third-party/pagerduty/build.gradle | 2 +- x-pack/qa/third-party/slack/build.gradle | 2 +- .../qa/xpack-prefix-rest-compat/build.gradle | 2 +- 224 files changed, 4114 insertions(+), 313 deletions(-) rename build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/test/rest/{YamlRestCompatTestPluginFuncTest.groovy => LegacyYamlRestCompatTestPluginFuncTest.groovy} (98%) rename build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/test/rest/{InternalYamlRestTestPluginFuncTest.groovy => LegacyYamlRestTestPluginFuncTest.groovy} (96%) rename build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/{RestTestBasePlugin.java => LegacyRestTestBasePlugin.java} (83%) create mode 100644 build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/LegacyJavaRestTestPlugin.java create mode 100644 build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/LegacyYamlRestTestPlugin.java create mode 100644 build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/RestTestBasePlugin.java rename build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/{rest/compat/YamlRestCompatTestPlugin.java => test/rest/compat/compat/AbstractYamlRestCompatTestPlugin.java} (91%) create mode 100644 build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/compat/compat/LegacyYamlRestCompatTestPlugin.java rename build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/{rest => test/rest/compat}/compat/RestCompatTestTransformTask.java (99%) create mode 100644 build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/compat/compat/YamlRestCompatTestPlugin.java create mode 100644 test/test-clusters/build.gradle create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/ClusterFactory.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/ClusterHandle.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/ClusterSpec.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/ElasticsearchCluster.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/EnvironmentProvider.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/FeatureFlag.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/MutableSettingsProvider.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/SettingsProvider.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/AbstractLocalSpecBuilder.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/DefaultEnvironmentProvider.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/DefaultLocalClusterSpecBuilder.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/DefaultSettingsProvider.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalClusterConfigProvider.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalClusterFactory.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalClusterHandle.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalClusterSpec.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalClusterSpecBuilder.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalElasticsearchCluster.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalNodeSpecBuilder.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalSpecBuilder.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/WaitForHttpResource.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/DefaultDistributionDescriptor.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/DistributionDescriptor.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/DistributionResolver.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/DistributionType.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/LocalDistributionResolver.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/SnapshotDistributionResolver.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/model/User.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/ExceptionUtils.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/IOUtils.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/OS.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/Pair.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/ProcessReaper.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/ProcessUtils.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/Retry.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/Version.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/resource/ClasspathTextResource.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/resource/FileTextResource.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/resource/StringTextResource.java create mode 100644 test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/resource/TextResource.java create mode 100644 test/test-clusters/src/main/resources/default_test_roles.yml rename x-pack/qa/multi-node/{ => src/javaRestTest/resources}/roles.yml (100%) diff --git a/build-tools-internal/build.gradle b/build-tools-internal/build.gradle index 707faf8749c1..92aed9cc9429 100644 --- a/build-tools-internal/build.gradle +++ b/build-tools-internal/build.gradle @@ -115,6 +115,10 @@ gradlePlugin { id = 'elasticsearch.java' implementationClass = 'org.elasticsearch.gradle.internal.ElasticsearchJavaPlugin' } + legacyInternalJavaRestTest { + id = 'elasticsearch.legacy-java-rest-test' + implementationClass = 'org.elasticsearch.gradle.internal.test.rest.LegacyJavaRestTestPlugin' + } internalJavaRestTest { id = 'elasticsearch.internal-java-rest-test' implementationClass = 'org.elasticsearch.gradle.internal.test.rest.InternalJavaRestTestPlugin' @@ -167,9 +171,17 @@ gradlePlugin { id = 'elasticsearch.validate-rest-spec' implementationClass = 'org.elasticsearch.gradle.internal.precommit.ValidateRestSpecPlugin' } + legacyYamlRestCompatTest { + id = 'elasticsearch.legacy-yaml-rest-compat-test' + implementationClass = 'org.elasticsearch.gradle.internal.test.rest.compat.compat.LegacyYamlRestCompatTestPlugin' + } yamlRestCompatTest { id = 'elasticsearch.yaml-rest-compat-test' - implementationClass = 'org.elasticsearch.gradle.internal.rest.compat.YamlRestCompatTestPlugin' + implementationClass = 'org.elasticsearch.gradle.internal.test.rest.compat.compat.YamlRestCompatTestPlugin' + } + legacyYamlRestTest { + id = 'elasticsearch.legacy-yaml-rest-test' + implementationClass = 'org.elasticsearch.gradle.internal.test.rest.LegacyYamlRestTestPlugin' } yamlRestTest { id = 'elasticsearch.internal-yaml-rest-test' diff --git a/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/fixtures/AbstractRestResourcesFuncTest.groovy b/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/fixtures/AbstractRestResourcesFuncTest.groovy index 2fa7ed6faaee..2756b9745bc7 100644 --- a/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/fixtures/AbstractRestResourcesFuncTest.groovy +++ b/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/fixtures/AbstractRestResourcesFuncTest.groovy @@ -13,6 +13,7 @@ abstract class AbstractRestResourcesFuncTest extends AbstractGradleFuncTest { def setup() { subProject(":test:framework") << "apply plugin: 'elasticsearch.java'" + subProject(":test:test-clusters") << "apply plugin: 'elasticsearch.java'" subProject(":test:yaml-rest-runner") << "apply plugin: 'elasticsearch.java'" subProject(":rest-api-spec") << """ diff --git a/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/precommit/TestingConventionsPrecommitPluginFuncTest.groovy b/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/precommit/TestingConventionsPrecommitPluginFuncTest.groovy index 017b7333f8de..31ecffc07f63 100644 --- a/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/precommit/TestingConventionsPrecommitPluginFuncTest.groovy +++ b/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/precommit/TestingConventionsPrecommitPluginFuncTest.groovy @@ -174,12 +174,12 @@ class TestingConventionsPrecommitPluginFuncTest extends AbstractGradleInternalPl given: clazz(dir('src/yamlRestTest/java'), "org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase") buildFile << """ - apply plugin:'elasticsearch.internal-yaml-rest-test' - + apply plugin:'elasticsearch.legacy-yaml-rest-test' + dependencies { yamlRestTestImplementation "org.apache.lucene:tests.util:1.0" yamlRestTestImplementation "org.junit:junit:4.42" - } + } """ clazz(dir("src/yamlRestTest/java"), "org.acme.valid.SomeMatchingIT", "org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase") { @@ -216,11 +216,11 @@ class TestingConventionsPrecommitPluginFuncTest extends AbstractGradleInternalPl buildFile << """ import org.elasticsearch.gradle.internal.precommit.TestingConventionsCheckTask apply plugin:'$pluginName' - + dependencies { ${sourceSetName}Implementation "org.apache.lucene:tests.util:1.0" ${sourceSetName}Implementation "org.junit:junit:4.42" - } + } tasks.withType(TestingConventionsCheckTask).configureEach { suffix 'IT' suffix 'Tests' @@ -252,19 +252,19 @@ class TestingConventionsPrecommitPluginFuncTest extends AbstractGradleInternalPl ) where: - pluginName | taskName | sourceSetName - "elasticsearch.internal-java-rest-test" | ":javaRestTestTestingConventions" | "javaRestTest" + pluginName | taskName | sourceSetName + "elasticsearch.legacy-java-rest-test" | ":javaRestTestTestingConventions" | "javaRestTest" "elasticsearch.internal-cluster-test" | ":internalClusterTestTestingConventions" | "internalClusterTest" } private void simpleJavaBuild() { buildFile << """ apply plugin:'java' - + dependencies { testImplementation "org.apache.lucene:tests.util:1.0" testImplementation "org.junit:junit:4.42" - } + } """ } } diff --git a/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/test/rest/YamlRestCompatTestPluginFuncTest.groovy b/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/test/rest/LegacyYamlRestCompatTestPluginFuncTest.groovy similarity index 98% rename from build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/test/rest/YamlRestCompatTestPluginFuncTest.groovy rename to build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/test/rest/LegacyYamlRestCompatTestPluginFuncTest.groovy index 58f894489ab7..cf0cab3b952f 100644 --- a/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/test/rest/YamlRestCompatTestPluginFuncTest.groovy +++ b/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/test/rest/LegacyYamlRestCompatTestPluginFuncTest.groovy @@ -17,7 +17,7 @@ import org.elasticsearch.gradle.fixtures.AbstractRestResourcesFuncTest import org.elasticsearch.gradle.VersionProperties import org.gradle.testkit.runner.TaskOutcome -class YamlRestCompatTestPluginFuncTest extends AbstractRestResourcesFuncTest { +class LegacyYamlRestCompatTestPluginFuncTest extends AbstractRestResourcesFuncTest { def compatibleVersion = Version.fromString(VersionProperties.getVersions().get("elasticsearch")).getMajor() - 1 def specIntermediateDir = "restResources/v${compatibleVersion}/yamlSpecs" @@ -45,7 +45,7 @@ class YamlRestCompatTestPluginFuncTest extends AbstractRestResourcesFuncTest { buildFile << """ plugins { - id 'elasticsearch.yaml-rest-compat-test' + id 'elasticsearch.legacy-yaml-rest-compat-test' } """ @@ -71,7 +71,7 @@ class YamlRestCompatTestPluginFuncTest extends AbstractRestResourcesFuncTest { """ buildFile << """ - apply plugin: 'elasticsearch.yaml-rest-compat-test' + apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' // avoids a dependency problem in this test, the distribution in use here is inconsequential to the test import org.elasticsearch.gradle.testclusters.TestDistribution; @@ -151,7 +151,7 @@ class YamlRestCompatTestPluginFuncTest extends AbstractRestResourcesFuncTest { buildFile << """ plugins { - id 'elasticsearch.yaml-rest-compat-test' + id 'elasticsearch.legacy-yaml-rest-compat-test' } """ @@ -194,7 +194,7 @@ class YamlRestCompatTestPluginFuncTest extends AbstractRestResourcesFuncTest { """ buildFile << """ - apply plugin: 'elasticsearch.yaml-rest-compat-test' + apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' // avoids a dependency problem in this test, the distribution in use here is inconsequential to the test import org.elasticsearch.gradle.testclusters.TestDistribution; diff --git a/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/test/rest/InternalYamlRestTestPluginFuncTest.groovy b/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/test/rest/LegacyYamlRestTestPluginFuncTest.groovy similarity index 96% rename from build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/test/rest/InternalYamlRestTestPluginFuncTest.groovy rename to build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/test/rest/LegacyYamlRestTestPluginFuncTest.groovy index e70be2b4de4d..1ad86d0174ef 100644 --- a/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/test/rest/InternalYamlRestTestPluginFuncTest.groovy +++ b/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/test/rest/LegacyYamlRestTestPluginFuncTest.groovy @@ -15,7 +15,7 @@ import org.elasticsearch.gradle.fixtures.AbstractRestResourcesFuncTest import org.gradle.testkit.runner.TaskOutcome @IgnoreIf({ os.isWindows() }) -class InternalYamlRestTestPluginFuncTest extends AbstractRestResourcesFuncTest { +class LegacyYamlRestTestPluginFuncTest extends AbstractRestResourcesFuncTest { def "yamlRestTest does nothing when there are no tests"() { given: @@ -23,7 +23,7 @@ class InternalYamlRestTestPluginFuncTest extends AbstractRestResourcesFuncTest { configurationCacheCompatible = false buildFile << """ plugins { - id 'elasticsearch.internal-yaml-rest-test' + id 'elasticsearch.legacy-yaml-rest-test' } """ @@ -42,7 +42,7 @@ class InternalYamlRestTestPluginFuncTest extends AbstractRestResourcesFuncTest { configurationCacheCompatible = false internalBuild() buildFile << """ - apply plugin: 'elasticsearch.internal-yaml-rest-test' + apply plugin: 'elasticsearch.legacy-yaml-rest-test' dependencies { yamlRestTestImplementation "junit:junit:4.12" @@ -96,7 +96,7 @@ class InternalYamlRestTestPluginFuncTest extends AbstractRestResourcesFuncTest { def subProjectBuildFile = subProject(pluginProjectPath) subProjectBuildFile << """ apply plugin: 'elasticsearch.esplugin' - apply plugin: 'elasticsearch.internal-yaml-rest-test' + apply plugin: 'elasticsearch.legacy-yaml-rest-test' dependencies { yamlRestTestImplementation "junit:junit:4.12" diff --git a/build-tools-internal/src/main/groovy/org/elasticsearch/gradle/internal/doc/DocsTestPlugin.groovy b/build-tools-internal/src/main/groovy/org/elasticsearch/gradle/internal/doc/DocsTestPlugin.groovy index f56a9fefceac..874141f2135a 100644 --- a/build-tools-internal/src/main/groovy/org/elasticsearch/gradle/internal/doc/DocsTestPlugin.groovy +++ b/build-tools-internal/src/main/groovy/org/elasticsearch/gradle/internal/doc/DocsTestPlugin.groovy @@ -38,7 +38,7 @@ class DocsTestPlugin implements Plugin { @Override void apply(Project project) { - project.pluginManager.apply('elasticsearch.internal-yaml-rest-test') + project.pluginManager.apply('elasticsearch.legacy-yaml-rest-test') String distribution = System.getProperty('tests.distribution', 'default') // The distribution can be configured with -Dtests.distribution on the command line diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ResolveAllDependencies.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ResolveAllDependencies.java index d86ec9001d41..0afa675c9dfc 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ResolveAllDependencies.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ResolveAllDependencies.java @@ -21,7 +21,7 @@ import javax.inject.Inject; import static org.elasticsearch.gradle.DistributionDownloadPlugin.DISTRO_EXTRACTED_CONFIG_PREFIX; -import static org.elasticsearch.gradle.internal.rest.compat.YamlRestCompatTestPlugin.BWC_MINOR_CONFIG_NAME; +import static org.elasticsearch.gradle.internal.test.rest.compat.compat.LegacyYamlRestCompatTestPlugin.BWC_MINOR_CONFIG_NAME; public class ResolveAllDependencies extends DefaultTask { diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/TestingConventionsPrecommitPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/TestingConventionsPrecommitPlugin.java index 95d0ad2be4cc..6adf422133db 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/TestingConventionsPrecommitPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/TestingConventionsPrecommitPlugin.java @@ -11,7 +11,8 @@ import org.elasticsearch.gradle.internal.conventions.precommit.PrecommitPlugin; import org.elasticsearch.gradle.internal.test.InternalClusterTestPlugin; import org.elasticsearch.gradle.internal.test.rest.InternalJavaRestTestPlugin; -import org.elasticsearch.gradle.internal.test.rest.InternalYamlRestTestPlugin; +import org.elasticsearch.gradle.internal.test.rest.LegacyJavaRestTestPlugin; +import org.elasticsearch.gradle.internal.test.rest.LegacyYamlRestTestPlugin; import org.gradle.api.Action; import org.gradle.api.NamedDomainObjectProvider; import org.gradle.api.Project; @@ -43,8 +44,8 @@ public TaskProvider createTask(Project project) { }); }); - project.getPlugins().withType(InternalYamlRestTestPlugin.class, yamlRestTestPlugin -> { - NamedDomainObjectProvider sourceSet = sourceSets.named(InternalYamlRestTestPlugin.SOURCE_SET_NAME); + project.getPlugins().withType(LegacyYamlRestTestPlugin.class, yamlRestTestPlugin -> { + NamedDomainObjectProvider sourceSet = sourceSets.named(LegacyYamlRestTestPlugin.SOURCE_SET_NAME); setupTaskForSourceSet(project, sourceSet, t -> { t.getSuffixes().convention(List.of("IT")); t.getBaseClasses().convention(List.of("org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase")); @@ -68,8 +69,17 @@ public TaskProvider createTask(Project project) { }); }); + project.getPlugins().withType(LegacyJavaRestTestPlugin.class, javaRestTestPlugin -> { + NamedDomainObjectProvider sourceSet = sourceSets.named(LegacyJavaRestTestPlugin.SOURCE_SET_NAME); + setupTaskForSourceSet(project, sourceSet, t -> { + t.getSuffixes().convention(List.of("IT")); + t.getBaseClasses() + .convention(List.of("org.elasticsearch.test.ESIntegTestCase", "org.elasticsearch.test.rest.ESRestTestCase")); + }); + }); + project.getPlugins().withType(InternalJavaRestTestPlugin.class, javaRestTestPlugin -> { - NamedDomainObjectProvider sourceSet = sourceSets.named(InternalJavaRestTestPlugin.SOURCE_SET_NAME); + NamedDomainObjectProvider sourceSet = sourceSets.named(LegacyJavaRestTestPlugin.SOURCE_SET_NAME); setupTaskForSourceSet(project, sourceSet, t -> { t.getSuffixes().convention(List.of("IT")); t.getBaseClasses() diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/RestTestBasePlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/LegacyRestTestBasePlugin.java similarity index 83% rename from build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/RestTestBasePlugin.java rename to build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/LegacyRestTestBasePlugin.java index fc27bfa43798..305af0865b9e 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/RestTestBasePlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/LegacyRestTestBasePlugin.java @@ -31,7 +31,11 @@ import static org.elasticsearch.gradle.plugin.BasePluginBuildPlugin.BUNDLE_PLUGIN_TASK_NAME; import static org.elasticsearch.gradle.plugin.BasePluginBuildPlugin.EXPLODED_BUNDLE_PLUGIN_TASK_NAME; -public class RestTestBasePlugin implements Plugin { +/** + * @deprecated use {@link RestTestBasePlugin} instead + */ +@Deprecated +public class LegacyRestTestBasePlugin implements Plugin { private static final String TESTS_REST_CLUSTER = "tests.rest.cluster"; private static final String TESTS_CLUSTER = "tests.cluster"; private static final String TESTS_CLUSTER_NAME = "tests.clustername"; @@ -40,7 +44,7 @@ public class RestTestBasePlugin implements Plugin { private ProviderFactory providerFactory; @Inject - public RestTestBasePlugin(ProviderFactory providerFactory) { + public LegacyRestTestBasePlugin(ProviderFactory providerFactory) { this.providerFactory = providerFactory; } @@ -87,17 +91,19 @@ public void apply(Project project) { .withType(StandaloneRestIntegTestTask.class) .configureEach(t -> t.finalizedBy(project.getTasks().withType(FixtureStop.class))); - project.getTasks().withType(StandaloneRestIntegTestTask.class).configureEach(t -> - // if this a module or plugin, it may have an associated zip file with it's contents, add that to the test cluster - project.getPluginManager().withPlugin("elasticsearch.esplugin", plugin -> { - if (GradleUtils.isModuleProject(project.getPath())) { - var bundle = project.getTasks().withType(Sync.class).named(EXPLODED_BUNDLE_PLUGIN_TASK_NAME); - t.getClusters().forEach(c -> c.module(bundle)); - } else { - var bundle = project.getTasks().withType(Zip.class).named(BUNDLE_PLUGIN_TASK_NAME); - t.getClusters().forEach(c -> c.plugin(bundle)); - } - })); + project.getTasks().withType(StandaloneRestIntegTestTask.class).configureEach(t -> { + t.setMaxParallelForks(1); + // if this a module or plugin, it may have an associated zip file with it's contents, add that to the test cluster + project.getPluginManager().withPlugin("elasticsearch.esplugin", plugin -> { + if (GradleUtils.isModuleProject(project.getPath())) { + var bundle = project.getTasks().withType(Sync.class).named(EXPLODED_BUNDLE_PLUGIN_TASK_NAME); + t.getClusters().forEach(c -> c.module(bundle)); + } else { + var bundle = project.getTasks().withType(Zip.class).named(BUNDLE_PLUGIN_TASK_NAME); + t.getClusters().forEach(c -> c.plugin(bundle)); + } + }); + }); } private String systemProperty(String propName) { diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/StandaloneRestTestPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/StandaloneRestTestPlugin.java index 9ffaf396e7fb..163df6cc40e1 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/StandaloneRestTestPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/StandaloneRestTestPlugin.java @@ -11,8 +11,8 @@ import org.elasticsearch.gradle.internal.ExportElasticsearchBuildResourcesTask; import org.elasticsearch.gradle.internal.info.GlobalBuildInfoPlugin; import org.elasticsearch.gradle.internal.precommit.InternalPrecommitTasks; -import org.elasticsearch.gradle.internal.test.rest.InternalJavaRestTestPlugin; -import org.elasticsearch.gradle.internal.test.rest.InternalYamlRestTestPlugin; +import org.elasticsearch.gradle.internal.test.rest.LegacyJavaRestTestPlugin; +import org.elasticsearch.gradle.internal.test.rest.LegacyYamlRestTestPlugin; import org.elasticsearch.gradle.internal.test.rest.RestTestUtil; import org.gradle.api.InvalidUserDataException; import org.gradle.api.Plugin; @@ -32,8 +32,8 @@ * and run REST tests. Use BuildPlugin if you want to build main code as well * as tests. * - * @deprecated use {@link InternalClusterTestPlugin}, {@link InternalJavaRestTestPlugin} or - * {@link InternalYamlRestTestPlugin} instead. + * @deprecated use {@link InternalClusterTestPlugin}, {@link LegacyJavaRestTestPlugin} or + * {@link LegacyYamlRestTestPlugin} instead. */ @Deprecated public class StandaloneRestTestPlugin implements Plugin { @@ -46,7 +46,7 @@ public void apply(final Project project) { } project.getRootProject().getPluginManager().apply(GlobalBuildInfoPlugin.class); - project.getPluginManager().apply(RestTestBasePlugin.class); + project.getPluginManager().apply(LegacyRestTestBasePlugin.class); project.getTasks().register("buildResources", ExportElasticsearchBuildResourcesTask.class); diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/TestWithSslPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/TestWithSslPlugin.java index 9eb2efc77835..524f3dfedf95 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/TestWithSslPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/TestWithSslPlugin.java @@ -14,7 +14,7 @@ import org.elasticsearch.gradle.internal.precommit.FilePermissionsPrecommitPlugin; import org.elasticsearch.gradle.internal.precommit.ForbiddenPatternsPrecommitPlugin; import org.elasticsearch.gradle.internal.precommit.ForbiddenPatternsTask; -import org.elasticsearch.gradle.internal.test.rest.InternalJavaRestTestPlugin; +import org.elasticsearch.gradle.internal.test.rest.LegacyJavaRestTestPlugin; import org.elasticsearch.gradle.testclusters.ElasticsearchCluster; import org.elasticsearch.gradle.testclusters.TestClustersAware; import org.elasticsearch.gradle.testclusters.TestClustersPlugin; @@ -62,8 +62,8 @@ public void apply(Project project) { .withType(RestIntegTestTask.class) .configureEach(runner -> runner.systemProperty("tests.ssl.enabled", "true")); }); - project.getPlugins().withType(InternalJavaRestTestPlugin.class).configureEach(restTestPlugin -> { - SourceSet testSourceSet = Util.getJavaSourceSets(project).getByName(InternalJavaRestTestPlugin.SOURCE_SET_NAME); + project.getPlugins().withType(LegacyJavaRestTestPlugin.class).configureEach(restTestPlugin -> { + SourceSet testSourceSet = Util.getJavaSourceSets(project).getByName(LegacyJavaRestTestPlugin.SOURCE_SET_NAME); testSourceSet.getResources().srcDir(new File(keyStoreDir, "test/ssl")); project.getTasks().named(testSourceSet.getProcessResourcesTaskName()).configure(t -> t.dependsOn(exportKeyStore)); project.getTasks().withType(TestClustersAware.class).configureEach(clusterAware -> clusterAware.dependsOn(exportKeyStore)); diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/InternalJavaRestTestPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/InternalJavaRestTestPlugin.java index 9dda731f7211..d18505ca4b11 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/InternalJavaRestTestPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/InternalJavaRestTestPlugin.java @@ -8,7 +8,7 @@ package org.elasticsearch.gradle.internal.test.rest; -import org.elasticsearch.gradle.internal.test.RestTestBasePlugin; +import org.elasticsearch.gradle.testclusters.StandaloneRestIntegTestTask; import org.elasticsearch.gradle.util.GradleUtils; import org.gradle.api.Plugin; import org.gradle.api.Project; @@ -33,8 +33,11 @@ public void apply(Project project) { SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); SourceSet javaTestSourceSet = sourceSets.create(SOURCE_SET_NAME); + project.getDependencies().add(javaTestSourceSet.getImplementationConfigurationName(), project.project(":test:test-clusters")); + // setup the javaRestTest task - registerTestTask(project, javaTestSourceSet); + // we use a StandloneRestIntegTestTask here so that the conventions of RestTestBasePlugin don't create a test cluster + registerTestTask(project, javaTestSourceSet, SOURCE_SET_NAME, StandaloneRestIntegTestTask.class); // setup dependencies setupJavaRestTestDependenciesDefaults(project, javaTestSourceSet); diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/InternalYamlRestTestPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/InternalYamlRestTestPlugin.java index dacf119994f4..b0fd142705a0 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/InternalYamlRestTestPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/InternalYamlRestTestPlugin.java @@ -8,7 +8,7 @@ package org.elasticsearch.gradle.internal.test.rest; -import org.elasticsearch.gradle.internal.test.RestTestBasePlugin; +import org.elasticsearch.gradle.testclusters.StandaloneRestIntegTestTask; import org.elasticsearch.gradle.util.GradleUtils; import org.gradle.api.Plugin; import org.gradle.api.Project; @@ -34,10 +34,10 @@ public void apply(Project project) { SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); SourceSet yamlTestSourceSet = sourceSets.create(SOURCE_SET_NAME); - registerTestTask(project, yamlTestSourceSet); + registerTestTask(project, yamlTestSourceSet, SOURCE_SET_NAME, StandaloneRestIntegTestTask.class); // setup the dependencies - setupYamlRestTestDependenciesDefaults(project, yamlTestSourceSet); + setupYamlRestTestDependenciesDefaults(project, yamlTestSourceSet, true); // setup the copy for the rest resources project.getTasks().withType(CopyRestApiTask.class).configureEach(copyRestApiTask -> { diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/LegacyJavaRestTestPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/LegacyJavaRestTestPlugin.java new file mode 100644 index 000000000000..a1b17c110b34 --- /dev/null +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/LegacyJavaRestTestPlugin.java @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.gradle.internal.test.rest; + +import org.elasticsearch.gradle.internal.test.LegacyRestTestBasePlugin; +import org.elasticsearch.gradle.util.GradleUtils; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; + +import static org.elasticsearch.gradle.internal.test.rest.RestTestUtil.registerTestTask; +import static org.elasticsearch.gradle.internal.test.rest.RestTestUtil.setupJavaRestTestDependenciesDefaults; + +/** + * Apply this plugin to run the Java based REST tests. + * + * @deprecated use {@link InternalJavaRestTestPlugin} + */ +@Deprecated +public class LegacyJavaRestTestPlugin implements Plugin { + + public static final String SOURCE_SET_NAME = "javaRestTest"; + + @Override + public void apply(Project project) { + project.getPluginManager().apply(LegacyRestTestBasePlugin.class); + + // create source set + SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); + SourceSet javaTestSourceSet = sourceSets.create(SOURCE_SET_NAME); + + // setup the javaRestTest task + registerTestTask(project, javaTestSourceSet); + + // setup dependencies + setupJavaRestTestDependenciesDefaults(project, javaTestSourceSet); + + // setup IDE + GradleUtils.setupIdeForTestSourceSet(project, javaTestSourceSet); + } +} diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/LegacyYamlRestTestPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/LegacyYamlRestTestPlugin.java new file mode 100644 index 000000000000..4977c0924efb --- /dev/null +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/LegacyYamlRestTestPlugin.java @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.gradle.internal.test.rest; + +import org.elasticsearch.gradle.internal.test.LegacyRestTestBasePlugin; +import org.elasticsearch.gradle.util.GradleUtils; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; + +import static org.elasticsearch.gradle.internal.test.rest.RestTestUtil.registerTestTask; +import static org.elasticsearch.gradle.internal.test.rest.RestTestUtil.setupYamlRestTestDependenciesDefaults; + +/** + * Apply this plugin to run the YAML based REST tests. + * + * @deprecated use {@link InternalYamlRestTestPlugin} + */ +@Deprecated +public class LegacyYamlRestTestPlugin implements Plugin { + + public static final String SOURCE_SET_NAME = "yamlRestTest"; + + @Override + public void apply(Project project) { + project.getPluginManager().apply(LegacyRestTestBasePlugin.class); + project.getPluginManager().apply(RestResourcesPlugin.class); + + // create source set + SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); + SourceSet yamlTestSourceSet = sourceSets.create(SOURCE_SET_NAME); + + registerTestTask(project, yamlTestSourceSet); + + // setup the dependencies + setupYamlRestTestDependenciesDefaults(project, yamlTestSourceSet); + + // setup the copy for the rest resources + project.getTasks().withType(CopyRestApiTask.class).configureEach(copyRestApiTask -> { + copyRestApiTask.setSourceResourceDir( + yamlTestSourceSet.getResources() + .getSrcDirs() + .stream() + .filter(f -> f.isDirectory() && f.getName().equals("resources")) + .findFirst() + .orElse(null) + ); + }); + + // Register rest resources with source set + yamlTestSourceSet.getOutput() + .dir( + project.getTasks() + .withType(CopyRestApiTask.class) + .named(RestResourcesPlugin.COPY_REST_API_SPECS_TASK) + .flatMap(CopyRestApiTask::getOutputResourceDir) + ); + + yamlTestSourceSet.getOutput() + .dir( + project.getTasks() + .withType(CopyRestTestsTask.class) + .named(RestResourcesPlugin.COPY_YAML_TESTS_TASK) + .flatMap(CopyRestTestsTask::getOutputResourceDir) + ); + + GradleUtils.setupIdeForTestSourceSet(project, yamlTestSourceSet); + } +} diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/RestTestBasePlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/RestTestBasePlugin.java new file mode 100644 index 000000000000..4e781b3bf5ad --- /dev/null +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/RestTestBasePlugin.java @@ -0,0 +1,227 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.gradle.internal.test.rest; + +import groovy.lang.Closure; + +import org.elasticsearch.gradle.Architecture; +import org.elasticsearch.gradle.DistributionDownloadPlugin; +import org.elasticsearch.gradle.ElasticsearchDistribution; +import org.elasticsearch.gradle.VersionProperties; +import org.elasticsearch.gradle.distribution.ElasticsearchDistributionTypes; +import org.elasticsearch.gradle.internal.ElasticsearchJavaPlugin; +import org.elasticsearch.gradle.internal.InternalDistributionDownloadPlugin; +import org.elasticsearch.gradle.internal.info.BuildParams; +import org.elasticsearch.gradle.plugin.PluginBuildPlugin; +import org.elasticsearch.gradle.plugin.PluginPropertiesExtension; +import org.elasticsearch.gradle.test.SystemPropertyCommandLineArgumentProvider; +import org.elasticsearch.gradle.testclusters.StandaloneRestIntegTestTask; +import org.elasticsearch.gradle.transform.UnzipTransform; +import org.elasticsearch.gradle.util.GradleUtils; +import org.gradle.api.Action; +import org.gradle.api.NamedDomainObjectContainer; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.ProjectDependency; +import org.gradle.api.artifacts.type.ArtifactTypeDefinition; +import org.gradle.api.file.FileTree; +import org.gradle.api.plugins.JavaBasePlugin; +import org.gradle.api.provider.ProviderFactory; +import org.gradle.api.tasks.ClasspathNormalizer; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.util.PatternFilterable; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.inject.Inject; + +/** + * Base plugin used for wiring up build tasks to REST testing tasks using new JUnit rule-based test clusters framework. + */ +public class RestTestBasePlugin implements Plugin { + + private static final String TESTS_RUNTIME_JAVA_SYSPROP = "tests.runtime.java"; + private static final String DEFAULT_DISTRIBUTION_SYSPROP = "tests.default.distribution"; + private static final String INTEG_TEST_DISTRIBUTION_SYSPROP = "tests.integ-test.distribution"; + private static final String TESTS_CLUSTER_MODULES_PATH_SYSPROP = "tests.cluster.modules.path"; + private static final String TESTS_CLUSTER_PLUGINS_PATH_SYSPROP = "tests.cluster.plugins.path"; + private static final String DEFAULT_REST_INTEG_TEST_DISTRO = "default_distro"; + private static final String INTEG_TEST_REST_INTEG_TEST_DISTRO = "integ_test_distro"; + private static final String MODULES_CONFIGURATION = "clusterModules"; + private static final String PLUGINS_CONFIGURATION = "clusterPlugins"; + private static final String EXTRACTED_PLUGINS_CONFIGURATION = "extractedPlugins"; + + private final ProviderFactory providerFactory; + + @Inject + public RestTestBasePlugin(ProviderFactory providerFactory) { + this.providerFactory = providerFactory; + } + + @Override + public void apply(Project project) { + project.getPluginManager().apply(ElasticsearchJavaPlugin.class); + project.getPluginManager().apply(InternalDistributionDownloadPlugin.class); + + // Register integ-test and default distributions + NamedDomainObjectContainer distributions = DistributionDownloadPlugin.getContainer(project); + ElasticsearchDistribution defaultDistro = distributions.create(DEFAULT_REST_INTEG_TEST_DISTRO, distro -> { + distro.setVersion(VersionProperties.getElasticsearch()); + distro.setArchitecture(Architecture.current()); + }); + ElasticsearchDistribution integTestDistro = distributions.create(INTEG_TEST_REST_INTEG_TEST_DISTRO, distro -> { + distro.setVersion(VersionProperties.getElasticsearch()); + distro.setArchitecture(Architecture.current()); + distro.setType(ElasticsearchDistributionTypes.INTEG_TEST_ZIP); + }); + + // Create configures for module and plugin dependencies + Configuration modulesConfiguration = createPluginConfiguration(project, MODULES_CONFIGURATION, true); + Configuration pluginsConfiguration = createPluginConfiguration(project, PLUGINS_CONFIGURATION, false); + Configuration extractedPluginsConfiguration = createPluginConfiguration(project, EXTRACTED_PLUGINS_CONFIGURATION, true); + extractedPluginsConfiguration.extendsFrom(pluginsConfiguration); + configureArtifactTransforms(project); + + // For plugin and module projects, register the current project plugin bundle as a dependency + project.getPluginManager().withPlugin("elasticsearch.esplugin", plugin -> { + if (GradleUtils.isModuleProject(project.getPath())) { + project.getDependencies() + .add(modulesConfiguration.getName(), project.getDependencies().project(Map.of("path", project.getPath()))); + } else { + project.getDependencies().add(pluginsConfiguration.getName(), project.files(project.getTasks().named("bundlePlugin"))); + } + + }); + + project.getTasks().withType(StandaloneRestIntegTestTask.class, task -> { + SystemPropertyCommandLineArgumentProvider nonInputSystemProperties = task.getExtensions() + .getByType(SystemPropertyCommandLineArgumentProvider.class); + + task.dependsOn(integTestDistro, modulesConfiguration); + registerDistributionInputs(task, integTestDistro); + + task.setMaxParallelForks(task.getProject().getGradle().getStartParameter().getMaxWorkerCount() / 2); + + // Disable the security manager and syscall filter since the test framework needs to fork processes + task.systemProperty("tests.security.manager", "false"); + task.systemProperty("tests.system_call_filter", "false"); + + // Register plugins and modules as task inputs and pass paths as system properties to tests + nonInputSystemProperties.systemProperty(TESTS_CLUSTER_MODULES_PATH_SYSPROP, modulesConfiguration::getAsPath); + registerConfigurationInputs(task, modulesConfiguration); + nonInputSystemProperties.systemProperty(TESTS_CLUSTER_PLUGINS_PATH_SYSPROP, pluginsConfiguration::getAsPath); + registerConfigurationInputs(task, extractedPluginsConfiguration); + + // Wire up integ-test distribution by default for all test tasks + nonInputSystemProperties.systemProperty( + INTEG_TEST_DISTRIBUTION_SYSPROP, + () -> integTestDistro.getExtracted().getSingleFile().getPath() + ); + nonInputSystemProperties.systemProperty(TESTS_RUNTIME_JAVA_SYSPROP, BuildParams.getRuntimeJavaHome()); + + // Add `usesDefaultDistribution()` extension method to test tasks to indicate they require the default distro + task.getExtensions().getExtraProperties().set("usesDefaultDistribution", new Closure(task) { + @Override + public Void call(Object... args) { + task.dependsOn(defaultDistro); + registerDistributionInputs(task, defaultDistro); + + nonInputSystemProperties.systemProperty( + DEFAULT_DISTRIBUTION_SYSPROP, + providerFactory.provider(() -> defaultDistro.getExtracted().getSingleFile().getPath()) + ); + return null; + } + }); + }); + + project.getTasks() + .named(JavaBasePlugin.CHECK_TASK_NAME) + .configure(check -> check.dependsOn(project.getTasks().withType(StandaloneRestIntegTestTask.class))); + } + + private FileTree getDistributionFiles(ElasticsearchDistribution distribution, Action patternFilter) { + return distribution.getExtracted().getAsFileTree().matching(patternFilter); + } + + private void registerConfigurationInputs(Task task, Configuration configuration) { + task.getInputs() + .files(providerFactory.provider(() -> configuration.getAsFileTree().filter(f -> f.getName().endsWith(".jar")))) + .withPropertyName(configuration.getName() + "-classpath") + .withNormalizer(ClasspathNormalizer.class); + + task.getInputs() + .files(providerFactory.provider(() -> configuration.getAsFileTree().filter(f -> f.getName().endsWith(".jar") == false))) + .withPropertyName(configuration.getName() + "-files") + .withPathSensitivity(PathSensitivity.RELATIVE); + } + + private void registerDistributionInputs(Task task, ElasticsearchDistribution distribution) { + task.getInputs() + .files(providerFactory.provider(() -> getDistributionFiles(distribution, filter -> filter.exclude("**/*.jar")))) + .withPropertyName(distribution.getName() + "-files") + .withPathSensitivity(PathSensitivity.RELATIVE); + + task.getInputs() + .files(providerFactory.provider(() -> getDistributionFiles(distribution, filter -> filter.include("**/*.jar")))) + .withPropertyName(distribution.getName() + "-classpath") + .withNormalizer(ClasspathNormalizer.class); + } + + private Optional findModulePath(Project project, String pluginName) { + return project.getRootProject() + .getAllprojects() + .stream() + .filter(p -> GradleUtils.isModuleProject(p.getPath())) + .filter(p -> p.getPlugins().hasPlugin(PluginBuildPlugin.class)) + .filter(p -> p.getExtensions().getByType(PluginPropertiesExtension.class).getName().equals(pluginName)) + .findFirst() + .map(Project::getPath); + } + + private Configuration createPluginConfiguration(Project project, String name, boolean useExploded) { + return project.getConfigurations().create(name, c -> { + if (useExploded) { + c.attributes(a -> a.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.DIRECTORY_TYPE)); + } else { + c.attributes(a -> a.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.ZIP_TYPE)); + } + c.withDependencies(dependencies -> { + // Add dependencies of any modules + for (Dependency dependency : dependencies) { + if (dependency instanceof ProjectDependency projectDependency) { + List extendedPlugins = projectDependency.getDependencyProject() + .getExtensions() + .getByType(PluginPropertiesExtension.class) + .getExtendedPlugins(); + + for (String extendedPlugin : extendedPlugins) { + findModulePath(project, extendedPlugin).ifPresent( + modulePath -> dependencies.add(project.getDependencies().project(Map.of("path", modulePath))) + ); + } + } + } + }); + }); + } + + private void configureArtifactTransforms(Project project) { + project.getDependencies().registerTransform(UnzipTransform.class, transformSpec -> { + transformSpec.getFrom().attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.ZIP_TYPE); + transformSpec.getTo().attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.DIRECTORY_TYPE); + transformSpec.getParameters().setAsFiletreeOutput(false); + }); + } +} diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/RestTestUtil.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/RestTestUtil.java index 922157333d80..99c25b9e2570 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/RestTestUtil.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/RestTestUtil.java @@ -15,6 +15,7 @@ import org.gradle.api.provider.Provider; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.testing.Test; /** * Utility class to configure the necessary tasks and dependencies. @@ -34,8 +35,17 @@ public static Provider registerTestTask(Project project, Sour * Creates a {@link RestIntegTestTask} task with a custom name for the provided source set */ public static TaskProvider registerTestTask(Project project, SourceSet sourceSet, String taskName) { + return registerTestTask(project, sourceSet, taskName, RestIntegTestTask.class); + } + + /** + * Creates a {@link T} task with a custom name for the provided source set + * + * @param test task type + */ + public static TaskProvider registerTestTask(Project project, SourceSet sourceSet, String taskName, Class clazz) { // lazily create the test task - return project.getTasks().register(taskName, RestIntegTestTask.class, testTask -> { + return project.getTasks().register(taskName, clazz, testTask -> { testTask.setGroup(JavaBasePlugin.VERIFICATION_GROUP); testTask.setDescription("Runs the REST tests against an external cluster"); project.getPlugins().withType(JavaPlugin.class, t -> testTask.mustRunAfter(project.getTasks().named("test"))); @@ -49,10 +59,20 @@ public static TaskProvider registerTestTask(Project project, * Setup the dependencies needed for the YAML REST tests. */ public static void setupYamlRestTestDependenciesDefaults(Project project, SourceSet sourceSet) { + setupYamlRestTestDependenciesDefaults(project, sourceSet, false); + } + + /** + * Setup the dependencies needed for the YAML REST tests. + */ + public static void setupYamlRestTestDependenciesDefaults(Project project, SourceSet sourceSet, boolean useNewTestClusters) { Project yamlTestRunnerProject = project.findProject(":test:yaml-rest-runner"); // we shield the project dependency to make integration tests easier if (yamlTestRunnerProject != null) { project.getDependencies().add(sourceSet.getImplementationConfigurationName(), yamlTestRunnerProject); + if (useNewTestClusters) { + project.getDependencies().add(sourceSet.getImplementationConfigurationName(), project.project(":test:test-clusters")); + } } } diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rest/compat/YamlRestCompatTestPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/compat/compat/AbstractYamlRestCompatTestPlugin.java similarity index 91% rename from build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rest/compat/YamlRestCompatTestPlugin.java rename to build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/compat/compat/AbstractYamlRestCompatTestPlugin.java index bb245bec61b9..273a0a379318 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rest/compat/YamlRestCompatTestPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/compat/compat/AbstractYamlRestCompatTestPlugin.java @@ -6,19 +6,17 @@ * Side Public License, v 1. */ -package org.elasticsearch.gradle.internal.rest.compat; +package org.elasticsearch.gradle.internal.test.rest.compat.compat; import org.elasticsearch.gradle.Version; import org.elasticsearch.gradle.VersionProperties; import org.elasticsearch.gradle.internal.ElasticsearchJavaBasePlugin; -import org.elasticsearch.gradle.internal.test.RestIntegTestTask; -import org.elasticsearch.gradle.internal.test.RestTestBasePlugin; +import org.elasticsearch.gradle.internal.test.LegacyRestTestBasePlugin; import org.elasticsearch.gradle.internal.test.rest.CopyRestApiTask; import org.elasticsearch.gradle.internal.test.rest.CopyRestTestsTask; -import org.elasticsearch.gradle.internal.test.rest.InternalYamlRestTestPlugin; +import org.elasticsearch.gradle.internal.test.rest.LegacyYamlRestTestPlugin; import org.elasticsearch.gradle.internal.test.rest.RestResourcesExtension; import org.elasticsearch.gradle.internal.test.rest.RestResourcesPlugin; -import org.elasticsearch.gradle.internal.test.rest.RestTestUtil; import org.elasticsearch.gradle.testclusters.TestClustersPlugin; import org.elasticsearch.gradle.util.GradleUtils; import org.gradle.api.Plugin; @@ -37,6 +35,7 @@ import org.gradle.api.tasks.SourceSetContainer; import org.gradle.api.tasks.Sync; import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.testing.Test; import java.io.File; import java.nio.file.Path; @@ -50,7 +49,7 @@ /** * Apply this plugin to run the YAML based REST tests from a prior major version against this version's cluster. */ -public class YamlRestCompatTestPlugin implements Plugin { +public abstract class AbstractYamlRestCompatTestPlugin implements Plugin { public static final String BWC_MINOR_CONFIG_NAME = "bwcMinor"; private static final String REST_COMPAT_CHECK_TASK_NAME = "checkRestCompat"; private static final String COMPATIBILITY_APIS_CONFIGURATION = "restCompatSpecs"; @@ -67,7 +66,7 @@ public class YamlRestCompatTestPlugin implements Plugin { private FileOperations fileOperations; @Inject - public YamlRestCompatTestPlugin(ProjectLayout projectLayout, FileOperations fileOperations) { + public AbstractYamlRestCompatTestPlugin(ProjectLayout projectLayout, FileOperations fileOperations) { this.projectLayout = projectLayout; this.fileOperations = fileOperations; } @@ -81,17 +80,17 @@ public void apply(Project project) { project.getPluginManager().apply(ElasticsearchJavaBasePlugin.class); project.getPluginManager().apply(TestClustersPlugin.class); - project.getPluginManager().apply(RestTestBasePlugin.class); + project.getPluginManager().apply(LegacyRestTestBasePlugin.class); project.getPluginManager().apply(RestResourcesPlugin.class); - project.getPluginManager().apply(InternalYamlRestTestPlugin.class); + project.getPluginManager().apply(getBasePlugin()); RestResourcesExtension extension = project.getExtensions().getByType(RestResourcesExtension.class); // create source set SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); SourceSet yamlCompatTestSourceSet = sourceSets.create(SOURCE_SET_NAME); - SourceSet yamlTestSourceSet = sourceSets.getByName(InternalYamlRestTestPlugin.SOURCE_SET_NAME); - GradleUtils.extendSourceSet(project, InternalYamlRestTestPlugin.SOURCE_SET_NAME, SOURCE_SET_NAME); + SourceSet yamlTestSourceSet = sourceSets.getByName(LegacyYamlRestTestPlugin.SOURCE_SET_NAME); + GradleUtils.extendSourceSet(project, LegacyYamlRestTestPlugin.SOURCE_SET_NAME, SOURCE_SET_NAME); // copy compatible rest specs Configuration bwcMinorConfig = project.getConfigurations().create(BWC_MINOR_CONFIG_NAME); @@ -217,11 +216,9 @@ public void apply(Project project) { .named(RestResourcesPlugin.COPY_YAML_TESTS_TASK) .flatMap(CopyRestTestsTask::getOutputResourceDir); - String testTaskName = "yamlRestTestV" + COMPATIBLE_VERSION + "CompatTest"; - // setup the test task - Provider yamlRestCompatTestTask = RestTestUtil.registerTestTask(project, yamlCompatTestSourceSet, testTaskName); - project.getTasks().withType(RestIntegTestTask.class).named(testTaskName).configure(testTask -> { + TaskProvider yamlRestCompatTestTask = registerTestTask(project, yamlCompatTestSourceSet); + yamlRestCompatTestTask.configure(testTask -> { testTask.systemProperty("tests.restCompat", true); // Use test runner and classpath from "normal" yaml source set testTask.setTestClassesDirs( @@ -236,11 +233,11 @@ public void apply(Project project) { ); // run compatibility tests after "normal" tests - testTask.mustRunAfter(project.getTasks().named(InternalYamlRestTestPlugin.SOURCE_SET_NAME)); + testTask.mustRunAfter(project.getTasks().named(LegacyYamlRestTestPlugin.SOURCE_SET_NAME)); testTask.onlyIf(t -> isEnabled(extraProperties)); }); - setupYamlRestTestDependenciesDefaults(project, yamlCompatTestSourceSet); + setupYamlRestTestDependenciesDefaults(project, yamlCompatTestSourceSet, true); // setup IDE GradleUtils.setupIdeForTestSourceSet(project, yamlCompatTestSourceSet); @@ -259,6 +256,10 @@ public void apply(Project project) { } + public abstract TaskProvider registerTestTask(Project project, SourceSet sourceSet); + + public abstract Class> getBasePlugin(); + private boolean isEnabled(ExtraPropertiesExtension extraProperties) { Object bwcEnabled = extraProperties.getProperties().get("bwc_tests_enabled"); return bwcEnabled == null || (Boolean) bwcEnabled; diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/compat/compat/LegacyYamlRestCompatTestPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/compat/compat/LegacyYamlRestCompatTestPlugin.java new file mode 100644 index 000000000000..e84c84cc426a --- /dev/null +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/compat/compat/LegacyYamlRestCompatTestPlugin.java @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.gradle.internal.test.rest.compat.compat; + +import org.elasticsearch.gradle.internal.test.rest.LegacyYamlRestTestPlugin; +import org.elasticsearch.gradle.internal.test.rest.RestTestUtil; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.file.ProjectLayout; +import org.gradle.api.internal.file.FileOperations; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.testing.Test; + +import javax.inject.Inject; + +/** + * Apply this plugin to run the YAML based REST tests from a prior major version against this version's cluster. + * + * @deprecated use {@link YamlRestCompatTestPlugin} + */ +@Deprecated +public class LegacyYamlRestCompatTestPlugin extends AbstractYamlRestCompatTestPlugin { + @Inject + public LegacyYamlRestCompatTestPlugin(ProjectLayout projectLayout, FileOperations fileOperations) { + super(projectLayout, fileOperations); + } + + @Override + public TaskProvider registerTestTask(Project project, SourceSet sourceSet) { + return RestTestUtil.registerTestTask(project, sourceSet, sourceSet.getTaskName(null, "test")); + } + + @Override + public Class> getBasePlugin() { + return LegacyYamlRestTestPlugin.class; + } +} diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rest/compat/RestCompatTestTransformTask.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/compat/compat/RestCompatTestTransformTask.java similarity index 99% rename from build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rest/compat/RestCompatTestTransformTask.java rename to build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/compat/compat/RestCompatTestTransformTask.java index bfb53c23b5f1..eee1c4c21eb0 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rest/compat/RestCompatTestTransformTask.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/compat/compat/RestCompatTestTransformTask.java @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -package org.elasticsearch.gradle.internal.rest.compat; +package org.elasticsearch.gradle.internal.test.rest.compat.compat; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/compat/compat/YamlRestCompatTestPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/compat/compat/YamlRestCompatTestPlugin.java new file mode 100644 index 000000000000..79588ca722ff --- /dev/null +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/compat/compat/YamlRestCompatTestPlugin.java @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.gradle.internal.test.rest.compat.compat; + +import org.elasticsearch.gradle.internal.test.rest.InternalYamlRestTestPlugin; +import org.elasticsearch.gradle.internal.test.rest.RestTestUtil; +import org.elasticsearch.gradle.testclusters.StandaloneRestIntegTestTask; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.file.ProjectLayout; +import org.gradle.api.internal.file.FileOperations; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.testing.Test; + +import javax.inject.Inject; + +/** + * Apply this plugin to run the YAML based REST tests from a prior major version against this version's cluster. + */ +public class YamlRestCompatTestPlugin extends AbstractYamlRestCompatTestPlugin { + @Inject + public YamlRestCompatTestPlugin(ProjectLayout projectLayout, FileOperations fileOperations) { + super(projectLayout, fileOperations); + } + + @Override + public TaskProvider registerTestTask(Project project, SourceSet sourceSet) { + return RestTestUtil.registerTestTask(project, sourceSet, sourceSet.getTaskName(null, "test"), StandaloneRestIntegTestTask.class); + } + + @Override + public Class> getBasePlugin() { + return InternalYamlRestTestPlugin.class; + } +} diff --git a/build-tools/src/main/java/org/elasticsearch/gradle/testclusters/StandaloneRestIntegTestTask.java b/build-tools/src/main/java/org/elasticsearch/gradle/testclusters/StandaloneRestIntegTestTask.java index 13d2c8b948d5..ab1436bb9a31 100644 --- a/build-tools/src/main/java/org/elasticsearch/gradle/testclusters/StandaloneRestIntegTestTask.java +++ b/build-tools/src/main/java/org/elasticsearch/gradle/testclusters/StandaloneRestIntegTestTask.java @@ -73,11 +73,6 @@ public void setDebugServer(boolean enabled) { this.debugServer = enabled; } - @Override - public int getMaxParallelForks() { - return 1; - } - @Nested @Override public Collection getClusters() { diff --git a/distribution/archives/integ-test-zip/build.gradle b/distribution/archives/integ-test-zip/build.gradle index e57c6cf32141..d91d919619ff 100644 --- a/distribution/archives/integ-test-zip/build.gradle +++ b/distribution/archives/integ-test-zip/build.gradle @@ -8,7 +8,7 @@ import org.apache.tools.ant.filters.ReplaceTokens -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' // The integ-test-distribution is published to maven apply plugin: 'elasticsearch.publish' diff --git a/distribution/build.gradle b/distribution/build.gradle index 6b8cdb128042..08920ed173d7 100644 --- a/distribution/build.gradle +++ b/distribution/build.gradle @@ -116,6 +116,14 @@ def processIntegTestOutputsTaskProvider = tasks.register("processIntegTestOutput into integTestOutputs } +def integTestConfigFiles = fileTree("${integTestOutputs}/config") { + builtBy processIntegTestOutputsTaskProvider +} + +def integTestBinFiles = fileTree("${integTestOutputs}/bin") { + builtBy processIntegTestOutputsTaskProvider +} + def defaultModulesFiles = fileTree("${defaultOutputs}/modules") { builtBy processDefaultOutputsTaskProvider } @@ -358,7 +366,7 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) { filter("tokens" : expansionsForDistribution(distributionType, isTestDistro), ReplaceTokens.class) } from buildDefaultLog4jConfigTaskProvider - from defaultConfigFiles + from isTestDistro ? integTestConfigFiles : defaultConfigFiles } } @@ -388,7 +396,7 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) { // module provided bin files with copySpec { eachFile { it.setMode(0755) } - from(defaultBinFiles) + from(testDistro ? integTestBinFiles : defaultBinFiles) if (distributionType != 'zip') { exclude '*.bat' } diff --git a/distribution/docker/build.gradle b/distribution/docker/build.gradle index fe4a1e8b48a7..47659080e2ba 100644 --- a/distribution/docker/build.gradle +++ b/distribution/docker/build.gradle @@ -15,7 +15,7 @@ import org.elasticsearch.gradle.util.GradleUtils import java.nio.file.Path import java.time.temporal.ChronoUnit -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' apply plugin: 'elasticsearch.test.fixtures' apply plugin: 'elasticsearch.internal-distribution-download' apply plugin: 'elasticsearch.dra-artifacts' diff --git a/modules/aggregations/build.gradle b/modules/aggregations/build.gradle index b3cc698a5542..15a95d96fca2 100644 --- a/modules/aggregations/build.gradle +++ b/modules/aggregations/build.gradle @@ -8,8 +8,8 @@ import org.elasticsearch.gradle.internal.info.BuildParams * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' apply plugin: 'elasticsearch.internal-cluster-test' esplugin { diff --git a/modules/aggs-matrix-stats/build.gradle b/modules/aggs-matrix-stats/build.gradle index e0621544c446..669739070caf 100644 --- a/modules/aggs-matrix-stats/build.gradle +++ b/modules/aggs-matrix-stats/build.gradle @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' esplugin { description 'Adds aggregations whose input are a list of numeric fields and output includes a matrix.' diff --git a/modules/analysis-common/build.gradle b/modules/analysis-common/build.gradle index d1345695d8a1..9988d70b6365 100644 --- a/modules/analysis-common/build.gradle +++ b/modules/analysis-common/build.gradle @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' apply plugin: 'elasticsearch.internal-cluster-test' esplugin { diff --git a/modules/data-streams/build.gradle b/modules/data-streams/build.gradle index 49136712b855..fa3346e26ab2 100644 --- a/modules/data-streams/build.gradle +++ b/modules/data-streams/build.gradle @@ -2,9 +2,9 @@ import org.elasticsearch.gradle.internal.info.BuildParams apply plugin: 'elasticsearch.test-with-dependencies' apply plugin: 'elasticsearch.internal-cluster-test' -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.internal-java-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' esplugin { description 'Elasticsearch Expanded Pack Plugin - Data Streams' diff --git a/modules/ingest-attachment/build.gradle b/modules/ingest-attachment/build.gradle index 4db193b6202a..36dfd027cd81 100644 --- a/modules/ingest-attachment/build.gradle +++ b/modules/ingest-attachment/build.gradle @@ -7,8 +7,8 @@ import org.elasticsearch.gradle.internal.info.BuildParams * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' esplugin { description 'Ingest processor that uses Apache Tika to extract contents' diff --git a/modules/ingest-common/build.gradle b/modules/ingest-common/build.gradle index 958e64d2ac4b..e48156ef98da 100644 --- a/modules/ingest-common/build.gradle +++ b/modules/ingest-common/build.gradle @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' apply plugin: 'elasticsearch.internal-cluster-test' esplugin { diff --git a/modules/ingest-geoip/build.gradle b/modules/ingest-geoip/build.gradle index 3211d88444cc..282d4a605923 100644 --- a/modules/ingest-geoip/build.gradle +++ b/modules/ingest-geoip/build.gradle @@ -8,8 +8,8 @@ import org.apache.tools.ant.taskdefs.condition.Os -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' apply plugin: 'elasticsearch.internal-cluster-test' esplugin { diff --git a/modules/ingest-geoip/qa/file-based-update/build.gradle b/modules/ingest-geoip/qa/file-based-update/build.gradle index f465172a55b7..0e396d62eb16 100644 --- a/modules/ingest-geoip/qa/file-based-update/build.gradle +++ b/modules/ingest-geoip/qa/file-based-update/build.gradle @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' testClusters.configureEach { testDistribution = 'DEFAULT' diff --git a/modules/ingest-user-agent/build.gradle b/modules/ingest-user-agent/build.gradle index bdf7fae42d84..5d3ae968b787 100644 --- a/modules/ingest-user-agent/build.gradle +++ b/modules/ingest-user-agent/build.gradle @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' esplugin { description 'Ingest processor that extracts information from a user agent' diff --git a/modules/kibana/build.gradle b/modules/kibana/build.gradle index d334fa380d7b..e57e16409495 100644 --- a/modules/kibana/build.gradle +++ b/modules/kibana/build.gradle @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' esplugin { description 'Plugin exposing APIs for Kibana system indices' diff --git a/modules/lang-expression/build.gradle b/modules/lang-expression/build.gradle index 1581de127427..771b4f7c0186 100644 --- a/modules/lang-expression/build.gradle +++ b/modules/lang-expression/build.gradle @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' apply plugin: 'elasticsearch.internal-cluster-test' esplugin { diff --git a/modules/lang-mustache/build.gradle b/modules/lang-mustache/build.gradle index 7536e2f76abf..c36275699e21 100644 --- a/modules/lang-mustache/build.gradle +++ b/modules/lang-mustache/build.gradle @@ -5,9 +5,9 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.internal-cluster-test' esplugin { diff --git a/modules/lang-painless/build.gradle b/modules/lang-painless/build.gradle index 17e43a755bed..682802534438 100644 --- a/modules/lang-painless/build.gradle +++ b/modules/lang-painless/build.gradle @@ -9,8 +9,8 @@ import org.elasticsearch.gradle.testclusters.DefaultTestClustersTask; apply plugin: 'elasticsearch.validate-rest-spec' -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' esplugin { description 'An easy, safe and fast scripting language for Elasticsearch' diff --git a/modules/mapper-extras/build.gradle b/modules/mapper-extras/build.gradle index a9db8325e1db..62e7744bfa7f 100644 --- a/modules/mapper-extras/build.gradle +++ b/modules/mapper-extras/build.gradle @@ -8,8 +8,8 @@ import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' apply plugin: 'elasticsearch.internal-cluster-test' esplugin { diff --git a/modules/parent-join/build.gradle b/modules/parent-join/build.gradle index 66079cfd444c..903192e6ce25 100644 --- a/modules/parent-join/build.gradle +++ b/modules/parent-join/build.gradle @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' apply plugin: 'elasticsearch.internal-cluster-test' esplugin { diff --git a/modules/percolator/build.gradle b/modules/percolator/build.gradle index 785edc32c143..a871056539d3 100644 --- a/modules/percolator/build.gradle +++ b/modules/percolator/build.gradle @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' apply plugin: 'elasticsearch.internal-cluster-test' esplugin { diff --git a/modules/rank-eval/build.gradle b/modules/rank-eval/build.gradle index 5ac9922a1e07..1268a40dd5bd 100644 --- a/modules/rank-eval/build.gradle +++ b/modules/rank-eval/build.gradle @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' apply plugin: 'elasticsearch.internal-cluster-test' esplugin { diff --git a/modules/reindex/build.gradle b/modules/reindex/build.gradle index 1496e32927f9..4cd12b44aaaa 100644 --- a/modules/reindex/build.gradle +++ b/modules/reindex/build.gradle @@ -17,9 +17,9 @@ import org.gradle.api.internal.artifacts.ArtifactAttributes apply plugin: 'elasticsearch.test-with-dependencies' apply plugin: 'elasticsearch.jdk-download' -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.internal-java-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' apply plugin: 'elasticsearch.internal-cluster-test' esplugin { diff --git a/modules/repository-azure/build.gradle b/modules/repository-azure/build.gradle index 0101c0d4df7c..221325706297 100644 --- a/modules/repository-azure/build.gradle +++ b/modules/repository-azure/build.gradle @@ -13,7 +13,7 @@ import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' apply plugin: 'elasticsearch.internal-cluster-test' apply plugin: 'elasticsearch.internal-test-artifact-base' diff --git a/modules/repository-gcs/build.gradle b/modules/repository-gcs/build.gradle index 7b0f62b54d6d..b98a97cb34be 100644 --- a/modules/repository-gcs/build.gradle +++ b/modules/repository-gcs/build.gradle @@ -1,7 +1,7 @@ import org.apache.tools.ant.filters.ReplaceTokens import org.elasticsearch.gradle.internal.info.BuildParams import org.elasticsearch.gradle.internal.test.RestIntegTestTask -import org.elasticsearch.gradle.internal.test.rest.InternalYamlRestTestPlugin +import org.elasticsearch.gradle.internal.test.rest.LegacyYamlRestTestPlugin import org.elasticsearch.gradle.internal.test.InternalClusterTestPlugin import java.nio.file.Files @@ -16,7 +16,7 @@ import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' apply plugin: 'elasticsearch.internal-cluster-test' apply plugin: 'elasticsearch.internal-test-artifact-base' @@ -271,7 +271,7 @@ def largeBlobYamlRestTest = tasks.register("largeBlobYamlRestTest", RestIntegTes dependsOn "createServiceAccountFile" } SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); - SourceSet yamlRestTestSourceSet = sourceSets.getByName(InternalYamlRestTestPlugin.SOURCE_SET_NAME) + SourceSet yamlRestTestSourceSet = sourceSets.getByName(LegacyYamlRestTestPlugin.SOURCE_SET_NAME) setTestClassesDirs(yamlRestTestSourceSet.getOutput().getClassesDirs()) setClasspath(yamlRestTestSourceSet.getRuntimeClasspath()) @@ -323,7 +323,7 @@ testClusters.matching { if (useFixture) { tasks.register("yamlRestTestApplicationDefaultCredentials", RestIntegTestTask.class) { SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); - SourceSet yamlRestTestSourceSet = sourceSets.getByName(InternalYamlRestTestPlugin.SOURCE_SET_NAME) + SourceSet yamlRestTestSourceSet = sourceSets.getByName(LegacyYamlRestTestPlugin.SOURCE_SET_NAME) setTestClassesDirs(yamlRestTestSourceSet.getOutput().getClassesDirs()) setClasspath(yamlRestTestSourceSet.getRuntimeClasspath()) } diff --git a/modules/repository-s3/build.gradle b/modules/repository-s3/build.gradle index c5e8341c0d8d..892c8c42903e 100644 --- a/modules/repository-s3/build.gradle +++ b/modules/repository-s3/build.gradle @@ -1,7 +1,7 @@ import org.apache.tools.ant.filters.ReplaceTokens import org.elasticsearch.gradle.internal.info.BuildParams import org.elasticsearch.gradle.internal.test.RestIntegTestTask -import org.elasticsearch.gradle.internal.test.rest.InternalYamlRestTestPlugin +import org.elasticsearch.gradle.internal.test.rest.LegacyYamlRestTestPlugin import org.elasticsearch.gradle.internal.test.InternalClusterTestPlugin import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE @@ -13,7 +13,7 @@ import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' apply plugin: 'elasticsearch.internal-cluster-test' apply plugin: 'elasticsearch.internal-test-artifact-base' @@ -244,7 +244,7 @@ if (useFixture) { tasks.register("yamlRestTestMinio", RestIntegTestTask) { description = "Runs REST tests using the Minio repository." SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); - SourceSet yamlRestTestSourceSet = sourceSets.getByName(InternalYamlRestTestPlugin.SOURCE_SET_NAME) + SourceSet yamlRestTestSourceSet = sourceSets.getByName(LegacyYamlRestTestPlugin.SOURCE_SET_NAME) setTestClassesDirs(yamlRestTestSourceSet.getOutput().getClassesDirs()) setClasspath(yamlRestTestSourceSet.getRuntimeClasspath()) @@ -272,7 +272,7 @@ if (useFixture) { tasks.register("yamlRestTestECS", RestIntegTestTask.class) { description = "Runs tests using the ECS repository." SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); - SourceSet yamlRestTestSourceSet = sourceSets.getByName(InternalYamlRestTestPlugin.SOURCE_SET_NAME) + SourceSet yamlRestTestSourceSet = sourceSets.getByName(LegacyYamlRestTestPlugin.SOURCE_SET_NAME) setTestClassesDirs(yamlRestTestSourceSet.getOutput().getClassesDirs()) setClasspath(yamlRestTestSourceSet.getRuntimeClasspath()) systemProperty 'tests.rest.blacklist', [ @@ -298,7 +298,7 @@ if (useFixture) { tasks.register("yamlRestTestSTS", RestIntegTestTask.class) { description = "Runs tests with the STS (Secure Token Service)" SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); - SourceSet yamlRestTestSourceSet = sourceSets.getByName(InternalYamlRestTestPlugin.SOURCE_SET_NAME) + SourceSet yamlRestTestSourceSet = sourceSets.getByName(LegacyYamlRestTestPlugin.SOURCE_SET_NAME) setTestClassesDirs(yamlRestTestSourceSet.getOutput().getClassesDirs()) setClasspath(yamlRestTestSourceSet.getRuntimeClasspath()) systemProperty 'tests.rest.blacklist', [ diff --git a/modules/repository-url/build.gradle b/modules/repository-url/build.gradle index 164c64ad1f61..7b671802f3a2 100644 --- a/modules/repository-url/build.gradle +++ b/modules/repository-url/build.gradle @@ -8,8 +8,8 @@ import org.elasticsearch.gradle.PropertyNormalization -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' apply plugin: 'elasticsearch.internal-cluster-test' apply plugin: 'elasticsearch.test.fixtures' diff --git a/modules/runtime-fields-common/build.gradle b/modules/runtime-fields-common/build.gradle index 5a2d268cf7a4..c4db67a89d36 100644 --- a/modules/runtime-fields-common/build.gradle +++ b/modules/runtime-fields-common/build.gradle @@ -7,8 +7,8 @@ */ apply plugin: 'elasticsearch.validate-rest-spec' -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' esplugin { description 'Module for runtime fields features and extensions that have large dependencies' diff --git a/modules/transport-netty4/build.gradle b/modules/transport-netty4/build.gradle index d3ce40ad32f0..74fef7098eff 100644 --- a/modules/transport-netty4/build.gradle +++ b/modules/transport-netty4/build.gradle @@ -7,12 +7,12 @@ */ import org.elasticsearch.gradle.internal.test.RestIntegTestTask -import org.elasticsearch.gradle.internal.test.rest.InternalJavaRestTestPlugin +import org.elasticsearch.gradle.internal.test.rest.LegacyJavaRestTestPlugin import org.elasticsearch.gradle.internal.test.InternalClusterTestPlugin -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.internal-cluster-test' apply plugin: 'elasticsearch.publish' @@ -73,7 +73,7 @@ TaskProvider pooledInternalClusterTest = tasks.register("pooledInternalClu TaskProvider pooledJavaRestTest = tasks.register("pooledJavaRestTest", RestIntegTestTask) { SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); - SourceSet javaRestTestSourceSet = sourceSets.getByName(InternalJavaRestTestPlugin.SOURCE_SET_NAME) + SourceSet javaRestTestSourceSet = sourceSets.getByName(LegacyJavaRestTestPlugin.SOURCE_SET_NAME) setTestClassesDirs(javaRestTestSourceSet.getOutput().getClassesDirs()) setClasspath(javaRestTestSourceSet.getRuntimeClasspath()) diff --git a/plugins/analysis-icu/build.gradle b/plugins/analysis-icu/build.gradle index b10ca2b6dd49..1c7db6d040be 100644 --- a/plugins/analysis-icu/build.gradle +++ b/plugins/analysis-icu/build.gradle @@ -7,8 +7,8 @@ import org.elasticsearch.gradle.internal.info.BuildParams * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' apply plugin: 'elasticsearch.internal-cluster-test' esplugin { diff --git a/plugins/analysis-kuromoji/build.gradle b/plugins/analysis-kuromoji/build.gradle index 0a9cc629e7a4..a91e34018179 100644 --- a/plugins/analysis-kuromoji/build.gradle +++ b/plugins/analysis-kuromoji/build.gradle @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' esplugin { description 'The Japanese (kuromoji) Analysis plugin integrates Lucene kuromoji analysis module into elasticsearch.' diff --git a/plugins/analysis-nori/build.gradle b/plugins/analysis-nori/build.gradle index 4c545d4f72d4..51e93bf6cc2c 100644 --- a/plugins/analysis-nori/build.gradle +++ b/plugins/analysis-nori/build.gradle @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' esplugin { description 'The Korean (nori) Analysis plugin integrates Lucene nori analysis module into elasticsearch.' diff --git a/plugins/analysis-phonetic/build.gradle b/plugins/analysis-phonetic/build.gradle index 96149295b496..7646c0ee874d 100644 --- a/plugins/analysis-phonetic/build.gradle +++ b/plugins/analysis-phonetic/build.gradle @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' esplugin { description 'The Phonetic Analysis plugin integrates phonetic token filter analysis with elasticsearch.' diff --git a/plugins/analysis-smartcn/build.gradle b/plugins/analysis-smartcn/build.gradle index dd577a550e8f..c75c5c304048 100644 --- a/plugins/analysis-smartcn/build.gradle +++ b/plugins/analysis-smartcn/build.gradle @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' esplugin { description 'Smart Chinese Analysis plugin integrates Lucene Smart Chinese analysis module into elasticsearch.' diff --git a/plugins/analysis-stempel/build.gradle b/plugins/analysis-stempel/build.gradle index 363d923492e0..ca6d91df5cd1 100644 --- a/plugins/analysis-stempel/build.gradle +++ b/plugins/analysis-stempel/build.gradle @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' esplugin { description 'The Stempel (Polish) Analysis plugin integrates Lucene stempel (polish) analysis module into elasticsearch.' diff --git a/plugins/analysis-ukrainian/build.gradle b/plugins/analysis-ukrainian/build.gradle index 67aa47db8bbd..709b8831628b 100644 --- a/plugins/analysis-ukrainian/build.gradle +++ b/plugins/analysis-ukrainian/build.gradle @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' esplugin { description 'The Ukrainian Analysis plugin integrates the Lucene UkrainianMorfologikAnalyzer into elasticsearch.' diff --git a/plugins/discovery-azure-classic/build.gradle b/plugins/discovery-azure-classic/build.gradle index de47739d382b..3db7c21309b5 100644 --- a/plugins/discovery-azure-classic/build.gradle +++ b/plugins/discovery-azure-classic/build.gradle @@ -8,7 +8,7 @@ import org.elasticsearch.gradle.internal.info.BuildParams * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' apply plugin: 'elasticsearch.internal-cluster-test' esplugin { diff --git a/plugins/discovery-ec2/build.gradle b/plugins/discovery-ec2/build.gradle index 1e48fa537e43..b21f6224c9fc 100644 --- a/plugins/discovery-ec2/build.gradle +++ b/plugins/discovery-ec2/build.gradle @@ -7,7 +7,7 @@ import org.elasticsearch.gradle.internal.info.BuildParams * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' apply plugin: 'elasticsearch.internal-cluster-test' esplugin { diff --git a/plugins/discovery-ec2/qa/amazon-ec2/build.gradle b/plugins/discovery-ec2/qa/amazon-ec2/build.gradle index e8afba63970c..c7c4ecba40fe 100644 --- a/plugins/discovery-ec2/qa/amazon-ec2/build.gradle +++ b/plugins/discovery-ec2/qa/amazon-ec2/build.gradle @@ -10,11 +10,11 @@ import org.apache.tools.ant.filters.ReplaceTokens import org.elasticsearch.gradle.internal.info.BuildParams import org.elasticsearch.gradle.internal.test.AntFixture import org.elasticsearch.gradle.internal.test.RestIntegTestTask -import org.elasticsearch.gradle.internal.test.rest.InternalYamlRestTestPlugin +import org.elasticsearch.gradle.internal.test.rest.LegacyYamlRestTestPlugin import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' dependencies { yamlRestTestImplementation project(':plugins:discovery-ec2') @@ -62,7 +62,7 @@ tasks.named("yamlRestTest").configure { enabled = false } def yamlRestTestTask = tasks.register("yamlRestTest${action}", RestIntegTestTask) { dependsOn fixture SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); - SourceSet yamlRestTestSourceSet = sourceSets.getByName(InternalYamlRestTestPlugin.SOURCE_SET_NAME) + SourceSet yamlRestTestSourceSet = sourceSets.getByName(LegacyYamlRestTestPlugin.SOURCE_SET_NAME) testClassesDirs = yamlRestTestSourceSet.getOutput().getClassesDirs() classpath = yamlRestTestSourceSet.getRuntimeClasspath() } diff --git a/plugins/discovery-gce/build.gradle b/plugins/discovery-gce/build.gradle index cfd1609078de..89e53f94853e 100644 --- a/plugins/discovery-gce/build.gradle +++ b/plugins/discovery-gce/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' apply plugin: 'elasticsearch.internal-cluster-test' esplugin { diff --git a/plugins/discovery-gce/qa/gce/build.gradle b/plugins/discovery-gce/qa/gce/build.gradle index c2cd26c2e928..7eb6e85903ba 100644 --- a/plugins/discovery-gce/qa/gce/build.gradle +++ b/plugins/discovery-gce/qa/gce/build.gradle @@ -13,7 +13,7 @@ import org.elasticsearch.gradle.internal.test.AntFixture import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' final int gceNumberOfNodes = 3 diff --git a/plugins/mapper-annotated-text/build.gradle b/plugins/mapper-annotated-text/build.gradle index 633cc3d3dcbb..275e7f5267e5 100644 --- a/plugins/mapper-annotated-text/build.gradle +++ b/plugins/mapper-annotated-text/build.gradle @@ -7,8 +7,8 @@ import org.elasticsearch.gradle.internal.info.BuildParams * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' esplugin { description 'The Mapper Annotated_text plugin adds support for text fields with markup used to inject annotation tokens into the index.' diff --git a/plugins/mapper-murmur3/build.gradle b/plugins/mapper-murmur3/build.gradle index 6243871ef3bd..7e93e6d29539 100644 --- a/plugins/mapper-murmur3/build.gradle +++ b/plugins/mapper-murmur3/build.gradle @@ -7,8 +7,8 @@ import org.elasticsearch.gradle.internal.info.BuildParams * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' esplugin { description 'The Mapper Murmur3 plugin allows to compute hashes of a field\'s values at index-time and to store them in the index.' diff --git a/plugins/mapper-size/src/yamlRestTest/java/org/elasticsearch/index/mapper/size/MapperSizeClientYamlTestSuiteIT.java b/plugins/mapper-size/src/yamlRestTest/java/org/elasticsearch/index/mapper/size/MapperSizeClientYamlTestSuiteIT.java index a1bef57849f2..e9730418307e 100644 --- a/plugins/mapper-size/src/yamlRestTest/java/org/elasticsearch/index/mapper/size/MapperSizeClientYamlTestSuiteIT.java +++ b/plugins/mapper-size/src/yamlRestTest/java/org/elasticsearch/index/mapper/size/MapperSizeClientYamlTestSuiteIT.java @@ -11,11 +11,16 @@ import com.carrotsearch.randomizedtesting.annotations.Name; import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; +import org.elasticsearch.test.cluster.ElasticsearchCluster; import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate; import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase; +import org.junit.ClassRule; public class MapperSizeClientYamlTestSuiteIT extends ESClientYamlSuiteTestCase { + @ClassRule + public static ElasticsearchCluster cluster = ElasticsearchCluster.local().plugin("mapper-size").build(); + public MapperSizeClientYamlTestSuiteIT(@Name("yaml") ClientYamlTestCandidate testCandidate) { super(testCandidate); } @@ -24,4 +29,9 @@ public MapperSizeClientYamlTestSuiteIT(@Name("yaml") ClientYamlTestCandidate tes public static Iterable parameters() throws Exception { return createParameters(); } + + @Override + protected String getTestRestCluster() { + return cluster.getHttpAddresses(); + } } diff --git a/plugins/repository-hdfs/build.gradle b/plugins/repository-hdfs/build.gradle index 5db8373dca8e..df8d3cdac1c4 100644 --- a/plugins/repository-hdfs/build.gradle +++ b/plugins/repository-hdfs/build.gradle @@ -15,8 +15,8 @@ import java.nio.file.Path import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE apply plugin: 'elasticsearch.test.fixtures' -apply plugin: 'elasticsearch.internal-java-rest-test' -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' esplugin { description 'The HDFS repository plugin adds support for Hadoop Distributed File-System (HDFS) repositories.' diff --git a/plugins/store-smb/build.gradle b/plugins/store-smb/build.gradle index 91c7dcadf022..7147c7302733 100644 --- a/plugins/store-smb/build.gradle +++ b/plugins/store-smb/build.gradle @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' apply plugin: 'elasticsearch.internal-cluster-test' esplugin { diff --git a/qa/ccs-unavailable-clusters/build.gradle b/qa/ccs-unavailable-clusters/build.gradle index 1b866385ecba..3b958bf9c8b9 100644 --- a/qa/ccs-unavailable-clusters/build.gradle +++ b/qa/ccs-unavailable-clusters/build.gradle @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' testClusters.matching { it.name == "javaRestTest" }.configureEach { setting 'xpack.security.enabled', 'true' diff --git a/qa/smoke-test-http/build.gradle b/qa/smoke-test-http/build.gradle index c08194f7db87..6766823dc21e 100644 --- a/qa/smoke-test-http/build.gradle +++ b/qa/smoke-test-http/build.gradle @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' //apply plugin: 'elasticsearch.test-with-dependencies' dependencies { diff --git a/qa/smoke-test-ingest-disabled/build.gradle b/qa/smoke-test-ingest-disabled/build.gradle index ad539bb515e1..5ffefed52b71 100644 --- a/qa/smoke-test-ingest-disabled/build.gradle +++ b/qa/smoke-test-ingest-disabled/build.gradle @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' dependencies { testImplementation project(':modules:ingest-common') diff --git a/qa/smoke-test-ingest-with-all-dependencies/build.gradle b/qa/smoke-test-ingest-with-all-dependencies/build.gradle index 1f043272b417..6181548757c6 100644 --- a/qa/smoke-test-ingest-with-all-dependencies/build.gradle +++ b/qa/smoke-test-ingest-with-all-dependencies/build.gradle @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' dependencies { yamlRestTestImplementation project(':modules:lang-mustache') @@ -24,4 +24,4 @@ tasks.named("yamlRestTestTestingConventions").configure { tasks.named("forbiddenPatterns").configure { exclude '**/*.mmdb' -} \ No newline at end of file +} diff --git a/qa/smoke-test-multinode/build.gradle b/qa/smoke-test-multinode/build.gradle index 8fbe7229facd..cba95391cc31 100644 --- a/qa/smoke-test-multinode/build.gradle +++ b/qa/smoke-test-multinode/build.gradle @@ -10,7 +10,7 @@ import org.elasticsearch.gradle.Version import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' restResources { restTests { diff --git a/qa/smoke-test-plugins/build.gradle b/qa/smoke-test-plugins/build.gradle index 06a9d9a6613a..7ae46b8e0d68 100644 --- a/qa/smoke-test-plugins/build.gradle +++ b/qa/smoke-test-plugins/build.gradle @@ -9,7 +9,7 @@ import org.apache.tools.ant.filters.ReplaceTokens import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' ext.pluginPaths = [] project(':plugins').getChildProjects().each { pluginName, pluginProject -> diff --git a/qa/system-indices/build.gradle b/qa/system-indices/build.gradle index a8be8e8374d5..4cb60860cca4 100644 --- a/qa/system-indices/build.gradle +++ b/qa/system-indices/build.gradle @@ -7,7 +7,7 @@ */ apply plugin: 'elasticsearch.base-internal-es-plugin' -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' esplugin { name 'system-indices-qa' diff --git a/qa/unconfigured-node-name/build.gradle b/qa/unconfigured-node-name/build.gradle index aae98def1db8..dd1dd0a2334f 100644 --- a/qa/unconfigured-node-name/build.gradle +++ b/qa/unconfigured-node-name/build.gradle @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' testClusters.configureEach { setting 'xpack.security.enabled', 'false' diff --git a/rest-api-spec/build.gradle b/rest-api-spec/build.gradle index e92d6767a803..e5049a117905 100644 --- a/rest-api-spec/build.gradle +++ b/rest-api-spec/build.gradle @@ -33,9 +33,8 @@ artifacts { restTests(new File(projectDir, "src/yamlRestTest/resources/rest-api-spec/test")) } -testClusters.configureEach { - module ':modules:mapper-extras' - requiresFeature 'es.index_mode_feature_flag_registered', Version.fromString("8.0.0") +dependencies { + clusterModules project(":modules:mapper-extras") } tasks.named("yamlRestTestV7CompatTransform").configure { task -> diff --git a/rest-api-spec/src/yamlRestTest/java/org/elasticsearch/test/rest/ClientYamlTestSuiteIT.java b/rest-api-spec/src/yamlRestTest/java/org/elasticsearch/test/rest/ClientYamlTestSuiteIT.java index 287c8a57627b..a10d53389af9 100644 --- a/rest-api-spec/src/yamlRestTest/java/org/elasticsearch/test/rest/ClientYamlTestSuiteIT.java +++ b/rest-api-spec/src/yamlRestTest/java/org/elasticsearch/test/rest/ClientYamlTestSuiteIT.java @@ -13,8 +13,11 @@ import com.carrotsearch.randomizedtesting.annotations.TimeoutSuite; import org.apache.lucene.tests.util.TimeUnits; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.cluster.FeatureFlag; import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate; import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase; +import org.junit.ClassRule; /** Rest integration test. Runs against a cluster started by {@code gradle integTest} */ @@ -22,6 +25,12 @@ @TimeoutSuite(millis = 40 * TimeUnits.MINUTE) public class ClientYamlTestSuiteIT extends ESClientYamlSuiteTestCase { + @ClassRule + public static ElasticsearchCluster cluster = ElasticsearchCluster.local() + .module("mapper-extras") + .feature(FeatureFlag.TIME_SERIES_MODE) + .build(); + public ClientYamlTestSuiteIT(@Name("yaml") ClientYamlTestCandidate testCandidate) { super(testCandidate); } @@ -30,4 +39,9 @@ public ClientYamlTestSuiteIT(@Name("yaml") ClientYamlTestCandidate testCandidate public static Iterable parameters() throws Exception { return createParameters(); } + + @Override + protected String getTestRestCluster() { + return cluster.getHttpAddresses(); + } } diff --git a/settings.gradle b/settings.gradle index 0fbb994ec248..a157ba478435 100644 --- a/settings.gradle +++ b/settings.gradle @@ -83,8 +83,9 @@ List projects = [ 'test:fixtures:url-fixture', 'test:fixtures:nginx-fixture', 'test:logger-usage', - 'test:yaml-rest-runner', - 'test:x-content' + 'test:test-clusters', + 'test:x-content', + 'test:yaml-rest-runner' ] /** diff --git a/test/external-modules/build.gradle b/test/external-modules/build.gradle index 7b2c74918124..538a0439f88f 100644 --- a/test/external-modules/build.gradle +++ b/test/external-modules/build.gradle @@ -3,7 +3,7 @@ import org.elasticsearch.gradle.internal.info.BuildParams subprojects { apply plugin: 'elasticsearch.base-internal-es-plugin' - apply plugin: 'elasticsearch.internal-yaml-rest-test' + apply plugin: 'elasticsearch.legacy-yaml-rest-test' esplugin { name it.name diff --git a/test/external-modules/die-with-dignity/build.gradle b/test/external-modules/die-with-dignity/build.gradle index 3466b7bb24ad..3046b045220a 100644 --- a/test/external-modules/die-with-dignity/build.gradle +++ b/test/external-modules/die-with-dignity/build.gradle @@ -1,7 +1,7 @@ import org.elasticsearch.gradle.internal.info.BuildParams import org.elasticsearch.gradle.util.GradleUtils -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.internal-es-plugin' esplugin { diff --git a/test/test-clusters/build.gradle b/test/test-clusters/build.gradle new file mode 100644 index 000000000000..48f55931c510 --- /dev/null +++ b/test/test-clusters/build.gradle @@ -0,0 +1,15 @@ +import org.elasticsearch.gradle.internal.conventions.util.Util + +apply plugin: 'elasticsearch.java' +apply plugin: 'com.github.johnrengelman.shadow' + +dependencies { + shadow "junit:junit:${versions.junit}" + shadow "org.apache.logging.log4j:log4j-api:${versions.log4j}" + + implementation "org.elasticsearch.gradle:reaper" +} + +tasks.named("processResources").configure { + from(new File(Util.locateElasticsearchWorkspace(gradle), "build-tools-internal/version.properties")) +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/ClusterFactory.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/ClusterFactory.java new file mode 100644 index 000000000000..313bfcc767fa --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/ClusterFactory.java @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster; + +public interface ClusterFactory { + + H create(S spec); +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/ClusterHandle.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/ClusterHandle.java new file mode 100644 index 000000000000..02a65aaeec7a --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/ClusterHandle.java @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster; + +import java.io.Closeable; + +/** + * A handle to an {@link ElasticsearchCluster}. + */ +public interface ClusterHandle extends Closeable { + /** + * Starts the cluster. This method will block until all nodes are started and cluster is ready to serve requests. + */ + void start(); + + /** + * Stops the cluster. This method will block until all cluster node processes have exited. This method is thread-safe and subsequent + * calls will wait for the exiting termination to complete. + * + * @param forcibly whether to forcibly terminate the cluster + */ + void stop(boolean forcibly); + + /** + * Whether the cluster is started or not. This method makes no guarantees on cluster availability, only that the node processes have + * been started. + * + * @return whether the cluster has been started + */ + boolean isStarted(); + + /** + * Returns a comma-separated list of HTTP transport endpoints for cluster. If this method is called on an unstarted cluster, the cluster + * will be started. This method is thread-safe and subsequent calls will wait for cluster start and availability. + * + * @return cluster node HTTP transport addresses + */ + String getHttpAddresses(); +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/ClusterSpec.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/ClusterSpec.java new file mode 100644 index 000000000000..528a24509c3b --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/ClusterSpec.java @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster; + +public interface ClusterSpec {} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/ElasticsearchCluster.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/ElasticsearchCluster.java new file mode 100644 index 000000000000..02eb3fb73df6 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/ElasticsearchCluster.java @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster; + +import org.elasticsearch.test.cluster.local.DefaultLocalClusterSpecBuilder; +import org.elasticsearch.test.cluster.local.LocalClusterSpecBuilder; +import org.junit.rules.TestRule; + +/** + *

    A JUnit test rule for orchestrating an Elasticsearch cluster for local integration testing. New clusters can be created via one of the + * various static builder methods. For example:

    + *
    + * @ClassRule
    + * public static ElasticsearchCluster myCluster = ElasticsearchCluster.local().build();
    + * 
    + */ +public interface ElasticsearchCluster extends TestRule, ClusterHandle { + + /** + * Creates a new {@link DefaultLocalClusterSpecBuilder} for defining a locally orchestrated cluster. Local clusters use a locally built + * Elasticsearch distribution. + * + * @return a builder for a local cluster + */ + static LocalClusterSpecBuilder local() { + return new DefaultLocalClusterSpecBuilder(); + } + +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/EnvironmentProvider.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/EnvironmentProvider.java new file mode 100644 index 000000000000..a63151b42f8f --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/EnvironmentProvider.java @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster; + +import org.elasticsearch.test.cluster.local.LocalClusterSpec.LocalNodeSpec; + +import java.util.Map; + +/** + * Functional interface for supplying environment variables to an Elasticsearch node. This interface is designed to be implemented by tests + * and fixtures wanting to provide settings to an {@link ElasticsearchCluster} in a dynamic fashion. Instances are evaluated lazily at + * cluster start time. + */ +public interface EnvironmentProvider { + + /** + * Returns a collection of environment variables to apply to an Elasticsearch cluster node. This method is called when the cluster is + * started so implementors can return dynamic environment values that may or may not be based on the given node spec. + * + * @param nodeSpec the specification for the given node to apply settings to + * @return environment variables to add to the node + */ + Map get(LocalNodeSpec nodeSpec); +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/FeatureFlag.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/FeatureFlag.java new file mode 100644 index 000000000000..62103dcf47e7 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/FeatureFlag.java @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster; + +import org.elasticsearch.test.cluster.util.Version; + +/** + * Elasticsearch feature flags. Used in conjunction with {@link org.elasticsearch.test.cluster.local.LocalSpecBuilder#feature(FeatureFlag)} + * to indicate that this feature is required and should be enabled when appropriate. + */ +public enum FeatureFlag { + TIME_SERIES_MODE("es.index_mode_feature_flag_registered=true", Version.fromString("8.0.0"), null); + + public final String systemProperty; + public final Version from; + public final Version until; + + FeatureFlag(String systemProperty, Version from, Version until) { + this.systemProperty = systemProperty; + this.from = from; + this.until = until; + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/MutableSettingsProvider.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/MutableSettingsProvider.java new file mode 100644 index 000000000000..172a114bc04d --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/MutableSettingsProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster; + +import org.elasticsearch.test.cluster.local.LocalClusterSpec; + +import java.util.HashMap; +import java.util.Map; + +public class MutableSettingsProvider implements SettingsProvider { + private final Map settings = new HashMap<>(); + + @Override + public Map get(LocalClusterSpec.LocalNodeSpec nodeSpec) { + return settings; + } + + public void put(String setting, String value) { + settings.put(setting, value); + } + + public void clear() { + settings.clear(); + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/SettingsProvider.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/SettingsProvider.java new file mode 100644 index 000000000000..3d4c694fad6c --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/SettingsProvider.java @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster; + +import org.elasticsearch.test.cluster.local.LocalClusterSpec.LocalNodeSpec; + +import java.util.Map; + +/** + * Functional interface for supplying settings to an Elasticsearch node. This interface is designed to be implemented by tests and fixtures + * wanting to provide settings to an {@link ElasticsearchCluster} in a dynamic fashion. Instances are evaluated lazily at cluster + * start time. + */ +public interface SettingsProvider { + + /** + * Returns a collection of settings to apply to an Elasticsearch cluster node. This method is called when the cluster is started so + * implementors can return dynamic setting values that may or may not be based on the given node spec. + * + * @param nodeSpec the specification for the given node to apply settings to + * @return settings to add to the node + */ + Map get(LocalNodeSpec nodeSpec); +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/AbstractLocalSpecBuilder.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/AbstractLocalSpecBuilder.java new file mode 100644 index 000000000000..64888c39268c --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/AbstractLocalSpecBuilder.java @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.local; + +import org.elasticsearch.test.cluster.EnvironmentProvider; +import org.elasticsearch.test.cluster.FeatureFlag; +import org.elasticsearch.test.cluster.SettingsProvider; +import org.elasticsearch.test.cluster.local.distribution.DistributionType; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +public abstract class AbstractLocalSpecBuilder> implements LocalSpecBuilder { + private final AbstractLocalSpecBuilder parent; + private final List settingsProviders = new ArrayList<>(); + private final Map settings = new HashMap<>(); + private final List environmentProviders = new ArrayList<>(); + private final Map environment = new HashMap<>(); + private final Set modules = new HashSet<>(); + private final Set plugins = new HashSet<>(); + private final Set features = new HashSet<>(); + private DistributionType distributionType; + + protected AbstractLocalSpecBuilder(AbstractLocalSpecBuilder parent) { + this.parent = parent; + } + + @Override + public T settings(SettingsProvider settingsProvider) { + this.settingsProviders.add(settingsProvider); + return cast(this); + } + + List getSettingsProviders() { + return inherit(() -> parent.getSettingsProviders(), settingsProviders); + } + + @Override + public T setting(String setting, String value) { + this.settings.put(setting, value); + return cast(this); + } + + Map getSettings() { + return inherit(() -> parent.getSettings(), settings); + } + + @Override + public T environment(EnvironmentProvider environmentProvider) { + this.environmentProviders.add(environmentProvider); + return cast(this); + } + + List getEnvironmentProviders() { + return inherit(() -> parent.getEnvironmentProviders(), environmentProviders); + + } + + @Override + public T environment(String key, String value) { + this.environment.put(key, value); + return cast(this); + } + + Map getEnvironment() { + return inherit(() -> parent.getEnvironment(), environment); + } + + @Override + public T distribution(DistributionType type) { + this.distributionType = type; + return cast(this); + } + + DistributionType getDistributionType() { + return inherit(() -> parent.getDistributionType(), distributionType); + } + + @Override + public T module(String moduleName) { + this.modules.add(moduleName); + return cast(this); + } + + Set getModules() { + return inherit(() -> parent.getModules(), modules); + } + + @Override + public T plugin(String pluginName) { + this.plugins.add(pluginName); + return cast(this); + } + + Set getPlugins() { + return inherit(() -> parent.getPlugins(), plugins); + } + + @Override + public T feature(FeatureFlag feature) { + this.features.add(feature); + return cast(this); + } + + Set getFeatures() { + return inherit(() -> parent.getFeatures(), features); + } + + private List inherit(Supplier> parent, List child) { + List combinedList = new ArrayList<>(); + if (this.parent != null) { + combinedList.addAll(parent.get()); + } + combinedList.addAll(child); + return combinedList; + } + + private Set inherit(Supplier> parent, Set child) { + Set combinedSet = new HashSet<>(); + if (this.parent != null) { + combinedSet.addAll(parent.get()); + } + combinedSet.addAll(child); + return combinedSet; + } + + private Map inherit(Supplier> parent, Map child) { + Map combinedMap = new HashMap<>(); + if (this.parent != null) { + combinedMap.putAll(parent.get()); + } + combinedMap.putAll(child); + return combinedMap; + } + + private T inherit(Supplier parent, T child) { + T value = null; + if (this.parent != null) { + value = parent.get(); + } + return child == null ? value : child; + } + + @SuppressWarnings("unchecked") + private static T cast(Object o) { + return (T) o; + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/DefaultEnvironmentProvider.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/DefaultEnvironmentProvider.java new file mode 100644 index 000000000000..a6e3ce1b63a6 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/DefaultEnvironmentProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.local; + +import org.elasticsearch.test.cluster.EnvironmentProvider; +import org.elasticsearch.test.cluster.local.LocalClusterSpec.LocalNodeSpec; +import org.elasticsearch.test.cluster.local.distribution.DistributionType; +import org.elasticsearch.test.cluster.util.Version; + +import java.util.HashMap; +import java.util.Map; + +public class DefaultEnvironmentProvider implements EnvironmentProvider { + private static final String HOSTNAME_OVERRIDE = "LinuxDarwinHostname"; + private static final String COMPUTERNAME_OVERRIDE = "WindowsComputername"; + private static final String TESTS_RUNTIME_JAVA_SYSPROP = "tests.runtime.java"; + + @Override + public Map get(LocalNodeSpec nodeSpec) { + Map environment = new HashMap<>(); + + // If we are testing the current version of Elasticsearch, use the configured runtime Java, otherwise use the bundled JDK + if (nodeSpec.getDistributionType() == DistributionType.INTEG_TEST || nodeSpec.getVersion().equals(Version.CURRENT)) { + environment.put("ES_JAVA_HOME", System.getProperty(TESTS_RUNTIME_JAVA_SYSPROP)); + } + + // Override the system hostname variables for testing + environment.put("HOSTNAME", HOSTNAME_OVERRIDE); + environment.put("COMPUTERNAME", COMPUTERNAME_OVERRIDE); + + return environment; + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/DefaultLocalClusterSpecBuilder.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/DefaultLocalClusterSpecBuilder.java new file mode 100644 index 000000000000..20e06fcb8e9b --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/DefaultLocalClusterSpecBuilder.java @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.local; + +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.cluster.local.LocalClusterSpec.LocalNodeSpec; +import org.elasticsearch.test.cluster.local.distribution.DistributionType; +import org.elasticsearch.test.cluster.local.model.User; +import org.elasticsearch.test.cluster.util.Version; +import org.elasticsearch.test.cluster.util.resource.TextResource; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; + +public class DefaultLocalClusterSpecBuilder extends AbstractLocalSpecBuilder implements LocalClusterSpecBuilder { + private String name = "test-cluster"; + private final List nodeBuilders = new ArrayList<>(); + private final List users = new ArrayList<>(); + private final List roleFiles = new ArrayList<>(); + + public DefaultLocalClusterSpecBuilder() { + super(null); + this.settings(new DefaultSettingsProvider()); + this.environment(new DefaultEnvironmentProvider()); + this.rolesFile(TextResource.fromClasspath("default_test_roles.yml")); + } + + @Override + public DefaultLocalClusterSpecBuilder name(String name) { + this.name = name; + return this; + } + + @Override + public DefaultLocalClusterSpecBuilder apply(LocalClusterConfigProvider configProvider) { + configProvider.apply(this); + return this; + } + + @Override + public DefaultLocalClusterSpecBuilder nodes(int nodes) { + if (nodes < nodeBuilders.size()) { + throw new IllegalArgumentException( + "Cannot shrink cluster to " + nodes + ". " + nodeBuilders.size() + " nodes already configured" + ); + } + + int newNodes = nodes - nodeBuilders.size(); + for (int i = 0; i < newNodes; i++) { + nodeBuilders.add(new DefaultLocalNodeSpecBuilder(this)); + } + + return this; + } + + @Override + public DefaultLocalClusterSpecBuilder withNode(Consumer config) { + DefaultLocalNodeSpecBuilder builder = new DefaultLocalNodeSpecBuilder(this); + config.accept(builder); + nodeBuilders.add(builder); + return this; + } + + @Override + public DefaultLocalClusterSpecBuilder node(int index, Consumer config) { + try { + DefaultLocalNodeSpecBuilder builder = nodeBuilders.get(index); + config.accept(builder); + } catch (IndexOutOfBoundsException e) { + throw new IllegalArgumentException( + "No node at index + " + index + " exists. Only " + nodeBuilders.size() + " nodes have been configured" + ); + } + return this; + } + + @Override + public DefaultLocalClusterSpecBuilder user(String username, String password) { + this.users.add(new User(username, password)); + return this; + } + + @Override + public DefaultLocalClusterSpecBuilder user(String username, String password, String role) { + this.users.add(new User(username, password, role)); + return this; + } + + @Override + public DefaultLocalClusterSpecBuilder rolesFile(TextResource rolesFile) { + this.roleFiles.add(rolesFile); + return this; + } + + @Override + public ElasticsearchCluster build() { + List clusterUsers = users.isEmpty() ? List.of(User.DEFAULT_USER) : users; + LocalClusterSpec clusterSpec = new LocalClusterSpec(name, clusterUsers, roleFiles); + List nodeSpecs; + + if (nodeBuilders.isEmpty()) { + // No node-specific configuration so assume a single-node cluster + nodeSpecs = List.of(new DefaultLocalNodeSpecBuilder(this).build(clusterSpec)); + } else { + nodeSpecs = nodeBuilders.stream().map(node -> node.build(clusterSpec)).toList(); + } + + clusterSpec.setNodes(nodeSpecs); + clusterSpec.validate(); + + return new LocalElasticsearchCluster(clusterSpec); + } + + public static class DefaultLocalNodeSpecBuilder extends AbstractLocalSpecBuilder implements LocalNodeSpecBuilder { + private String name; + + protected DefaultLocalNodeSpecBuilder(AbstractLocalSpecBuilder parent) { + super(parent); + } + + @Override + public DefaultLocalNodeSpecBuilder name(String name) { + this.name = name; + return this; + } + + private LocalNodeSpec build(LocalClusterSpec cluster) { + + return new LocalNodeSpec( + cluster, + name, + Version.CURRENT, + getSettingsProviders(), + getSettings(), + getEnvironmentProviders(), + getEnvironment(), + getModules(), + getPlugins(), + Optional.ofNullable(getDistributionType()).orElse(DistributionType.INTEG_TEST), + getFeatures() + ); + } + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/DefaultSettingsProvider.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/DefaultSettingsProvider.java new file mode 100644 index 000000000000..aaf2f786b543 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/DefaultSettingsProvider.java @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.local; + +import org.elasticsearch.test.cluster.SettingsProvider; +import org.elasticsearch.test.cluster.local.LocalClusterSpec.LocalNodeSpec; +import org.elasticsearch.test.cluster.local.distribution.DistributionType; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +public class DefaultSettingsProvider implements SettingsProvider { + @Override + public Map get(LocalNodeSpec nodeSpec) { + Map settings = new HashMap<>(); + + settings.put("node.name", nodeSpec.getName()); + settings.put("node.attr.testattr", "test"); + settings.put("node.portsfile", "true"); + settings.put("http.port", "0"); + settings.put("transport.port", "0"); + + if (nodeSpec.getDistributionType() == DistributionType.INTEG_TEST) { + settings.put("xpack.security.enabled", "false"); + } else { + // Disable deprecation indexing which is enabled by default in 7.16 + if (nodeSpec.getVersion().onOrAfter("7.16.0")) { + settings.put("cluster.deprecation_indexing.enabled", "false"); + } + } + + // Default the watermarks to absurdly low to prevent the tests from failing on nodes without enough disk space + settings.put("cluster.routing.allocation.disk.watermark.low", "1b"); + settings.put("cluster.routing.allocation.disk.watermark.high", "1b"); + settings.put("cluster.routing.allocation.disk.watermark.flood_stage", "1b"); + + // increase script compilation limit since tests can rapid-fire script compilations + if (nodeSpec.getVersion().onOrAfter("7.9.0")) { + settings.put("script.disable_max_compilations_rate", "true"); + } else { + settings.put("script.max_compilations_rate", "2048/1m"); + } + + // Temporarily disable the real memory usage circuit breaker. It depends on real memory usage which we have no full control + // over and the REST client will not retry on circuit breaking exceptions yet (see #31986 for details). Once the REST client + // can retry on circuit breaking exceptions, we can revert again to the default configuration. + settings.put("indices.breaker.total.use_real_memory", "false"); + + // Don't wait for state, just start up quickly. This will also allow new and old nodes in the BWC case to become the master + settings.put("discovery.initial_state_timeout", "0s"); + + if (nodeSpec.getVersion().getMajor() >= 8) { + settings.put("cluster.service.slow_task_logging_threshold", "5s"); + settings.put("cluster.service.slow_master_task_logging_threshold", "5s"); + } + + settings.put("action.destructive_requires_name", "false"); + + // Setup cluster discovery + String nodeNames = nodeSpec.getCluster().getNodes().stream().map(LocalNodeSpec::getName).collect(Collectors.joining(",")); + settings.put("cluster.initial_master_nodes", "[" + nodeNames + "]"); + settings.put("discovery.seed_providers", "file"); + settings.put("discovery.seed_hosts", "[]"); + + return settings; + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalClusterConfigProvider.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalClusterConfigProvider.java new file mode 100644 index 000000000000..7e802247709d --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalClusterConfigProvider.java @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.local; + +public interface LocalClusterConfigProvider { + + void apply(LocalClusterSpecBuilder builder); +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalClusterFactory.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalClusterFactory.java new file mode 100644 index 000000000000..2a4223fc8ff5 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalClusterFactory.java @@ -0,0 +1,503 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.local; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.test.cluster.ClusterFactory; +import org.elasticsearch.test.cluster.local.LocalClusterSpec.LocalNodeSpec; +import org.elasticsearch.test.cluster.local.distribution.DistributionDescriptor; +import org.elasticsearch.test.cluster.local.distribution.DistributionResolver; +import org.elasticsearch.test.cluster.local.distribution.DistributionType; +import org.elasticsearch.test.cluster.local.model.User; +import org.elasticsearch.test.cluster.util.IOUtils; +import org.elasticsearch.test.cluster.util.OS; +import org.elasticsearch.test.cluster.util.Pair; +import org.elasticsearch.test.cluster.util.ProcessReaper; +import org.elasticsearch.test.cluster.util.ProcessUtils; +import org.elasticsearch.test.cluster.util.Retry; +import org.elasticsearch.test.cluster.util.Version; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.time.Duration; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class LocalClusterFactory implements ClusterFactory { + private static final Logger LOGGER = LogManager.getLogger(LocalClusterFactory.class); + private static final Duration NODE_UP_TIMEOUT = Duration.ofMinutes(2); + private static final Map, DistributionDescriptor> TEST_DISTRIBUTIONS = new ConcurrentHashMap<>(); + private static final String TESTS_CLUSTER_MODULES_PATH_SYSPROP = "tests.cluster.modules.path"; + private static final String TESTS_CLUSTER_PLUGINS_PATH_SYSPROP = "tests.cluster.plugins.path"; + + private final Path baseWorkingDir; + private final DistributionResolver distributionResolver; + + public LocalClusterFactory(Path baseWorkingDir, DistributionResolver distributionResolver) { + this.baseWorkingDir = baseWorkingDir; + this.distributionResolver = distributionResolver; + } + + @Override + public LocalClusterHandle create(LocalClusterSpec spec) { + return new LocalClusterHandle(spec.getName(), spec.getNodes().stream().map(Node::new).toList()); + } + + public class Node { + private final LocalNodeSpec spec; + private final Path workingDir; + private final Path distributionDir; + private final Path snapshotsDir; + private final Path dataDir; + private final Path logsDir; + private final Path configDir; + private final Path tempDir; + + private boolean initialized = false; + private Process process = null; + private DistributionDescriptor distributionDescriptor; + + public Node(LocalNodeSpec spec) { + this.spec = spec; + this.workingDir = baseWorkingDir.resolve(spec.getCluster().getName()).resolve(spec.getName()); + this.distributionDir = workingDir.resolve("distro"); // location of es distribution files, typically hard-linked + this.snapshotsDir = workingDir.resolve("repo"); + this.dataDir = workingDir.resolve("data"); + this.logsDir = workingDir.resolve("logs"); + this.configDir = workingDir.resolve("config"); + this.tempDir = workingDir.resolve("tmp"); // elasticsearch temporary directory + } + + public synchronized void start() { + LOGGER.info("Starting Elasticsearch node '{}'", spec.getName()); + + if (initialized == false) { + LOGGER.info("Creating installation for node '{}' in {}", spec.getName(), workingDir); + distributionDescriptor = resolveDistribution(); + LOGGER.info("Distribution for node '{}': {}", spec.getName(), distributionDescriptor); + initializeWorkingDirectory(); + writeConfiguration(); + createKeystore(); + configureSecurity(); + installPlugins(); + if (spec.getDistributionType() == DistributionType.INTEG_TEST) { + installModules(); + } + initialized = true; + } + + startElasticsearch(); + } + + public synchronized void stop(boolean forcibly) { + if (process != null) { + ProcessUtils.stopHandle(process.toHandle(), forcibly); + ProcessReaper.instance().unregister(getServiceName()); + } + } + + public void waitForExit() { + if (process != null) { + ProcessUtils.waitForExit(process.toHandle()); + } + } + + public String getHttpAddress() { + Path portFile = workingDir.resolve("logs").resolve("http.ports"); + if (Files.notExists(portFile)) { + waitUntilReady(); + } + return readPortsFile(portFile).get(0); + } + + public String getTransportEndpoint() { + Path portsFile = workingDir.resolve("logs").resolve("transport.ports"); + if (Files.notExists(portsFile)) { + waitUntilReady(); + } + return readPortsFile(portsFile).get(0); + } + + public LocalNodeSpec getSpec() { + return spec; + } + + Path getWorkingDir() { + return workingDir; + } + + public void waitUntilReady() { + try { + Retry.retryUntilTrue(NODE_UP_TIMEOUT, Duration.ofMillis(500), () -> { + if (process.isAlive() == false) { + throw new RuntimeException( + "Elasticsearch process died while waiting for ports file. See console output for details." + ); + } + + Path httpPorts = workingDir.resolve("logs").resolve("http.ports"); + Path transportPorts = workingDir.resolve("logs").resolve("transport.ports"); + + return Files.exists(httpPorts) && Files.exists(transportPorts); + }); + } catch (TimeoutException e) { + throw new RuntimeException("Timed out after " + NODE_UP_TIMEOUT + " waiting for ports files for: " + this); + } catch (ExecutionException e) { + throw new RuntimeException("An error occurred while waiting for ports file for: " + this, e); + } + } + + private List readPortsFile(Path file) { + try (Stream lines = Files.lines(file, StandardCharsets.UTF_8)) { + return lines.map(String::trim).collect(Collectors.toList()); + } catch (IOException e) { + throw new UncheckedIOException("Unable to read ports file: " + file, e); + } + } + + private void initializeWorkingDirectory() { + try { + IOUtils.deleteWithRetry(workingDir); + try { + IOUtils.syncWithLinks(distributionDescriptor.getDistributionDir(), distributionDir); + } catch (IOUtils.LinkCreationException e) { + // Note does not work for network drives, e.g. Vagrant + LOGGER.info("Failed to create working dir using hard links. Falling back to copy", e); + // ensure we get a clean copy + IOUtils.deleteWithRetry(distributionDir); + IOUtils.syncWithCopy(distributionDescriptor.getDistributionDir(), distributionDir); + } + Files.createDirectories(configDir); + Files.createDirectories(snapshotsDir); + Files.createDirectories(dataDir); + Files.createDirectories(logsDir); + Files.createDirectories(tempDir); + } catch (IOException e) { + throw new UncheckedIOException("Failed to create working directory for node '" + spec.getName() + "'", e); + } + } + + private DistributionDescriptor resolveDistribution() { + return TEST_DISTRIBUTIONS.computeIfAbsent( + Pair.of(spec.getVersion(), spec.getDistributionType()), + key -> distributionResolver.resolve(key.left, key.right) + ); + } + + private void writeConfiguration() { + Path configFile = configDir.resolve("elasticsearch.yml"); + Path jvmOptionsFile = configDir.resolve("jvm.options"); + + try { + // Write settings to elasticsearch.yml + Map pathSettings = new HashMap<>(); + pathSettings.put("path.repo", workingDir.resolve("repo").toString()); + pathSettings.put("path.data", workingDir.resolve("data").toString()); + pathSettings.put("path.logs", workingDir.resolve("logs").toString()); + + Files.writeString( + configFile, + Stream.concat(spec.resolveSettings().entrySet().stream(), pathSettings.entrySet().stream()) + .map(entry -> entry.getKey() + ": " + entry.getValue()) + .collect(Collectors.joining("\n")), + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.CREATE + ); + + // Copy additional configuration from distribution + try (Stream configFiles = Files.list(distributionDir.resolve("config"))) { + for (Path file : configFiles.toList()) { + Path dest = configFile.getParent().resolve(file.getFileName()); + if (Files.exists(dest) == false) { + Files.copy(file, dest); + } + } + } + + // Patch jvm.options file to update paths + String content = Files.readString(jvmOptionsFile); + Map expansions = getJvmOptionsReplacements(); + for (String key : expansions.keySet()) { + if (content.contains(key) == false) { + throw new IOException("Template property '" + key + "' not found in template."); + } + content = content.replace(key, expansions.get(key)); + } + Files.writeString(jvmOptionsFile, content); + } catch (IOException e) { + throw new UncheckedIOException("Could not write config file: " + configFile, e); + } + } + + private void createKeystore() { + try { + ProcessUtils.exec( + workingDir, + OS.conditional( + c -> c.onWindows(() -> distributionDir.resolve("bin").resolve("elasticsearch-keystore.bat")) + .onUnix(() -> distributionDir.resolve("bin").resolve("elasticsearch-keystore")) + ), + getEnvironmentVariables(), + false, + "-v", + "create" + ).waitFor(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void configureSecurity() { + if (spec.isSecurityEnabled()) { + if (spec.getUsers().isEmpty() == false) { + LOGGER.info("Setting up roles.yml for node '{}'", spec.getName()); + + Path destination = workingDir.resolve("config").resolve("roles.yml"); + spec.getRolesFiles().forEach(rolesFile -> { + try { + Files.writeString( + destination, + rolesFile.getText() + System.lineSeparator(), + StandardCharsets.UTF_8, + StandardOpenOption.APPEND + ); + } catch (IOException e) { + throw new UncheckedIOException("Failed to append roles file " + rolesFile + " to " + destination, e); + } + }); + } + + LOGGER.info("Creating users for node '{}'", spec.getName()); + for (User user : spec.getUsers()) { + try { + ProcessUtils.exec( + workingDir, + distributionDir.resolve("bin").resolve("elasticsearch-users"), + getEnvironmentVariables(), + false, + "useradd", + user.getUsername(), + "-p", + user.getPassword(), + "-r", + user.getRole() + ).waitFor(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + } + + private void installPlugins() { + if (spec.getPlugins().isEmpty() == false) { + Pattern pattern = Pattern.compile("(.+)(?:-\\d\\.\\d\\.\\d-SNAPSHOT\\.zip)?"); + + LOGGER.info("Installing plugins {} into node '{}", spec.getPlugins(), spec.getName()); + List pluginPaths = Arrays.stream(System.getProperty(TESTS_CLUSTER_PLUGINS_PATH_SYSPROP).split(File.pathSeparator)) + .map(Path::of) + .toList(); + + List toInstall = spec.getPlugins() + .stream() + .map( + pluginName -> pluginPaths.stream() + .map(path -> Pair.of(pattern.matcher(path.getFileName().toString()), path)) + .filter(pair -> pair.left.matches()) + .map(p -> p.right.getParent().resolve(p.left.group(1))) + .findFirst() + .orElseThrow(() -> { + String taskPath = System.getProperty("tests.task"); + String project = taskPath.substring(0, taskPath.lastIndexOf(':')); + + throw new RuntimeException( + "Unable to locate plugin '" + + pluginName + + "'. Ensure you've added the following to the build script for project '" + + project + + "':\n\n" + + "dependencies {\n" + + " clusterModules " + + "project(':plugins:" + + pluginName + + "')" + + "\n}" + ); + }) + ) + .map(p -> p.toUri().toString()) + .toList(); + + Path pluginCommand = OS.conditional( + c -> c.onWindows(() -> distributionDir.resolve("bin").resolve("elasticsearch-plugin.bat")) + .onUnix(() -> distributionDir.resolve("bin").resolve("elasticsearch-plugin")) + ); + if (spec.getVersion().onOrAfter("7.6.0")) { + try { + ProcessUtils.exec( + workingDir, + pluginCommand, + getEnvironmentVariables(), + false, + Stream.concat(Stream.of("install", "--batch"), toInstall.stream()).toArray(String[]::new) + ).waitFor(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } else { + toInstall.forEach(plugin -> { + try { + ProcessUtils.exec(workingDir, pluginCommand, getEnvironmentVariables(), false, "install", "--batch", plugin) + .waitFor(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + } + } + } + + private void installModules() { + if (spec.getModules().isEmpty() == false) { + LOGGER.info("Installing modules {} into node '{}", spec.getModules(), spec.getName()); + List modulePaths = Arrays.stream(System.getProperty(TESTS_CLUSTER_MODULES_PATH_SYSPROP).split(File.pathSeparator)) + .map(Path::of) + .toList(); + + spec.getModules().forEach(module -> installModule(module, modulePaths)); + } + } + + private void installModule(String moduleName, List modulePaths) { + Path destination = distributionDir.resolve("modules").resolve(moduleName); + if (Files.notExists(destination)) { + Path modulePath = modulePaths.stream().filter(path -> path.endsWith(moduleName)).findFirst().orElseThrow(() -> { + String taskPath = System.getProperty("tests.task"); + String project = taskPath.substring(0, taskPath.lastIndexOf(':')); + String moduleDependency = moduleName.startsWith("x-pack") + ? "project(xpackModule('" + moduleName.substring(7) + "'))" + : "project(':modules:" + moduleName + "')"; + + throw new RuntimeException( + "Unable to locate module '" + + moduleName + + "'. Ensure you've added the following to the build script for project '" + + project + + "':\n\n" + + "dependencies {\n" + + " clusterModules " + + moduleDependency + + "\n}" + ); + + }); + + IOUtils.syncWithCopy(modulePath.getParent(), destination); + + // Install any extended plugins + Properties pluginProperties = new Properties(); + try ( + InputStream in = new BufferedInputStream( + new FileInputStream(modulePath.resolve("plugin-descriptor.properties").toFile()) + ) + ) { + pluginProperties.load(in); + String extendedProperty = pluginProperties.getProperty("extended.plugins"); + if (extendedProperty != null) { + String[] extendedPlugins = extendedProperty.split(","); + for (String plugin : extendedPlugins) { + installModule(plugin, modulePaths); + } + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + private void startElasticsearch() { + process = ProcessUtils.exec( + workingDir, + OS.conditional( + c -> c.onWindows(() -> distributionDir.resolve("bin").resolve("elasticsearch.bat")) + .onUnix(() -> distributionDir.resolve("bin").resolve("elasticsearch")) + ), + getEnvironmentVariables(), + true + ); + + ProcessReaper.instance().registerPid(getServiceName(), process.pid()); + } + + private Map getEnvironmentVariables() { + Map environment = new HashMap<>(spec.resolveEnvironment()); + environment.put("ES_PATH_CONF", workingDir.resolve("config").toString()); + environment.put("ES_TMPDIR", workingDir.resolve("tmp").toString()); + // Windows requires this as it defaults to `c:\windows` despite ES_TMPDIR + environment.put("TMP", workingDir.resolve("tmp").toString()); + + String featureFlagProperties = ""; + if (spec.getFeatures().isEmpty() == false && distributionDescriptor.isSnapshot() == false) { + featureFlagProperties = spec.getFeatures() + .stream() + .filter(f -> spec.getVersion().onOrAfter(f.from) && (f.until == null || spec.getVersion().before(f.until))) + .map(f -> "-D" + f.systemProperty) + .collect(Collectors.joining(" ")); + } + + String heapSize = System.getProperty("tests.heap.size", "512m"); + environment.put("ES_JAVA_OPTS", "-Xms" + heapSize + " -Xmx" + heapSize + " -ea -esa " + // Support passing in additional JVM arguments + + System.getProperty("tests.jvm.argline", "") + + " " + + featureFlagProperties); + + return environment; + } + + private Map getJvmOptionsReplacements() { + Path relativeLogsDir = workingDir.relativize(logsDir); + return Map.of( + "-XX:HeapDumpPath=data", + "-XX:HeapDumpPath=" + relativeLogsDir, + "logs/gc.log", + relativeLogsDir.resolve("gc.log").toString(), + "-XX:ErrorFile=logs/hs_err_pid%p.log", + "-XX:ErrorFile=" + relativeLogsDir.resolve("hs_err_pid%p.log") + ); + } + + private String getServiceName() { + return baseWorkingDir.getFileName() + "-" + spec.getCluster().getName() + "-" + spec.getName(); + } + + @Override + public String toString() { + return "{ cluster: '" + spec.getCluster().getName() + "', node: '" + spec.getName() + "' }"; + } + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalClusterHandle.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalClusterHandle.java new file mode 100644 index 000000000000..3255e4c17914 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalClusterHandle.java @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.local; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.test.cluster.ClusterHandle; +import org.elasticsearch.test.cluster.local.LocalClusterFactory.Node; +import org.elasticsearch.test.cluster.local.model.User; +import org.elasticsearch.test.cluster.util.ExceptionUtils; +import org.elasticsearch.test.cluster.util.Retry; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinWorkerThread; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +public class LocalClusterHandle implements ClusterHandle { + private static final Logger LOGGER = LogManager.getLogger(LocalClusterHandle.class); + private static final Duration CLUSTER_UP_TIMEOUT = Duration.ofSeconds(30); + + public final ForkJoinPool executor = new ForkJoinPool( + Math.max(Runtime.getRuntime().availableProcessors(), 4), + new ForkJoinPool.ForkJoinWorkerThreadFactory() { + private final AtomicLong counter = new AtomicLong(0); + + @Override + public ForkJoinWorkerThread newThread(ForkJoinPool pool) { + ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); + thread.setName(name + "-node-executor-" + counter.getAndIncrement()); + return thread; + } + }, + null, + false + ); + private final AtomicBoolean started = new AtomicBoolean(false); + private final String name; + private final List nodes; + + public LocalClusterHandle(String name, List nodes) { + this.name = name; + this.nodes = nodes; + } + + @Override + public void start() { + if (started.getAndSet(true) == false) { + LOGGER.info("Starting Elasticsearch test cluster '{}'", name); + execute(() -> nodes.parallelStream().forEach(Node::start)); + } + waitUntilReady(); + } + + @Override + public void stop(boolean forcibly) { + if (started.getAndSet(false)) { + LOGGER.info("Stopping Elasticsearch test cluster '{}', forcibly: {}", name, forcibly); + execute(() -> nodes.forEach(n -> n.stop(forcibly))); + } else { + // Make sure the process is stopped, otherwise wait + execute(() -> nodes.forEach(n -> n.waitForExit())); + } + } + + @Override + public boolean isStarted() { + return started.get(); + } + + @Override + public void close() { + stop(false); + + executor.shutdownNow(); + try { + executor.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Override + public String getHttpAddresses() { + start(); + return execute(() -> nodes.parallelStream().map(Node::getHttpAddress).collect(Collectors.joining(","))); + } + + private void waitUntilReady() { + writeUnicastHostsFile(); + try { + Retry.retryUntilTrue(CLUSTER_UP_TIMEOUT, Duration.ZERO, () -> { + Node node = nodes.get(0); + String scheme = node.getSpec().isSettingTrue("xpack.security.http.ssl.enabled") ? "https" : "http"; + WaitForHttpResource wait = new WaitForHttpResource(scheme, node.getHttpAddress(), nodes.size()); + User credentials = node.getSpec().getUsers().get(0); + wait.setUsername(credentials.getUsername()); + wait.setPassword(credentials.getPassword()); + return wait.wait(500); + }); + } catch (TimeoutException e) { + throw new RuntimeException("Timed out after " + CLUSTER_UP_TIMEOUT + " waiting for cluster '" + name + "' status to be yellow"); + } catch (ExecutionException e) { + throw new RuntimeException("An error occurred while checking cluster '" + name + "' status.", e); + } + } + + private void writeUnicastHostsFile() { + String transportUris = execute(() -> nodes.parallelStream().map(Node::getTransportEndpoint).collect(Collectors.joining("\n"))); + nodes.forEach(node -> { + try { + Path hostsFile = node.getWorkingDir().resolve("config").resolve("unicast_hosts.txt"); + if (Files.notExists(hostsFile)) { + Files.writeString(hostsFile, transportUris); + } + } catch (IOException e) { + throw new UncheckedIOException("Failed to write unicast_hosts for: " + node, e); + } + }); + } + + private T execute(Callable task) { + try { + return executor.submit(task).get(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException("An error occurred orchestrating test cluster.", ExceptionUtils.findRootCause(e)); + } + } + + private void execute(Runnable task) { + execute(() -> { + task.run(); + return true; + }); + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalClusterSpec.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalClusterSpec.java new file mode 100644 index 000000000000..aac8c2794279 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalClusterSpec.java @@ -0,0 +1,190 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.local; + +import org.elasticsearch.test.cluster.ClusterSpec; +import org.elasticsearch.test.cluster.EnvironmentProvider; +import org.elasticsearch.test.cluster.FeatureFlag; +import org.elasticsearch.test.cluster.SettingsProvider; +import org.elasticsearch.test.cluster.local.distribution.DistributionType; +import org.elasticsearch.test.cluster.local.model.User; +import org.elasticsearch.test.cluster.util.Version; +import org.elasticsearch.test.cluster.util.resource.TextResource; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class LocalClusterSpec implements ClusterSpec { + private final String name; + private final List users; + private final List roleFiles; + private List nodes; + + public LocalClusterSpec(String name, List users, List roleFiles) { + this.name = name; + this.users = users; + this.roleFiles = roleFiles; + } + + public String getName() { + return name; + } + + public List getUsers() { + return users; + } + + public List getRoleFiles() { + return roleFiles; + } + + public List getNodes() { + return nodes; + } + + public void setNodes(List nodes) { + this.nodes = nodes; + } + + void validate() { + // Ensure we don't have nodes with duplicate names + List nodeNames = nodes.stream().map(LocalNodeSpec::getName).collect(Collectors.toList()); + Set uniqueNames = nodes.stream().map(LocalNodeSpec::getName).collect(Collectors.toSet()); + uniqueNames.forEach(name -> nodeNames.remove(nodeNames.indexOf(name))); + + if (nodeNames.isEmpty() == false) { + throw new IllegalArgumentException("Cluster cannot contain nodes with duplicates names: " + nodeNames); + } + } + + public static class LocalNodeSpec { + private final LocalClusterSpec cluster; + private final String name; + private final Version version; + private final List settingsProviders; + private final Map settings; + private final List environmentProviders; + private final Map environment; + private final Set modules; + private final Set plugins; + private final DistributionType distributionType; + private final Set features; + + public LocalNodeSpec( + LocalClusterSpec cluster, + String name, + Version version, + List settingsProviders, + Map settings, + List environmentProviders, + Map environment, + Set modules, + Set plugins, + DistributionType distributionType, + Set features + ) { + this.cluster = cluster; + this.name = name; + this.version = version; + this.settingsProviders = settingsProviders; + this.settings = settings; + this.environmentProviders = environmentProviders; + this.environment = environment; + this.modules = modules; + this.plugins = plugins; + this.distributionType = distributionType; + this.features = features; + } + + public LocalClusterSpec getCluster() { + return cluster; + } + + public String getName() { + return name == null ? cluster.getName() + "-" + cluster.getNodes().indexOf(this) : name; + } + + public Version getVersion() { + return version; + } + + public List getUsers() { + return cluster.getUsers(); + } + + public List getRolesFiles() { + return cluster.getRoleFiles(); + } + + public DistributionType getDistributionType() { + return distributionType; + } + + public Set getModules() { + return modules; + } + + public Set getPlugins() { + return plugins; + } + + public Set getFeatures() { + return features; + } + + public boolean isSecurityEnabled() { + return Boolean.parseBoolean( + resolveSettings().getOrDefault("xpack.security.enabled", getVersion().onOrAfter("8.0.0") ? "true" : "false") + ); + } + + public boolean isSettingTrue(String setting) { + return Boolean.parseBoolean(resolveSettings().getOrDefault(setting, "false")); + } + + /** + * Resolve node settings. Order of precedence is as follows: + *
      + *
    1. Settings from cluster configured {@link SettingsProvider}
    2. + *
    3. Settings from node configured {@link SettingsProvider}
    4. + *
    5. Explicit cluster settings
    6. + *
    7. Explicit node settings
    8. + *
    + * + * @return resolved settings for node + */ + public Map resolveSettings() { + Map resolvedSettings = new HashMap<>(); + settingsProviders.forEach(p -> resolvedSettings.putAll(p.get(this))); + resolvedSettings.putAll(settings); + return resolvedSettings; + } + + /** + * Resolve node environment variables. Order of precedence is as follows: + *
      + *
    1. Environment variables from cluster configured {@link EnvironmentProvider}
    2. + *
    3. Environment variables from node configured {@link EnvironmentProvider}
    4. + *
    5. Environment variables cluster settings
    6. + *
    7. Environment variables node settings
    8. + *
    + * + * @return resolved environment variables for node + */ + public Map resolveEnvironment() { + Map resolvedEnvironment = new HashMap<>(); + environmentProviders.forEach(p -> resolvedEnvironment.putAll(p.get(this))); + resolvedEnvironment.putAll(environment); + return resolvedEnvironment; + } + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalClusterSpecBuilder.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalClusterSpecBuilder.java new file mode 100644 index 000000000000..63b5ca00f248 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalClusterSpecBuilder.java @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.local; + +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.cluster.util.resource.TextResource; + +import java.util.function.Consumer; + +public interface LocalClusterSpecBuilder extends LocalSpecBuilder { + /** + * Sets the node name. By default, "test-cluster" is used. + */ + LocalClusterSpecBuilder name(String name); + + LocalClusterSpecBuilder apply(LocalClusterConfigProvider configProvider); + + /** + * Sets the number of nodes for the cluster. + */ + LocalClusterSpecBuilder nodes(int nodes); + + /** + * Adds a new node to the cluster and configures the node. + */ + LocalClusterSpecBuilder withNode(Consumer config); + + /** + * Configures an existing node. + * + * @param index the index of the node to configure + * @param config configuration to apply to the node + */ + LocalClusterSpecBuilder node(int index, Consumer config); + + /** + * Register a user using the default test role. + */ + LocalClusterSpecBuilder user(String username, String password); + + /** + * Register a user using the given role. + */ + LocalClusterSpecBuilder user(String username, String password, String role); + + /** + * Register a roles file with cluster via the supplied {@link TextResource}. + */ + LocalClusterSpecBuilder rolesFile(TextResource rolesFile); + + ElasticsearchCluster build(); +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalElasticsearchCluster.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalElasticsearchCluster.java new file mode 100644 index 000000000000..55e41255bb6e --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalElasticsearchCluster.java @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.local; + +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.cluster.local.distribution.LocalDistributionResolver; +import org.elasticsearch.test.cluster.local.distribution.SnapshotDistributionResolver; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.nio.file.Path; + +public class LocalElasticsearchCluster implements ElasticsearchCluster { + private final LocalClusterSpec spec; + private LocalClusterHandle handle; + + public LocalElasticsearchCluster(LocalClusterSpec spec) { + this.spec = spec; + } + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + try { + handle = new LocalClusterFactory( + Path.of(System.getProperty("java.io.tmpdir")).resolve(description.getDisplayName()).toAbsolutePath(), + new LocalDistributionResolver(new SnapshotDistributionResolver()) + ).create(spec); + handle.start(); + base.evaluate(); + } finally { + close(); + } + } + }; + } + + @Override + public void start() { + checkHandle(); + handle.start(); + } + + @Override + public void stop(boolean forcibly) { + checkHandle(); + handle.stop(forcibly); + } + + @Override + public boolean isStarted() { + checkHandle(); + return handle.isStarted(); + } + + @Override + public void close() { + checkHandle(); + handle.close(); + } + + @Override + public String getHttpAddresses() { + checkHandle(); + return handle.getHttpAddresses(); + } + + private void checkHandle() { + if (handle == null) { + throw new IllegalStateException("Cluster handle has not been initialized. Did you forget the @ClassRule annotation?"); + } + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalNodeSpecBuilder.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalNodeSpecBuilder.java new file mode 100644 index 000000000000..f9b28407aaa4 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalNodeSpecBuilder.java @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.local; + +public interface LocalNodeSpecBuilder extends LocalSpecBuilder { + + /** + * Sets the node name. By default, nodes are named after the cluster with an incrementing suffix (ex: my-cluster-0). + */ + LocalNodeSpecBuilder name(String name); +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalSpecBuilder.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalSpecBuilder.java new file mode 100644 index 000000000000..0f6fcebd30df --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/LocalSpecBuilder.java @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.local; + +import org.elasticsearch.test.cluster.EnvironmentProvider; +import org.elasticsearch.test.cluster.FeatureFlag; +import org.elasticsearch.test.cluster.SettingsProvider; +import org.elasticsearch.test.cluster.local.distribution.DistributionType; + +interface LocalSpecBuilder> { + /** + * Register a {@link SettingsProvider}. + */ + T settings(SettingsProvider settingsProvider); + + /** + * Add a new node setting. + */ + T setting(String setting, String value); + + /** + * Register a {@link EnvironmentProvider}. + */ + T environment(EnvironmentProvider environmentProvider); + + /** + * Add a new node environment variable. + */ + T environment(String key, String value); + + /** + * Set the cluster {@link DistributionType}. By default, the {@link DistributionType#INTEG_TEST} distribution is used. + */ + T distribution(DistributionType type); + + /** + * Ensure module is installed into the distribution when using the {@link DistributionType#INTEG_TEST} distribution. This is ignored + * when the {@link DistributionType#DEFAULT} is being used. + */ + T module(String moduleName); + + /** + * Ensure plugin is installed into the distribution. + */ + T plugin(String pluginName); + + /** + * Require feature to be enabled in the cluster. + */ + T feature(FeatureFlag feature); +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/WaitForHttpResource.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/WaitForHttpResource.java new file mode 100644 index 000000000000..edab2cdf1e7e --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/WaitForHttpResource.java @@ -0,0 +1,219 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.local; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +/** + * A utility to wait for a specific HTTP resource to be available, optionally with customized TLS trusted CAs. + * This is logically similar to using the Ant Get task to retrieve a resource, but with the difference that it can + * access resources that do not use the JRE's default trusted CAs. + */ +public class WaitForHttpResource { + + private static final Logger logger = LogManager.getLogger(WaitForHttpResource.class); + + private Set validResponseCodes = Collections.singleton(200); + private URL url; + private Set certificateAuthorities; + private File trustStoreFile; + private String trustStorePassword; + private String username; + private String password; + + public WaitForHttpResource(String protocol, String host, int numberOfNodes) throws MalformedURLException { + this(new URL(protocol + "://" + host + "/_cluster/health?wait_for_nodes=>=" + numberOfNodes + "&wait_for_status=yellow")); + } + + public WaitForHttpResource(URL url) { + this.url = url; + } + + public void setValidResponseCodes(int... validResponseCodes) { + this.validResponseCodes = new HashSet<>(validResponseCodes.length); + for (int rc : validResponseCodes) { + this.validResponseCodes.add(rc); + } + } + + public void setCertificateAuthorities(File... certificateAuthorities) { + this.certificateAuthorities = new HashSet<>(Arrays.asList(certificateAuthorities)); + } + + public void setTrustStoreFile(File trustStoreFile) { + this.trustStoreFile = trustStoreFile; + } + + public void setTrustStorePassword(String trustStorePassword) { + this.trustStorePassword = trustStorePassword; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setPassword(String password) { + this.password = password; + } + + public boolean wait(int durationInMs) throws GeneralSecurityException, InterruptedException, IOException { + final long waitUntil = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(durationInMs); + final long sleep = Long.max(durationInMs / 10, 100); + + final SSLContext ssl; + final KeyStore trustStore = buildTrustStore(); + if (trustStore != null) { + ssl = createSslContext(trustStore); + } else { + ssl = null; + } + IOException failure = null; + while (true) { + try { + checkResource(ssl); + return true; + } catch (IOException e) { + logger.debug("Failed to access resource [{}]", url, e); + failure = e; + } + if (System.nanoTime() < waitUntil) { + Thread.sleep(sleep); + } else { + throw failure; + } + } + } + + protected void checkResource(SSLContext ssl) throws IOException { + final HttpURLConnection connection = buildConnection(ssl); + connection.connect(); + final Integer response = connection.getResponseCode(); + if (validResponseCodes.contains(response)) { + logger.info("Got successful response [{}] from URL [{}]", response, url); + return; + } else { + throw new IOException(response + " " + connection.getResponseMessage()); + } + } + + HttpURLConnection buildConnection(SSLContext ssl) throws IOException { + final HttpURLConnection connection = (HttpURLConnection) this.url.openConnection(); + configureSslContext(connection, ssl); + configureBasicAuth(connection); + connection.setRequestMethod("GET"); + return connection; + } + + private void configureSslContext(HttpURLConnection connection, SSLContext ssl) { + if (ssl != null) { + if (connection instanceof HttpsURLConnection) { + ((HttpsURLConnection) connection).setSSLSocketFactory(ssl.getSocketFactory()); + } else { + throw new IllegalStateException("SSL trust has been configured, but [" + url + "] is not a 'https' URL"); + } + } + } + + private void configureBasicAuth(HttpURLConnection connection) { + if (username != null) { + if (password == null) { + throw new IllegalStateException("Basic Auth user [" + username + "] has been set, but no password has been configured"); + } + connection.setRequestProperty( + "Authorization", + "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8)) + ); + } + } + + KeyStore buildTrustStore() throws GeneralSecurityException, IOException { + if (this.certificateAuthorities != null) { + if (trustStoreFile != null) { + throw new IllegalStateException("Cannot specify both truststore and CAs"); + } + return buildTrustStoreFromCA(); + } else if (trustStoreFile != null) { + return buildTrustStoreFromFile(); + } else { + return null; + } + } + + private KeyStore buildTrustStoreFromFile() throws GeneralSecurityException, IOException { + KeyStore keyStore = KeyStore.getInstance(trustStoreFile.getName().endsWith(".jks") ? "JKS" : "PKCS12"); + try (InputStream input = new FileInputStream(trustStoreFile)) { + keyStore.load(input, trustStorePassword == null ? null : trustStorePassword.toCharArray()); + } + return keyStore; + } + + private KeyStore buildTrustStoreFromCA() throws GeneralSecurityException, IOException { + final KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType()); + store.load(null, null); + final CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + int counter = 0; + for (File ca : certificateAuthorities) { + try (InputStream input = new FileInputStream(ca)) { + for (Certificate certificate : certFactory.generateCertificates(input)) { + store.setCertificateEntry("cert-" + counter, certificate); + counter++; + } + } + } + return store; + } + + private SSLContext createSslContext(KeyStore trustStore) throws GeneralSecurityException { + checkForTrustEntry(trustStore); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(trustStore); + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); + sslContext.init(new KeyManager[0], tmf.getTrustManagers(), new SecureRandom()); + return sslContext; + } + + private void checkForTrustEntry(KeyStore trustStore) throws KeyStoreException { + Enumeration enumeration = trustStore.aliases(); + while (enumeration.hasMoreElements()) { + if (trustStore.isCertificateEntry(enumeration.nextElement())) { + // found trusted cert entry + return; + } + } + throw new IllegalStateException("Trust-store does not contain any trusted certificate entries"); + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/DefaultDistributionDescriptor.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/DefaultDistributionDescriptor.java new file mode 100644 index 000000000000..ca0f92b9139a --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/DefaultDistributionDescriptor.java @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.local.distribution; + +import org.elasticsearch.test.cluster.util.Version; + +import java.nio.file.Path; + +public class DefaultDistributionDescriptor implements DistributionDescriptor { + private final Version version; + private final boolean snapshot; + private final Path distributionDir; + private final DistributionType type; + + public DefaultDistributionDescriptor(Version version, boolean snapshot, Path distributionDir, DistributionType type) { + this.version = version; + this.snapshot = snapshot; + this.distributionDir = distributionDir; + this.type = type; + } + + public Version getVersion() { + return version; + } + + public boolean isSnapshot() { + return snapshot; + } + + public Path getDistributionDir() { + return distributionDir; + } + + public DistributionType getType() { + return type; + } + + @Override + public String toString() { + return "DefaultDistributionDescriptor{" + + "version=" + + version + + ", snapshot=" + + snapshot + + ", distributionDir=" + + distributionDir + + ", type=" + + type + + '}'; + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/DistributionDescriptor.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/DistributionDescriptor.java new file mode 100644 index 000000000000..fb213a87d038 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/DistributionDescriptor.java @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.local.distribution; + +import org.elasticsearch.test.cluster.util.Version; + +import java.nio.file.Path; + +public interface DistributionDescriptor { + Version getVersion(); + + boolean isSnapshot(); + + Path getDistributionDir(); + + DistributionType getType(); +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/DistributionResolver.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/DistributionResolver.java new file mode 100644 index 000000000000..93e6d20d7071 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/DistributionResolver.java @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.local.distribution; + +import org.elasticsearch.test.cluster.util.Version; + +public interface DistributionResolver { + + DistributionDescriptor resolve(Version version, DistributionType type); +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/DistributionType.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/DistributionType.java new file mode 100644 index 000000000000..6e19c0fa160d --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/DistributionType.java @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.local.distribution; + +public enum DistributionType { + INTEG_TEST("tests.integ-test.distribution"), + DEFAULT("tests.default.distribution"); + + private final String systemProperty; + + DistributionType(String systemProperty) { + this.systemProperty = systemProperty; + } + + public String getSystemProperty() { + return systemProperty; + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/LocalDistributionResolver.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/LocalDistributionResolver.java new file mode 100644 index 000000000000..3c09b75fe9f6 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/LocalDistributionResolver.java @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.local.distribution; + +import org.elasticsearch.test.cluster.util.Version; + +import java.nio.file.Files; +import java.nio.file.Path; + +public class LocalDistributionResolver implements DistributionResolver { + private final DistributionResolver delegate; + + public LocalDistributionResolver(DistributionResolver delegate) { + this.delegate = delegate; + } + + @Override + public DistributionDescriptor resolve(Version version, DistributionType type) { + if (version.equals(Version.CURRENT)) { + String testDistributionPath = System.getProperty(type.getSystemProperty()); + + if (testDistributionPath == null) { + if (type == DistributionType.DEFAULT) { + String taskPath = System.getProperty("tests.task"); + String project = taskPath.substring(0, taskPath.lastIndexOf(':')); + String taskName = taskPath.substring(taskPath.lastIndexOf(':') + 1); + + throw new IllegalStateException( + "Cannot locate Elasticsearch distribution. Ensure you've added the following to the build script for project '" + + project + + "':\n\n" + + "tasks.named('" + + taskName + + "').configure {\n" + + " usesDefaultDistribution()\n" + + "}" + ); + } else { + throw new IllegalStateException( + "Cannot locate Elasticsearch distribution. The '" + type.getSystemProperty() + "' system property is null." + ); + } + } + + Path testDistributionDir = Path.of(testDistributionPath); + if (Files.notExists(testDistributionDir)) { + throw new IllegalStateException( + "Cannot locate Elasticsearch distribution. Directory at '" + testDistributionDir + "' does not exist." + ); + } + + return new DefaultDistributionDescriptor( + version, + Boolean.parseBoolean(System.getProperty("build.snapshot", "true")), + testDistributionDir, + type + ); + } + + return delegate.resolve(version, type); + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/SnapshotDistributionResolver.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/SnapshotDistributionResolver.java new file mode 100644 index 000000000000..182dbe66a584 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/distribution/SnapshotDistributionResolver.java @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.local.distribution; + +import org.elasticsearch.test.cluster.util.Version; + +public class SnapshotDistributionResolver implements DistributionResolver { + @Override + public DistributionDescriptor resolve(Version version, DistributionType type) { + // Not yet implemented + throw new UnsupportedOperationException("Cannot resolve distribution for version " + version); + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/model/User.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/model/User.java new file mode 100644 index 000000000000..f056bbb4d0bd --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/local/model/User.java @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.local.model; + +public class User { + public static final User DEFAULT_USER = new User("test_user", "x-pack-test-password", "_es_test_root"); + + private final String username; + private final String password; + private final String role; + + public User(String username, String password) { + this.username = username; + this.password = password; + this.role = "_es_test_root"; + } + + public User(String username, String password, String role) { + this.username = username; + this.password = password; + this.role = role; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public String getRole() { + return role; + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/ExceptionUtils.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/ExceptionUtils.java new file mode 100644 index 000000000000..dd3fc999d8b0 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/ExceptionUtils.java @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.util; + +public final class ExceptionUtils { + private ExceptionUtils() {} + + public static Throwable findRootCause(Throwable t) { + Throwable cause = t.getCause(); + if (cause == null) { + return t; + } + + return findRootCause(cause); + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/IOUtils.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/IOUtils.java new file mode 100644 index 000000000000..980aa6a272f3 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/IOUtils.java @@ -0,0 +1,199 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.util; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.function.BiConsumer; +import java.util.stream.Stream; + +public final class IOUtils { + private static final int RETRY_DELETE_MILLIS = OS.current() == OS.WINDOWS ? 500 : 0; + private static final int MAX_RETRY_DELETE_TIMES = OS.current() == OS.WINDOWS ? 15 : 0; + + private IOUtils() {} + + /** + * Deletes a path, retrying if necessary. + * + * @param path the path to delete + * @throws IOException + * if an I/O error occurs + */ + public static void deleteWithRetry(Path path) throws IOException { + try { + deleteWithRetry0(path); + } catch (InterruptedException x) { + throw new IOException("Interrupted while deleting.", x); + } + } + + /** Unchecked variant of deleteWithRetry. */ + public static void uncheckedDeleteWithRetry(Path path) { + try { + deleteWithRetry0(path); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (InterruptedException x) { + throw new UncheckedIOException("Interrupted while deleting.", new IOException()); + } + } + + /** + * Does the equivalent of `cp -lr` and `chmod -r a-w` to save space and improve speed. + * We remove write permissions to make sure files are note mistakenly edited ( e.x. the config file ) and changes + * reflected across all copies. Permissions are retained to be able to replace the links. + * + * @param sourceRoot where to copy from + * @param destinationRoot destination to link to + */ + public static void syncWithLinks(Path sourceRoot, Path destinationRoot) { + sync(sourceRoot, destinationRoot, (Path d, Path s) -> { + try { + Files.createLink(d, s); + } catch (IOException e) { + // Note does not work for network drives, e.g. Vagrant + throw new LinkCreationException("Failed to create hard link " + d + " pointing to " + s, e); + } + }); + } + + /** + * Sync source folder to destination folder. This method does an actual copy of file contents. When possible, + * {@link #syncWithLinks(Path, Path)} is preferred for better performance when the synced contents don't need to be subsequently + * modified. + * + * @param sourceRoot where to copy from + * @param destinationRoot destination to link to + */ + public static void syncWithCopy(Path sourceRoot, Path destinationRoot) { + sync(sourceRoot, destinationRoot, (Path d, Path s) -> { + try { + Files.copy(s, d); + } catch (IOException e) { + throw new UncheckedIOException("Failed to copy " + s + " to " + d, e); + } + }); + } + + public static String readStringFromStream(InputStream in) { + try { + return new String(in.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + } + + private static void sync(Path sourceRoot, Path destinationRoot, BiConsumer syncMethod) { + assert Files.exists(destinationRoot) == false; + try (Stream stream = Files.walk(sourceRoot)) { + stream.forEach(source -> { + Path relativeDestination = sourceRoot.relativize(source); + if (relativeDestination.getNameCount() <= 1) { + return; + } + // Throw away the first name as the archives have everything in a single top level folder we are not interested in + relativeDestination = relativeDestination.subpath(1, relativeDestination.getNameCount()); + + Path destination = destinationRoot.resolve(relativeDestination); + if (Files.isDirectory(source)) { + try { + Files.createDirectories(destination); + } catch (IOException e) { + throw new UncheckedIOException("Can't create directory " + destination.getParent(), e); + } + } else { + try { + Files.createDirectories(destination.getParent()); + } catch (IOException e) { + throw new UncheckedIOException("Can't create directory " + destination.getParent(), e); + } + syncMethod.accept(destination, source); + } + }); + } catch (UncheckedIOException e) { + if (e.getCause()instanceof NoSuchFileException cause) { + // Ignore these files that are sometimes left behind by the JVM + if (cause.getFile() == null || cause.getFile().contains(".attach_pid") == false) { + throw new UncheckedIOException(cause); + } + } else { + throw e; + } + } catch (IOException e) { + throw new UncheckedIOException("Can't walk source " + sourceRoot, e); + } + } + + // The exception handling here is loathsome, but necessary! + // TODO: Some of the loathsomeness here was copied from our Gradle plugin that was required because of Gradle exception wrapping. That + // may no longer be strictly necessary in this context. + private static void deleteWithRetry0(Path path) throws IOException, InterruptedException { + int times = 0; + IOException ioe = null; + while (true) { + try { + recursiveDelete(path); + times++; + // Checks for absence of the file. Semantics of Files.exists() is not the same. + while (Files.notExists(path) == false) { + if (times > MAX_RETRY_DELETE_TIMES) { + throw new IOException("File still exists after " + times + " waits."); + } + Thread.sleep(RETRY_DELETE_MILLIS); + // retry + recursiveDelete(path); + times++; + } + break; + } catch (NoSuchFileException ignore) { + // already deleted, ignore + break; + } catch (IOException x) { + if (x.getCause() instanceof NoSuchFileException) { + // already deleted, ignore + break; + } + // Backoff/retry in case another process is accessing the file + times++; + if (ioe == null) ioe = new IOException(); + ioe.addSuppressed(x); + if (times > MAX_RETRY_DELETE_TIMES) throw ioe; + Thread.sleep(RETRY_DELETE_MILLIS); + } + } + } + + private static void recursiveDelete(Path path) throws IOException { + try (Stream files = Files.walk(path)) { + files.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } + } + + public static class LinkCreationException extends UncheckedIOException { + LinkCreationException(String message, IOException cause) { + super(message, cause); + } + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/OS.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/OS.java new file mode 100644 index 000000000000..7ea0daaa3b24 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/OS.java @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.util; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public enum OS { + WINDOWS, + MAC, + LINUX; + + public static OS current() { + String os = System.getProperty("os.name", ""); + if (os.startsWith("Windows")) { + return OS.WINDOWS; + } + if (os.startsWith("Linux") || os.startsWith("LINUX")) { + return OS.LINUX; + } + if (os.startsWith("Mac")) { + return OS.MAC; + } + throw new IllegalStateException("Can't determine OS from: " + os); + } + + public static class Conditional { + + private final Map> conditions = new HashMap<>(); + + public Conditional onWindows(Supplier supplier) { + conditions.put(WINDOWS, supplier); + return this; + } + + public Conditional onLinux(Supplier supplier) { + conditions.put(LINUX, supplier); + return this; + } + + public Conditional onMac(Supplier supplier) { + conditions.put(MAC, supplier); + return this; + } + + public Conditional onUnix(Supplier supplier) { + conditions.put(MAC, supplier); + conditions.put(LINUX, supplier); + return this; + } + + T supply() { + HashSet missingOS = new HashSet<>(Arrays.asList(OS.values())); + missingOS.removeAll(conditions.keySet()); + if (missingOS.isEmpty() == false) { + throw new IllegalArgumentException("No condition specified for " + missingOS); + } + return conditions.get(OS.current()).get(); + } + + } + + public static T conditional(Consumer> config) { + Conditional conditional = new Conditional<>(); + config.accept(conditional); + + return conditional.supply(); + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/Pair.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/Pair.java new file mode 100644 index 000000000000..863cbce1c9e9 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/Pair.java @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.util; + +import java.util.Objects; + +public final class Pair { + public final L left; + public final R right; + + private Pair(L left, R right) { + this.left = left; + this.right = right; + } + + public static Pair of(L left, R right) { + return new Pair<>(left, right); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Pair pair = (Pair) o; + return Objects.equals(left, pair.left) && Objects.equals(right, pair.right); + } + + @Override + public int hashCode() { + return Objects.hash(left, right); + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/ProcessReaper.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/ProcessReaper.java new file mode 100644 index 000000000000..7d3d19a204cb --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/ProcessReaper.java @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.util; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ProcessReaper { + private static final String REAPER_CLASS = "org/elasticsearch/gradle/reaper/Reaper.class"; + private static final Pattern REAPER_JAR_PATH_PATTERN = Pattern.compile("file:(.*)!/" + REAPER_CLASS); + private static final Logger LOGGER = LogManager.getLogger(ProcessReaper.class); + private static final ProcessReaper INSTANCE = new ProcessReaper(); + private final Path reaperDir; + + private volatile Process process; + + private ProcessReaper() { + try { + this.reaperDir = Files.createTempDirectory("reaper-"); + } catch (IOException e) { + throw new UncheckedIOException("Failed to create process reaper working directory.", e); + } + + Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown, "process-reaper-shutdown")); + } + + public static ProcessReaper instance() { + return INSTANCE; + } + + /** + * Register a pid that will be killed by the reaper. + */ + public void registerPid(String serviceId, long pid) { + String[] killPidCommand = OS.conditional( + c -> c.onWindows(() -> new String[] { "Taskkill", "/F", "/PID", String.valueOf(pid) }) + .onUnix(() -> new String[] { "kill", "-9", String.valueOf(pid) }) + ); + registerCommand(serviceId, killPidCommand); + } + + public void unregister(String serviceId) { + try { + Files.deleteIfExists(getCmdFile(serviceId)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private void registerCommand(String serviceId, String... command) { + ensureReaperStarted(); + + try { + Files.writeString(getCmdFile(serviceId), String.join(" ", command)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private Path getCmdFile(String serviceId) { + return reaperDir.resolve(serviceId.replaceAll("[^a-zA-Z0-9]", "-") + ".cmd"); + } + + void shutdown() { + if (process != null) { + ensureReaperAlive(); + try { + process.getOutputStream().close(); + LOGGER.info("Waiting for reaper to exit normally"); + if (process.waitFor() != 0) { + throw new RuntimeException("Reaper process failed. Check log at " + reaperDir.resolve("reaper.log") + " for details"); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + } + } + + private synchronized void ensureReaperStarted() { + if (process == null) { + try { + Path jarPath = locateReaperJar(); + + // ensure the input directory exists + Files.createDirectories(reaperDir); + // start the reaper + ProcessBuilder builder = new ProcessBuilder( + findJavaHome().resolve("bin") + .resolve(OS.conditional(c -> c.onWindows(() -> "java.exe").onUnix(() -> "java"))) + .toString(), // same jvm as gradle + "-Xms4m", + "-Xmx16m", // no need for a big heap, just need to read some files and execute + "-jar", + jarPath.toString(), + reaperDir.toString() + ); + LOGGER.info("Launching reaper: " + String.join(" ", builder.command())); + // be explicit for stdin, we use closing of the pipe to signal shutdown to the reaper + builder.redirectInput(ProcessBuilder.Redirect.PIPE); + File logFile = reaperDir.resolve("reaper.log").toFile(); + builder.redirectOutput(logFile); + builder.redirectErrorStream(); + process = builder.start(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + ensureReaperAlive(); + } + } + + private Path locateReaperJar() { + URL main = this.getClass().getClassLoader().getResource(REAPER_CLASS); + if (main != null) { + String mainPath = main.getFile(); + Matcher matcher = REAPER_JAR_PATH_PATTERN.matcher(mainPath); + + if (matcher.matches()) { + String path = matcher.group(1); + return Path.of(OS.conditional(c -> c.onWindows(() -> path.substring(1)).onUnix(() -> path))); + } + } + + throw new RuntimeException("Unable to locate " + REAPER_CLASS + " on classpath."); + } + + private void ensureReaperAlive() { + if (process.isAlive() == false) { + throw new IllegalStateException("Reaper process died unexpectedly! Check the log at " + reaperDir.resolve("reaper.log")); + } + } + + private Path findJavaHome() { + Path javaBase = Path.of(System.getProperty("java.home")); + return javaBase.endsWith("jre") && Files.exists(javaBase.getParent().resolve("bin/java")) ? javaBase.getParent() : javaBase; + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/ProcessUtils.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/ProcessUtils.java new file mode 100644 index 000000000000..2e41e89a3eca --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/ProcessUtils.java @@ -0,0 +1,177 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.util; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; + +public final class ProcessUtils { + private static final Logger LOGGER = LogManager.getLogger(ProcessUtils.class); + private static final Logger PROCESS_LOGGER = LogManager.getLogger("process-output"); + private static final Duration PROCESS_DESTROY_TIMEOUT = Duration.ofSeconds(20); + + private ProcessUtils() {} + + public static Process exec(Path workingDir, Path executable, Map environment, boolean inheritIO, String... args) { + return exec(null, workingDir, executable, environment, inheritIO, args); + } + + public static Process exec( + String input, + Path workingDir, + Path executable, + Map environment, + boolean inheritIO, + String... args + ) { + Process process; + Path inputFile = null; + + if (Files.exists(executable) == false) { + throw new IllegalArgumentException("Can't run executable: `" + executable + "` does not exist."); + } + + ProcessBuilder processBuilder = new ProcessBuilder(); + List command = new ArrayList<>(); + command.addAll( + OS.conditional( + c -> c.onWindows(() -> List.of("cmd", "/c", executable.toAbsolutePath().toString())) + .onUnix(() -> List.of(executable.toAbsolutePath().toString())) + ) + ); + command.addAll(Arrays.asList(args)); + + processBuilder.command(command); + processBuilder.directory(workingDir.toFile()); + processBuilder.environment().clear(); + processBuilder.environment().putAll(environment); + + if (input != null) { + try { + inputFile = Files.createTempFile("exec-input-", ".tmp"); + Files.writeString(inputFile, input); + processBuilder.redirectInput(inputFile.toFile()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + try { + process = processBuilder.start(); + Process finalProcess = process; + + startLoggingThread( + finalProcess.getInputStream(), + inheritIO ? System.out::println : PROCESS_LOGGER::info, + executable.getFileName().toString() + ); + + startLoggingThread( + finalProcess.getErrorStream(), + inheritIO ? System.err::println : PROCESS_LOGGER::error, + executable.getFileName().toString() + ); + } catch (IOException e) { + throw new UncheckedIOException("Error executing process: " + executable.getFileName(), e); + } finally { + if (inputFile != null) { + IOUtils.uncheckedDeleteWithRetry(inputFile); + } + } + + return process; + } + + public static void stopHandle(ProcessHandle processHandle, boolean forcibly) { + // No-op if the process has already exited by itself. + if (processHandle.isAlive() == false) { + return; + } + + // Stop all children last - if the ML processes are killed before the ES JVM then + // they'll be recorded as having failed and won't restart when the cluster restarts. + // ES could actually be a child when there's some wrapper process like on Windows, + // and in that case the ML processes will be grandchildren of the wrapper. + List children = processHandle.children().toList(); + try { + LOGGER.info("Terminating Elasticsearch process {}: {}", forcibly ? " forcibly " : "gracefully", processHandle.info()); + + if (forcibly) { + processHandle.destroyForcibly(); + } else { + processHandle.destroy(); + waitForProcessToExit(processHandle); + if (processHandle.isAlive() == false) { + return; + } + LOGGER.info("Process did not terminate after {}, stopping it forcefully", PROCESS_DESTROY_TIMEOUT); + processHandle.destroyForcibly(); + } + + waitForProcessToExit(processHandle); + + if (processHandle.isAlive()) { + throw new RuntimeException("Failed to terminate terminate elasticsearch process."); + } + } finally { + children.forEach(each -> stopHandle(each, forcibly)); + } + } + + public static void waitForExit(ProcessHandle processHandle) { + // No-op if the process has already exited by itself. + if (processHandle.isAlive() == false) { + return; + } + + waitForProcessToExit(processHandle); + } + + private static void waitForProcessToExit(ProcessHandle processHandle) { + try { + Retry.retryUntilTrue(PROCESS_DESTROY_TIMEOUT, Duration.ofSeconds(1), () -> { + processHandle.destroy(); + return processHandle.isAlive() == false; + }); + } catch (ExecutionException e) { + LOGGER.info("Failure while waiting for process to exist", e); + } catch (TimeoutException e) { + LOGGER.info("Timed out waiting for process to exit", e); + } + } + + private static void startLoggingThread(InputStream is, Consumer logAppender, String name) { + new Thread(() -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) { + String line; + while ((line = reader.readLine()) != null) { + logAppender.accept(line); + } + } catch (IOException e) { + throw new UncheckedIOException("Error reading output from process.", e); + } + }, name).start(); + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/Retry.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/Retry.java new file mode 100644 index 000000000000..6ea36a38b717 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/Retry.java @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.util; + +import java.time.Duration; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public final class Retry { + private static final Executor EXECUTOR = new Executor() { + @Override + public void execute(Runnable command) { + new Thread(command).start(); + } + }; + + private Retry() {} + + public static void retryUntilTrue(Duration timeout, Duration delay, Callable predicate) throws TimeoutException, + ExecutionException { + getValueWithTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS, () -> { + while (true) { + Boolean call = predicate.call(); + if (call) { + return true; + } + + Thread.sleep(delay.toMillis()); + } + }); + } + + private static T getValueWithTimeout(long timeout, TimeUnit timeUnit, Callable predicate) throws TimeoutException, + ExecutionException { + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + try { + return predicate.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }, EXECUTOR); + + try { + return future.get(timeout, timeUnit); + } catch (InterruptedException e) { + throw new TimeoutException(); + } + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/Version.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/Version.java new file mode 100644 index 000000000000..b83a6e8edf6c --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/Version.java @@ -0,0 +1,177 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +package org.elasticsearch.test.cluster.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.io.UncheckedIOException; +import java.util.Objects; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Encapsulates comparison and printing logic for an x.y.z version. + */ +public final class Version implements Comparable, Serializable { + public static final Version CURRENT; + private static final Pattern pattern = Pattern.compile("(\\d+)\\.(\\d+)\\.(\\d+)(?:-(alpha\\d+|beta\\d+|rc\\d+|SNAPSHOT))?"); + private static final Pattern relaxedPattern = Pattern.compile( + "v?(\\d+)\\.(\\d+)(?:\\.(\\d+))?(?:[\\-+]+([a-zA-Z0-9_]+(?:-[a-zA-Z0-9]+)*))?" + ); + + private final int major; + private final int minor; + private final int revision; + private final int id; + private final String qualifier; + + static { + Properties versionProperties = new Properties(); + try (InputStream in = Version.class.getClassLoader().getResourceAsStream("version.properties")) { + versionProperties.load(in); + CURRENT = Version.fromString(versionProperties.getProperty("elasticsearch")); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * Specifies how a version string should be parsed. + */ + public enum Mode { + /** + * Strict parsing only allows known suffixes after the patch number: "-alpha", "-beta" or "-rc". The + * suffix "-SNAPSHOT" is also allowed, either after the patch number, or after the other suffices. + */ + STRICT, + + /** + * Relaxed parsing allows any alphanumeric suffix after the patch number. + */ + RELAXED; + } + + public Version(int major, int minor, int revision) { + this(major, minor, revision, null); + } + + public Version(int major, int minor, int revision, String qualifier) { + this.major = major; + this.minor = minor; + this.revision = revision; + + // currently qualifier is not taken into account + this.id = major * 10000000 + minor * 100000 + revision * 1000; + + this.qualifier = qualifier; + } + + public static Version fromString(final String s) { + return fromString(s, Mode.STRICT); + } + + public static Version fromString(final String s, final Mode mode) { + Objects.requireNonNull(s); + Matcher matcher = mode == Mode.STRICT ? pattern.matcher(s) : relaxedPattern.matcher(s); + if (matcher.matches() == false) { + String expected = mode == Mode.STRICT + ? "major.minor.revision[-(alpha|beta|rc)Number|-SNAPSHOT]" + : "major.minor.revision[-extra]"; + throw new IllegalArgumentException("Invalid version format: '" + s + "'. Should be " + expected); + } + + String major = matcher.group(1); + String minor = matcher.group(2); + String revision = matcher.group(3); + String qualifier = matcher.group(4); + + return new Version(Integer.parseInt(major), Integer.parseInt(minor), revision == null ? 0 : Integer.parseInt(revision), qualifier); + } + + @Override + public String toString() { + return getMajor() + "." + getMinor() + "." + getRevision(); + } + + public boolean before(Version compareTo) { + return id < compareTo.getId(); + } + + public boolean before(String compareTo) { + return before(fromString(compareTo)); + } + + public boolean onOrBefore(Version compareTo) { + return id <= compareTo.getId(); + } + + public boolean onOrBefore(String compareTo) { + return onOrBefore(fromString(compareTo)); + } + + public boolean onOrAfter(Version compareTo) { + return id >= compareTo.getId(); + } + + public boolean onOrAfter(String compareTo) { + return onOrAfter(fromString(compareTo)); + } + + public boolean after(Version compareTo) { + return id > compareTo.getId(); + } + + public boolean after(String compareTo) { + return after(fromString(compareTo)); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Version version = (Version) o; + return major == version.major && minor == version.minor && revision == version.revision; + } + + @Override + public int hashCode() { + return Objects.hash(major, minor, revision, id); + } + + public int getMajor() { + return major; + } + + public int getMinor() { + return minor; + } + + public int getRevision() { + return revision; + } + + protected int getId() { + return id; + } + + public String getQualifier() { + return qualifier; + } + + @Override + public int compareTo(Version other) { + return Integer.compare(getId(), other.getId()); + } + +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/resource/ClasspathTextResource.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/resource/ClasspathTextResource.java new file mode 100644 index 000000000000..cdde9f10328d --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/resource/ClasspathTextResource.java @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.util.resource; + +import org.elasticsearch.test.cluster.util.IOUtils; + +import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; + +class ClasspathTextResource implements TextResource { + private final String resourcePath; + + ClasspathTextResource(String resourcePath) { + this.resourcePath = resourcePath; + } + + @Override + public String getText() { + final Set classLoadersToSearch = new HashSet<>(); + // try context and system classloaders as well + classLoadersToSearch.add(Thread.currentThread().getContextClassLoader()); + classLoadersToSearch.add(ClassLoader.getSystemClassLoader()); + classLoadersToSearch.add(ClasspathTextResource.class.getClassLoader()); + + for (final ClassLoader classLoader : classLoadersToSearch) { + if (classLoader == null) { + continue; + } + + InputStream resource = classLoader.getResourceAsStream(resourcePath); + if (resource != null) { + return IOUtils.readStringFromStream(resource); + } + + // Be lenient if an absolute path was given + if (resourcePath.startsWith("/")) { + resource = classLoader.getResourceAsStream(resourcePath.replaceFirst("/", "")); + if (resource != null) { + return IOUtils.readStringFromStream(resource); + } + } + } + + throw new IllegalArgumentException( + "Resource with path " + resourcePath + " could not be found on any of these classloaders: " + classLoadersToSearch + ); + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/resource/FileTextResource.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/resource/FileTextResource.java new file mode 100644 index 000000000000..32929c8ec675 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/resource/FileTextResource.java @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.util.resource; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class FileTextResource implements TextResource { + private final Path file; + + FileTextResource(File file) { + this.file = file.toPath(); + } + + @Override + public String getText() { + try { + return Files.readString(file); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/resource/StringTextResource.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/resource/StringTextResource.java new file mode 100644 index 000000000000..1dacf5be8ed9 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/resource/StringTextResource.java @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.util.resource; + +class StringTextResource implements TextResource { + private final String text; + + StringTextResource(String text) { + this.text = text; + } + + @Override + public String getText() { + return text; + } +} diff --git a/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/resource/TextResource.java b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/resource/TextResource.java new file mode 100644 index 000000000000..384dc09ba553 --- /dev/null +++ b/test/test-clusters/src/main/java/org/elasticsearch/test/cluster/util/resource/TextResource.java @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test.cluster.util.resource; + +import java.io.File; + +public interface TextResource { + String getText(); + + static TextResource fromString(String text) { + return new StringTextResource(text); + } + + static TextResource fromClasspath(String path) { + return new ClasspathTextResource(path); + } + + static TextResource fromFile(File file) { + return new FileTextResource(file); + } +} diff --git a/test/test-clusters/src/main/resources/default_test_roles.yml b/test/test-clusters/src/main/resources/default_test_roles.yml new file mode 100644 index 000000000000..433dfdf79719 --- /dev/null +++ b/test/test-clusters/src/main/resources/default_test_roles.yml @@ -0,0 +1,13 @@ + +# The roles below are automatically defined by the "testcluster" setup +_es_test_root: + cluster: [ "ALL" ] + indices: + - names: [ "*" ] + allow_restricted_indices: true + privileges: [ "ALL" ] + run_as: [ "*" ] + applications: + - application: "*" + privileges: [ "*" ] + resources: [ "*" ] diff --git a/test/yaml-rest-runner/build.gradle b/test/yaml-rest-runner/build.gradle index 1ae1315ac9ef..37d2a00a68dd 100644 --- a/test/yaml-rest-runner/build.gradle +++ b/test/yaml-rest-runner/build.gradle @@ -1,6 +1,6 @@ apply plugin: 'elasticsearch.build' -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' dependencies { api project(':test:framework') diff --git a/x-pack/plugin/async-search/qa/rest/build.gradle b/x-pack/plugin/async-search/qa/rest/build.gradle index 07659ad9360c..759c756ccd88 100644 --- a/x-pack/plugin/async-search/qa/rest/build.gradle +++ b/x-pack/plugin/async-search/qa/rest/build.gradle @@ -1,8 +1,8 @@ import org.elasticsearch.gradle.internal.info.BuildParams apply plugin: 'elasticsearch.base-internal-es-plugin' -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' esplugin { name 'x-pack-test-deprecated-query' diff --git a/x-pack/plugin/async-search/qa/security/build.gradle b/x-pack/plugin/async-search/qa/security/build.gradle index 0ae64a3bd81d..3e29a711b0a8 100644 --- a/x-pack/plugin/async-search/qa/security/build.gradle +++ b/x-pack/plugin/async-search/qa/security/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' dependencies { javaRestTestImplementation(testArtifact(project(xpackModule('core')))) diff --git a/x-pack/plugin/autoscaling/qa/rest/build.gradle b/x-pack/plugin/autoscaling/qa/rest/build.gradle index 143e271412d4..19254880a708 100644 --- a/x-pack/plugin/autoscaling/qa/rest/build.gradle +++ b/x-pack/plugin/autoscaling/qa/rest/build.gradle @@ -1,7 +1,7 @@ import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' dependencies { yamlRestTestImplementation(testArtifact(project(xpackModule('core')))) diff --git a/x-pack/plugin/build.gradle b/x-pack/plugin/build.gradle index c293a46885e6..bb9bae10e573 100644 --- a/x-pack/plugin/build.gradle +++ b/x-pack/plugin/build.gradle @@ -3,11 +3,11 @@ import org.elasticsearch.gradle.VersionProperties import org.elasticsearch.gradle.internal.info.BuildParams import org.elasticsearch.gradle.util.GradleUtils -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' apply plugin: 'elasticsearch.validate-rest-spec' apply plugin: 'elasticsearch.internal-test-artifact' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' archivesBaseName = 'x-pack' diff --git a/x-pack/plugin/ccr/qa/rest/build.gradle b/x-pack/plugin/ccr/qa/rest/build.gradle index 98814f04d6c5..5f173b8831df 100644 --- a/x-pack/plugin/ccr/qa/rest/build.gradle +++ b/x-pack/plugin/ccr/qa/rest/build.gradle @@ -1,5 +1,5 @@ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' restResources { restApi { diff --git a/x-pack/plugin/core/build.gradle b/x-pack/plugin/core/build.gradle index 0b548af7da43..e09c82fa27e4 100644 --- a/x-pack/plugin/core/build.gradle +++ b/x-pack/plugin/core/build.gradle @@ -6,9 +6,9 @@ import java.nio.file.Paths apply plugin: 'elasticsearch.internal-es-plugin' apply plugin: 'elasticsearch.publish' apply plugin: 'elasticsearch.internal-cluster-test' -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.internal-java-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' apply plugin: 'elasticsearch.internal-test-artifact' archivesBaseName = 'x-pack-core' diff --git a/x-pack/plugin/deprecation/qa/early-deprecation-rest/build.gradle b/x-pack/plugin/deprecation/qa/early-deprecation-rest/build.gradle index 65e571e9c284..3e0b02e8f71b 100644 --- a/x-pack/plugin/deprecation/qa/early-deprecation-rest/build.gradle +++ b/x-pack/plugin/deprecation/qa/early-deprecation-rest/build.gradle @@ -2,7 +2,7 @@ import org.elasticsearch.gradle.util.GradleUtils import org.elasticsearch.gradle.internal.info.BuildParams apply plugin: 'elasticsearch.base-internal-es-plugin' -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' esplugin { description 'Deprecated query plugin' diff --git a/x-pack/plugin/deprecation/qa/rest/build.gradle b/x-pack/plugin/deprecation/qa/rest/build.gradle index 33ada7c6a432..b7d4757f668c 100644 --- a/x-pack/plugin/deprecation/qa/rest/build.gradle +++ b/x-pack/plugin/deprecation/qa/rest/build.gradle @@ -2,7 +2,7 @@ import org.elasticsearch.gradle.util.GradleUtils import org.elasticsearch.gradle.internal.info.BuildParams apply plugin: 'elasticsearch.base-internal-es-plugin' -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' esplugin { description 'Deprecated query plugin' diff --git a/x-pack/plugin/enrich/qa/rest-with-advanced-security/build.gradle b/x-pack/plugin/enrich/qa/rest-with-advanced-security/build.gradle index b2ddbc8aebf4..2e649e718b08 100644 --- a/x-pack/plugin/enrich/qa/rest-with-advanced-security/build.gradle +++ b/x-pack/plugin/enrich/qa/rest-with-advanced-security/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' import org.elasticsearch.gradle.internal.info.BuildParams diff --git a/x-pack/plugin/enrich/qa/rest-with-security/build.gradle b/x-pack/plugin/enrich/qa/rest-with-security/build.gradle index a016c47de614..69fec4ad32c7 100644 --- a/x-pack/plugin/enrich/qa/rest-with-security/build.gradle +++ b/x-pack/plugin/enrich/qa/rest-with-security/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' import org.elasticsearch.gradle.internal.info.BuildParams diff --git a/x-pack/plugin/enrich/qa/rest/build.gradle b/x-pack/plugin/enrich/qa/rest/build.gradle index 697144fc81be..e8473d15ed9e 100644 --- a/x-pack/plugin/enrich/qa/rest/build.gradle +++ b/x-pack/plugin/enrich/qa/rest/build.gradle @@ -1,6 +1,6 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' import org.elasticsearch.gradle.Version diff --git a/x-pack/plugin/eql/qa/correctness/build.gradle b/x-pack/plugin/eql/qa/correctness/build.gradle index 890584d0055c..aeddca54d145 100644 --- a/x-pack/plugin/eql/qa/correctness/build.gradle +++ b/x-pack/plugin/eql/qa/correctness/build.gradle @@ -1,5 +1,5 @@ apply plugin: 'elasticsearch.java' -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.internal-testclusters' import org.elasticsearch.gradle.testclusters.RunTask diff --git a/x-pack/plugin/eql/qa/mixed-node/build.gradle b/x-pack/plugin/eql/qa/mixed-node/build.gradle index 9aa0a93ff153..1e93ea035058 100644 --- a/x-pack/plugin/eql/qa/mixed-node/build.gradle +++ b/x-pack/plugin/eql/qa/mixed-node/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.bwc-test' import org.elasticsearch.gradle.VersionProperties diff --git a/x-pack/plugin/eql/qa/multi-cluster-with-security/build.gradle b/x-pack/plugin/eql/qa/multi-cluster-with-security/build.gradle index b637517b29dc..5fc247693c45 100644 --- a/x-pack/plugin/eql/qa/multi-cluster-with-security/build.gradle +++ b/x-pack/plugin/eql/qa/multi-cluster-with-security/build.gradle @@ -1,6 +1,6 @@ import org.elasticsearch.gradle.testclusters.DefaultTestClustersTask -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' dependencies { javaRestTestImplementation project(path: xpackModule('eql:qa:common')) diff --git a/x-pack/plugin/eql/qa/rest/build.gradle b/x-pack/plugin/eql/qa/rest/build.gradle index 01a9b176a007..a49317ad8c4e 100644 --- a/x-pack/plugin/eql/qa/rest/build.gradle +++ b/x-pack/plugin/eql/qa/rest/build.gradle @@ -1,6 +1,6 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' import org.elasticsearch.gradle.internal.info.BuildParams diff --git a/x-pack/plugin/eql/qa/security/build.gradle b/x-pack/plugin/eql/qa/security/build.gradle index d13c3570ee61..bcc9d0cb4ef8 100644 --- a/x-pack/plugin/eql/qa/security/build.gradle +++ b/x-pack/plugin/eql/qa/security/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' import org.elasticsearch.gradle.internal.info.BuildParams diff --git a/x-pack/plugin/fleet/build.gradle b/x-pack/plugin/fleet/build.gradle index f54ee022a744..9fef7a928bcf 100644 --- a/x-pack/plugin/fleet/build.gradle +++ b/x-pack/plugin/fleet/build.gradle @@ -7,7 +7,7 @@ apply plugin: 'elasticsearch.internal-es-plugin' apply plugin: 'elasticsearch.internal-cluster-test' -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' esplugin { name 'x-pack-fleet' diff --git a/x-pack/plugin/fleet/qa/rest/build.gradle b/x-pack/plugin/fleet/qa/rest/build.gradle index 8a9736c710b3..7dfbfaafa7d3 100644 --- a/x-pack/plugin/fleet/qa/rest/build.gradle +++ b/x-pack/plugin/fleet/qa/rest/build.gradle @@ -1,6 +1,6 @@ import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' dependencies { yamlRestTestImplementation(testArtifact(project(xpackModule('core')))) diff --git a/x-pack/plugin/graph/qa/with-security/build.gradle b/x-pack/plugin/graph/qa/with-security/build.gradle index 75109373b400..6a4264477eaf 100644 --- a/x-pack/plugin/graph/qa/with-security/build.gradle +++ b/x-pack/plugin/graph/qa/with-security/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' dependencies { yamlRestTestImplementation project(":x-pack:plugin:core") diff --git a/x-pack/plugin/identity-provider/qa/idp-rest-tests/build.gradle b/x-pack/plugin/identity-provider/qa/idp-rest-tests/build.gradle index 14e9b91edb1a..d44f4fa3ab3c 100644 --- a/x-pack/plugin/identity-provider/qa/idp-rest-tests/build.gradle +++ b/x-pack/plugin/identity-provider/qa/idp-rest-tests/build.gradle @@ -1,5 +1,5 @@ import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' dependencies { javaRestTestImplementation(testArtifact(project(xpackModule('core')))) diff --git a/x-pack/plugin/ilm/qa/multi-node/build.gradle b/x-pack/plugin/ilm/qa/multi-node/build.gradle index e60c772e5a59..523bf5d04ae4 100644 --- a/x-pack/plugin/ilm/qa/multi-node/build.gradle +++ b/x-pack/plugin/ilm/qa/multi-node/build.gradle @@ -1,6 +1,6 @@ import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' dependencies { javaRestTestImplementation(testArtifact(project(xpackModule('core')))) diff --git a/x-pack/plugin/ilm/qa/rest/build.gradle b/x-pack/plugin/ilm/qa/rest/build.gradle index 92b5b4e77c20..a17c130d4bbb 100644 --- a/x-pack/plugin/ilm/qa/rest/build.gradle +++ b/x-pack/plugin/ilm/qa/rest/build.gradle @@ -1,5 +1,5 @@ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' apply plugin: 'elasticsearch.authenticated-testclusters' dependencies { diff --git a/x-pack/plugin/ilm/qa/with-security/build.gradle b/x-pack/plugin/ilm/qa/with-security/build.gradle index 27a7da011c7e..7aaa43f14fe3 100644 --- a/x-pack/plugin/ilm/qa/with-security/build.gradle +++ b/x-pack/plugin/ilm/qa/with-security/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.authenticated-testclusters' dependencies { diff --git a/x-pack/plugin/logstash/build.gradle b/x-pack/plugin/logstash/build.gradle index 5ad1bcae1f1e..b8bfe255e152 100644 --- a/x-pack/plugin/logstash/build.gradle +++ b/x-pack/plugin/logstash/build.gradle @@ -1,5 +1,5 @@ apply plugin: 'elasticsearch.internal-es-plugin' -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' esplugin { name 'x-pack-logstash' diff --git a/x-pack/plugin/mapper-constant-keyword/build.gradle b/x-pack/plugin/mapper-constant-keyword/build.gradle index fe57cdc4380f..ffcbc8ed45be 100644 --- a/x-pack/plugin/mapper-constant-keyword/build.gradle +++ b/x-pack/plugin/mapper-constant-keyword/build.gradle @@ -1,7 +1,7 @@ import org.elasticsearch.gradle.internal.info.BuildParams apply plugin: 'elasticsearch.internal-es-plugin' -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' esplugin { name 'constant-keyword' diff --git a/x-pack/plugin/mapper-unsigned-long/build.gradle b/x-pack/plugin/mapper-unsigned-long/build.gradle index eaff7fe36255..adead5dc126b 100644 --- a/x-pack/plugin/mapper-unsigned-long/build.gradle +++ b/x-pack/plugin/mapper-unsigned-long/build.gradle @@ -11,8 +11,8 @@ import org.elasticsearch.gradle.internal.info.BuildParams evaluationDependsOn(xpackModule('core')) apply plugin: 'elasticsearch.internal-es-plugin' -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' esplugin { name 'unsigned-long' diff --git a/x-pack/plugin/mapper-version/build.gradle b/x-pack/plugin/mapper-version/build.gradle index 0558120c9578..437e03a42a4b 100644 --- a/x-pack/plugin/mapper-version/build.gradle +++ b/x-pack/plugin/mapper-version/build.gradle @@ -4,8 +4,8 @@ evaluationDependsOn(xpackModule('core')) apply plugin: 'elasticsearch.internal-es-plugin' -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' apply plugin: 'elasticsearch.internal-cluster-test' esplugin { diff --git a/x-pack/plugin/ml/qa/basic-multi-node/build.gradle b/x-pack/plugin/ml/qa/basic-multi-node/build.gradle index 2234f5e43d51..72793d1f9a41 100644 --- a/x-pack/plugin/ml/qa/basic-multi-node/build.gradle +++ b/x-pack/plugin/ml/qa/basic-multi-node/build.gradle @@ -1,6 +1,6 @@ import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' testClusters.configureEach { testDistribution = 'DEFAULT' diff --git a/x-pack/plugin/ml/qa/disabled/build.gradle b/x-pack/plugin/ml/qa/disabled/build.gradle index b0690460115d..97a7b0eed73a 100644 --- a/x-pack/plugin/ml/qa/disabled/build.gradle +++ b/x-pack/plugin/ml/qa/disabled/build.gradle @@ -1,6 +1,6 @@ import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' //dependencies { // testImplementation project(":x-pack:plugin:core") diff --git a/x-pack/plugin/ml/qa/ml-with-security/build.gradle b/x-pack/plugin/ml/qa/ml-with-security/build.gradle index e74e61fdb7d5..4b34d6fae046 100644 --- a/x-pack/plugin/ml/qa/ml-with-security/build.gradle +++ b/x-pack/plugin/ml/qa/ml-with-security/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' dependencies { yamlRestTestImplementation(testArtifact(project(xpackModule('core')))) diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/build.gradle b/x-pack/plugin/ml/qa/native-multi-node-tests/build.gradle index 9baa7d38c529..2645a3f3fa0d 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/build.gradle +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/build.gradle @@ -1,5 +1,5 @@ import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' dependencies { javaRestTestImplementation(testArtifact(project(xpackModule('core')))) diff --git a/x-pack/plugin/ml/qa/semantic-search-tests/build.gradle b/x-pack/plugin/ml/qa/semantic-search-tests/build.gradle index 52c532a5d654..42d47d0d700f 100644 --- a/x-pack/plugin/ml/qa/semantic-search-tests/build.gradle +++ b/x-pack/plugin/ml/qa/semantic-search-tests/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' dependencies { yamlRestTestImplementation(testArtifact(project(xpackModule('core')))) diff --git a/x-pack/plugin/ml/qa/single-node-tests/build.gradle b/x-pack/plugin/ml/qa/single-node-tests/build.gradle index cf1d2571c2d9..eb86ca600d75 100644 --- a/x-pack/plugin/ml/qa/single-node-tests/build.gradle +++ b/x-pack/plugin/ml/qa/single-node-tests/build.gradle @@ -1,6 +1,6 @@ import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' testClusters.configureEach { testDistribution = 'DEFAULT' diff --git a/x-pack/plugin/repositories-metering-api/qa/azure/build.gradle b/x-pack/plugin/repositories-metering-api/qa/azure/build.gradle index 82785d162121..b917c9907a56 100644 --- a/x-pack/plugin/repositories-metering-api/qa/azure/build.gradle +++ b/x-pack/plugin/repositories-metering-api/qa/azure/build.gradle @@ -8,7 +8,7 @@ import org.elasticsearch.gradle.internal.info.BuildParams import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.rest-resources' final Project fixture = project(':test:fixtures:azure-fixture') diff --git a/x-pack/plugin/repositories-metering-api/qa/gcs/build.gradle b/x-pack/plugin/repositories-metering-api/qa/gcs/build.gradle index 0c833fcb6f2d..2db3bf4f64cd 100644 --- a/x-pack/plugin/repositories-metering-api/qa/gcs/build.gradle +++ b/x-pack/plugin/repositories-metering-api/qa/gcs/build.gradle @@ -14,7 +14,7 @@ import java.security.KeyPairGenerator import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.rest-resources' final Project fixture = project(':test:fixtures:gcs-fixture') diff --git a/x-pack/plugin/repositories-metering-api/qa/s3/build.gradle b/x-pack/plugin/repositories-metering-api/qa/s3/build.gradle index 83dbde8c0093..05dbe3d6bd8c 100644 --- a/x-pack/plugin/repositories-metering-api/qa/s3/build.gradle +++ b/x-pack/plugin/repositories-metering-api/qa/s3/build.gradle @@ -1,7 +1,7 @@ import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.rest-resources' final Project fixture = project(':test:fixtures:s3-fixture') diff --git a/x-pack/plugin/rollup/qa/rest/build.gradle b/x-pack/plugin/rollup/qa/rest/build.gradle index 6e69f1e929d8..e91e56d2d1ba 100644 --- a/x-pack/plugin/rollup/qa/rest/build.gradle +++ b/x-pack/plugin/rollup/qa/rest/build.gradle @@ -8,8 +8,8 @@ import org.elasticsearch.gradle.Version import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' dependencies { yamlRestTestImplementation project(path: xpackModule('rollup')) diff --git a/x-pack/plugin/searchable-snapshots/qa/azure/build.gradle b/x-pack/plugin/searchable-snapshots/qa/azure/build.gradle index b4f60a7e09b0..73032e36679a 100644 --- a/x-pack/plugin/searchable-snapshots/qa/azure/build.gradle +++ b/x-pack/plugin/searchable-snapshots/qa/azure/build.gradle @@ -1,7 +1,7 @@ import org.elasticsearch.gradle.internal.info.BuildParams import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.rest-resources' final Project fixture = project(':test:fixtures:azure-fixture') diff --git a/x-pack/plugin/searchable-snapshots/qa/gcs/build.gradle b/x-pack/plugin/searchable-snapshots/qa/gcs/build.gradle index 5c017b2e34eb..c9ce9c1e05a5 100644 --- a/x-pack/plugin/searchable-snapshots/qa/gcs/build.gradle +++ b/x-pack/plugin/searchable-snapshots/qa/gcs/build.gradle @@ -7,7 +7,7 @@ import java.security.KeyPairGenerator import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.rest-resources' final Project fixture = project(':test:fixtures:gcs-fixture') diff --git a/x-pack/plugin/searchable-snapshots/qa/hdfs/build.gradle b/x-pack/plugin/searchable-snapshots/qa/hdfs/build.gradle index a6ef39dc6fe6..a1d4bd121160 100644 --- a/x-pack/plugin/searchable-snapshots/qa/hdfs/build.gradle +++ b/x-pack/plugin/searchable-snapshots/qa/hdfs/build.gradle @@ -16,7 +16,7 @@ import java.nio.file.Paths import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE apply plugin: 'elasticsearch.test.fixtures' -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.rest-resources' apply plugin: 'elasticsearch.internal-available-ports' diff --git a/x-pack/plugin/searchable-snapshots/qa/minio/build.gradle b/x-pack/plugin/searchable-snapshots/qa/minio/build.gradle index d00b51c0c2da..860e42378dcd 100644 --- a/x-pack/plugin/searchable-snapshots/qa/minio/build.gradle +++ b/x-pack/plugin/searchable-snapshots/qa/minio/build.gradle @@ -1,6 +1,6 @@ import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.test.fixtures' apply plugin: 'elasticsearch.rest-resources' diff --git a/x-pack/plugin/searchable-snapshots/qa/rest/build.gradle b/x-pack/plugin/searchable-snapshots/qa/rest/build.gradle index 75b62df35b55..a15f0b2b3c83 100644 --- a/x-pack/plugin/searchable-snapshots/qa/rest/build.gradle +++ b/x-pack/plugin/searchable-snapshots/qa/rest/build.gradle @@ -1,6 +1,6 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' dependencies { javaRestTestImplementation(testArtifact(project(xpackModule('searchable-snapshots')))) diff --git a/x-pack/plugin/searchable-snapshots/qa/s3/build.gradle b/x-pack/plugin/searchable-snapshots/qa/s3/build.gradle index 2b359f88f2d6..01a2dc065ec6 100644 --- a/x-pack/plugin/searchable-snapshots/qa/s3/build.gradle +++ b/x-pack/plugin/searchable-snapshots/qa/s3/build.gradle @@ -1,7 +1,7 @@ import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.rest-resources' final Project fixture = project(':test:fixtures:s3-fixture') diff --git a/x-pack/plugin/searchable-snapshots/qa/url/build.gradle b/x-pack/plugin/searchable-snapshots/qa/url/build.gradle index 0faab0acfe61..160eb8d28cbc 100644 --- a/x-pack/plugin/searchable-snapshots/qa/url/build.gradle +++ b/x-pack/plugin/searchable-snapshots/qa/url/build.gradle @@ -1,6 +1,6 @@ import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.rest-resources' final Project fixture = project(':test:fixtures:nginx-fixture') diff --git a/x-pack/plugin/security/qa/basic-enable-security/build.gradle b/x-pack/plugin/security/qa/basic-enable-security/build.gradle index 1ed92dff3381..5ed2aaea4d7e 100644 --- a/x-pack/plugin/security/qa/basic-enable-security/build.gradle +++ b/x-pack/plugin/security/qa/basic-enable-security/build.gradle @@ -8,7 +8,7 @@ import org.elasticsearch.gradle.testclusters.StandaloneRestIntegTestTask import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' dependencies { javaRestTestImplementation(testArtifact(project(xpackModule('security')))) diff --git a/x-pack/plugin/security/qa/jwt-realm/build.gradle b/x-pack/plugin/security/qa/jwt-realm/build.gradle index 14fee4918645..9de0f5aabbc4 100644 --- a/x-pack/plugin/security/qa/jwt-realm/build.gradle +++ b/x-pack/plugin/security/qa/jwt-realm/build.gradle @@ -1,7 +1,7 @@ import org.elasticsearch.gradle.Version import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' dependencies { javaRestTestImplementation project(path: xpackModule('core')) diff --git a/x-pack/plugin/security/qa/operator-privileges-tests/build.gradle b/x-pack/plugin/security/qa/operator-privileges-tests/build.gradle index ab3a80956d52..c42b8583e648 100644 --- a/x-pack/plugin/security/qa/operator-privileges-tests/build.gradle +++ b/x-pack/plugin/security/qa/operator-privileges-tests/build.gradle @@ -1,7 +1,7 @@ import org.elasticsearch.gradle.Version apply plugin: 'elasticsearch.base-internal-es-plugin' -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' esplugin { name 'operator-privileges-test' diff --git a/x-pack/plugin/security/qa/profile/build.gradle b/x-pack/plugin/security/qa/profile/build.gradle index ee59c985e12b..eae33a5ba587 100644 --- a/x-pack/plugin/security/qa/profile/build.gradle +++ b/x-pack/plugin/security/qa/profile/build.gradle @@ -1,7 +1,7 @@ import org.elasticsearch.gradle.Version import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' dependencies { javaRestTestImplementation project(':x-pack:plugin:core') diff --git a/x-pack/plugin/security/qa/security-basic/build.gradle b/x-pack/plugin/security/qa/security-basic/build.gradle index b6aed6bb26e9..10a808178965 100644 --- a/x-pack/plugin/security/qa/security-basic/build.gradle +++ b/x-pack/plugin/security/qa/security-basic/build.gradle @@ -1,5 +1,5 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' import org.elasticsearch.gradle.internal.info.BuildParams diff --git a/x-pack/plugin/security/qa/security-disabled/build.gradle b/x-pack/plugin/security/qa/security-disabled/build.gradle index e1d850c4adb9..e3d9c2f4f65b 100644 --- a/x-pack/plugin/security/qa/security-disabled/build.gradle +++ b/x-pack/plugin/security/qa/security-disabled/build.gradle @@ -7,7 +7,7 @@ */ import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' dependencies { javaRestTestImplementation(testArtifact(project(xpackModule('security')))) diff --git a/x-pack/plugin/security/qa/security-trial/build.gradle b/x-pack/plugin/security/qa/security-trial/build.gradle index b0f5a6da10d7..c2f13f2dae6e 100644 --- a/x-pack/plugin/security/qa/security-trial/build.gradle +++ b/x-pack/plugin/security/qa/security-trial/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' dependencies { javaRestTestImplementation project(path: xpackModule('core')) diff --git a/x-pack/plugin/security/qa/service-account/build.gradle b/x-pack/plugin/security/qa/service-account/build.gradle index 4c9724d14704..3a599de8dddb 100644 --- a/x-pack/plugin/security/qa/service-account/build.gradle +++ b/x-pack/plugin/security/qa/service-account/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' dependencies { javaRestTestImplementation project(':x-pack:plugin:core') diff --git a/x-pack/plugin/security/qa/smoke-test-all-realms/build.gradle b/x-pack/plugin/security/qa/smoke-test-all-realms/build.gradle index 02e40b7c8cf4..ca33b61de4ca 100644 --- a/x-pack/plugin/security/qa/smoke-test-all-realms/build.gradle +++ b/x-pack/plugin/security/qa/smoke-test-all-realms/build.gradle @@ -6,7 +6,7 @@ * This test is also intended to work correctly on FIPS mode because we also want to know if a realm breaks on FIPS. */ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' dependencies { javaRestTestImplementation(testArtifact(project(xpackModule('security')))) diff --git a/x-pack/plugin/security/qa/tls-basic/build.gradle b/x-pack/plugin/security/qa/tls-basic/build.gradle index a4c3554afd93..fbe91009011e 100644 --- a/x-pack/plugin/security/qa/tls-basic/build.gradle +++ b/x-pack/plugin/security/qa/tls-basic/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' import org.elasticsearch.gradle.internal.info.BuildParams diff --git a/x-pack/plugin/shutdown/qa/multi-node/build.gradle b/x-pack/plugin/shutdown/qa/multi-node/build.gradle index 2e2f8e12d79e..74dbfeb5cbef 100644 --- a/x-pack/plugin/shutdown/qa/multi-node/build.gradle +++ b/x-pack/plugin/shutdown/qa/multi-node/build.gradle @@ -1,6 +1,6 @@ import org.elasticsearch.gradle.VersionProperties -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.authenticated-testclusters' dependencies { diff --git a/x-pack/plugin/snapshot-based-recoveries/qa/azure/build.gradle b/x-pack/plugin/snapshot-based-recoveries/qa/azure/build.gradle index 002ddfeaad8c..c9e312fe6361 100644 --- a/x-pack/plugin/snapshot-based-recoveries/qa/azure/build.gradle +++ b/x-pack/plugin/snapshot-based-recoveries/qa/azure/build.gradle @@ -9,7 +9,7 @@ import org.elasticsearch.gradle.internal.info.BuildParams import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.rest-resources' final Project fixture = project(':test:fixtures:azure-fixture') diff --git a/x-pack/plugin/snapshot-based-recoveries/qa/fs/build.gradle b/x-pack/plugin/snapshot-based-recoveries/qa/fs/build.gradle index 3e53f734d736..0c0c1930f860 100644 --- a/x-pack/plugin/snapshot-based-recoveries/qa/fs/build.gradle +++ b/x-pack/plugin/snapshot-based-recoveries/qa/fs/build.gradle @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.rest-resources' dependencies { diff --git a/x-pack/plugin/snapshot-based-recoveries/qa/gcs/build.gradle b/x-pack/plugin/snapshot-based-recoveries/qa/gcs/build.gradle index 0249677a3504..3824906da3c4 100644 --- a/x-pack/plugin/snapshot-based-recoveries/qa/gcs/build.gradle +++ b/x-pack/plugin/snapshot-based-recoveries/qa/gcs/build.gradle @@ -6,7 +6,7 @@ import java.security.KeyPairGenerator import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.rest-resources' final Project fixture = project(':test:fixtures:gcs-fixture') diff --git a/x-pack/plugin/snapshot-based-recoveries/qa/license-enforcing/build.gradle b/x-pack/plugin/snapshot-based-recoveries/qa/license-enforcing/build.gradle index b9f843ad2670..1c9a87750779 100644 --- a/x-pack/plugin/snapshot-based-recoveries/qa/license-enforcing/build.gradle +++ b/x-pack/plugin/snapshot-based-recoveries/qa/license-enforcing/build.gradle @@ -8,7 +8,7 @@ import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE * Side Public License, v 1. */ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.rest-resources' dependencies { diff --git a/x-pack/plugin/snapshot-based-recoveries/qa/s3/build.gradle b/x-pack/plugin/snapshot-based-recoveries/qa/s3/build.gradle index eccdc63386e4..dfac9046ec32 100644 --- a/x-pack/plugin/snapshot-based-recoveries/qa/s3/build.gradle +++ b/x-pack/plugin/snapshot-based-recoveries/qa/s3/build.gradle @@ -9,7 +9,7 @@ import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.rest-resources' final Project fixture = project(':test:fixtures:s3-fixture') diff --git a/x-pack/plugin/snapshot-repo-test-kit/qa/azure/build.gradle b/x-pack/plugin/snapshot-repo-test-kit/qa/azure/build.gradle index 887549241ab8..92fa1a662911 100644 --- a/x-pack/plugin/snapshot-repo-test-kit/qa/azure/build.gradle +++ b/x-pack/plugin/snapshot-repo-test-kit/qa/azure/build.gradle @@ -8,7 +8,7 @@ import org.elasticsearch.gradle.internal.info.BuildParams import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.rest-resources' final Project fixture = project(':test:fixtures:azure-fixture') diff --git a/x-pack/plugin/snapshot-repo-test-kit/qa/gcs/build.gradle b/x-pack/plugin/snapshot-repo-test-kit/qa/gcs/build.gradle index 89f76a2f8e2b..31f8bb717741 100644 --- a/x-pack/plugin/snapshot-repo-test-kit/qa/gcs/build.gradle +++ b/x-pack/plugin/snapshot-repo-test-kit/qa/gcs/build.gradle @@ -13,7 +13,7 @@ import java.security.KeyPairGenerator import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.rest-resources' final Project fixture = project(':test:fixtures:gcs-fixture') diff --git a/x-pack/plugin/snapshot-repo-test-kit/qa/hdfs/build.gradle b/x-pack/plugin/snapshot-repo-test-kit/qa/hdfs/build.gradle index 4acc4b6415e8..a323d18afbb3 100644 --- a/x-pack/plugin/snapshot-repo-test-kit/qa/hdfs/build.gradle +++ b/x-pack/plugin/snapshot-repo-test-kit/qa/hdfs/build.gradle @@ -17,7 +17,7 @@ import java.nio.file.Paths import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE apply plugin: 'elasticsearch.test.fixtures' -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.rest-resources' apply plugin: 'elasticsearch.internal-available-ports' diff --git a/x-pack/plugin/snapshot-repo-test-kit/qa/minio/build.gradle b/x-pack/plugin/snapshot-repo-test-kit/qa/minio/build.gradle index 8e10165d243c..225e0146a6ec 100644 --- a/x-pack/plugin/snapshot-repo-test-kit/qa/minio/build.gradle +++ b/x-pack/plugin/snapshot-repo-test-kit/qa/minio/build.gradle @@ -7,7 +7,7 @@ import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.test.fixtures' apply plugin: 'elasticsearch.rest-resources' diff --git a/x-pack/plugin/snapshot-repo-test-kit/qa/rest/build.gradle b/x-pack/plugin/snapshot-repo-test-kit/qa/rest/build.gradle index dd5a54e828cd..6b32e11ef81d 100644 --- a/x-pack/plugin/snapshot-repo-test-kit/qa/rest/build.gradle +++ b/x-pack/plugin/snapshot-repo-test-kit/qa/rest/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' apply plugin: 'elasticsearch.rest-resources' dependencies { diff --git a/x-pack/plugin/snapshot-repo-test-kit/qa/s3/build.gradle b/x-pack/plugin/snapshot-repo-test-kit/qa/s3/build.gradle index a4609cd2cc2c..36b13ef8b12a 100644 --- a/x-pack/plugin/snapshot-repo-test-kit/qa/s3/build.gradle +++ b/x-pack/plugin/snapshot-repo-test-kit/qa/s3/build.gradle @@ -7,7 +7,7 @@ import static org.elasticsearch.gradle.PropertyNormalization.IGNORE_VALUE import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.rest-resources' final Project fixture = project(':test:fixtures:s3-fixture') diff --git a/x-pack/plugin/sql/qa/jdbc/build.gradle b/x-pack/plugin/sql/qa/jdbc/build.gradle index ad6e71dce594..65d074538361 100644 --- a/x-pack/plugin/sql/qa/jdbc/build.gradle +++ b/x-pack/plugin/sql/qa/jdbc/build.gradle @@ -52,7 +52,7 @@ subprojects { if (project.name != 'security') { // The security project just configures its subprojects - apply plugin: 'elasticsearch.internal-java-rest-test' + apply plugin: 'elasticsearch.legacy-java-rest-test' dependencies { javaRestTestImplementation(project(':x-pack:plugin:sql:qa:jdbc')) diff --git a/x-pack/plugin/sql/qa/mixed-node/build.gradle b/x-pack/plugin/sql/qa/mixed-node/build.gradle index 73d02ef3c516..d7fa587dfc40 100644 --- a/x-pack/plugin/sql/qa/mixed-node/build.gradle +++ b/x-pack/plugin/sql/qa/mixed-node/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.bwc-test' import org.elasticsearch.gradle.VersionProperties diff --git a/x-pack/plugin/sql/qa/server/build.gradle b/x-pack/plugin/sql/qa/server/build.gradle index 3e8903a97965..56ee4ef971f4 100644 --- a/x-pack/plugin/sql/qa/server/build.gradle +++ b/x-pack/plugin/sql/qa/server/build.gradle @@ -41,7 +41,7 @@ subprojects { if (project.name != 'security') { // The security project just configures its subprojects - apply plugin: 'elasticsearch.internal-java-rest-test' + apply plugin: 'elasticsearch.legacy-java-rest-test' testClusters.matching { it.name == "javaRestTest" }.configureEach { testDistribution = 'DEFAULT' diff --git a/x-pack/plugin/sql/qa/server/multi-cluster-with-security/build.gradle b/x-pack/plugin/sql/qa/server/multi-cluster-with-security/build.gradle index 6755dd15badb..b56d8ff21199 100644 --- a/x-pack/plugin/sql/qa/server/multi-cluster-with-security/build.gradle +++ b/x-pack/plugin/sql/qa/server/multi-cluster-with-security/build.gradle @@ -1,6 +1,6 @@ import org.elasticsearch.gradle.testclusters.DefaultTestClustersTask -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' dependencies { javaRestTestImplementation project(path: xpackModule('ql:test-fixtures')) diff --git a/x-pack/plugin/stack/qa/rest/build.gradle b/x-pack/plugin/stack/qa/rest/build.gradle index 770a52fde68b..a5bdb388d610 100644 --- a/x-pack/plugin/stack/qa/rest/build.gradle +++ b/x-pack/plugin/stack/qa/rest/build.gradle @@ -1,5 +1,5 @@ -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' apply plugin: 'elasticsearch.authenticated-testclusters' dependencies { diff --git a/x-pack/plugin/text-structure/qa/text-structure-with-security/build.gradle b/x-pack/plugin/text-structure/qa/text-structure-with-security/build.gradle index 4431351e5618..5fc76885aa7e 100644 --- a/x-pack/plugin/text-structure/qa/text-structure-with-security/build.gradle +++ b/x-pack/plugin/text-structure/qa/text-structure-with-security/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' dependencies { yamlRestTestImplementation(testArtifact(project(xpackModule('core')))) diff --git a/x-pack/plugin/transform/qa/multi-node-tests/build.gradle b/x-pack/plugin/transform/qa/multi-node-tests/build.gradle index 6cd6c529a879..4f1dfd334589 100644 --- a/x-pack/plugin/transform/qa/multi-node-tests/build.gradle +++ b/x-pack/plugin/transform/qa/multi-node-tests/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' dependencies { javaRestTestImplementation(testArtifact(project(xpackModule('core')))) diff --git a/x-pack/plugin/transform/qa/single-node-tests/build.gradle b/x-pack/plugin/transform/qa/single-node-tests/build.gradle index be073f64a2ef..c321b3aeb028 100644 --- a/x-pack/plugin/transform/qa/single-node-tests/build.gradle +++ b/x-pack/plugin/transform/qa/single-node-tests/build.gradle @@ -1,5 +1,5 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' dependencies { javaRestTestImplementation(testArtifact(project(xpackModule('core')))) diff --git a/x-pack/plugin/vector-tile/build.gradle b/x-pack/plugin/vector-tile/build.gradle index a8a72080736a..017685e32dd7 100644 --- a/x-pack/plugin/vector-tile/build.gradle +++ b/x-pack/plugin/vector-tile/build.gradle @@ -14,7 +14,7 @@ */ apply plugin: 'elasticsearch.internal-es-plugin' -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' esplugin { name 'vector-tile' diff --git a/x-pack/plugin/vector-tile/qa/multi-cluster/build.gradle b/x-pack/plugin/vector-tile/qa/multi-cluster/build.gradle index 0a987a3faa1e..ea20c8eacd17 100644 --- a/x-pack/plugin/vector-tile/qa/multi-cluster/build.gradle +++ b/x-pack/plugin/vector-tile/qa/multi-cluster/build.gradle @@ -7,7 +7,7 @@ import org.elasticsearch.gradle.testclusters.DefaultTestClustersTask -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' dependencies { javaRestTestImplementation project(':x-pack:plugin:vector-tile') diff --git a/x-pack/plugin/watcher/qa/rest/build.gradle b/x-pack/plugin/watcher/qa/rest/build.gradle index 026ec7edb0fb..3f61bdcb3c2e 100644 --- a/x-pack/plugin/watcher/qa/rest/build.gradle +++ b/x-pack/plugin/watcher/qa/rest/build.gradle @@ -1,8 +1,8 @@ import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-java-rest-test' -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' dependencies { yamlRestTestImplementation project(path: ':x-pack:plugin:watcher:qa:common') diff --git a/x-pack/plugin/watcher/qa/with-monitoring/build.gradle b/x-pack/plugin/watcher/qa/with-monitoring/build.gradle index b1f8d157394e..6a2ef6fbd7bd 100644 --- a/x-pack/plugin/watcher/qa/with-monitoring/build.gradle +++ b/x-pack/plugin/watcher/qa/with-monitoring/build.gradle @@ -1,6 +1,6 @@ import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' dependencies { javaRestTestImplementation project(':x-pack:qa') diff --git a/x-pack/plugin/watcher/qa/with-security/build.gradle b/x-pack/plugin/watcher/qa/with-security/build.gradle index d3b064b271b5..5c01573e0941 100644 --- a/x-pack/plugin/watcher/qa/with-security/build.gradle +++ b/x-pack/plugin/watcher/qa/with-security/build.gradle @@ -1,6 +1,6 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' -apply plugin: 'elasticsearch.internal-yaml-rest-test' -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' dependencies { yamlRestTestImplementation project(path: ':x-pack:plugin:watcher:qa:common') diff --git a/x-pack/plugin/wildcard/build.gradle b/x-pack/plugin/wildcard/build.gradle index 82f2d33ac004..63437cb2f132 100644 --- a/x-pack/plugin/wildcard/build.gradle +++ b/x-pack/plugin/wildcard/build.gradle @@ -1,7 +1,7 @@ import org.elasticsearch.gradle.internal.info.BuildParams apply plugin: 'elasticsearch.internal-es-plugin' -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' esplugin { name 'wildcard' diff --git a/x-pack/qa/core-rest-tests-with-security/build.gradle b/x-pack/qa/core-rest-tests-with-security/build.gradle index 87cb38469465..f5b6730a4411 100644 --- a/x-pack/qa/core-rest-tests-with-security/build.gradle +++ b/x-pack/qa/core-rest-tests-with-security/build.gradle @@ -1,7 +1,7 @@ import org.elasticsearch.gradle.Version import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' apply plugin: 'elasticsearch.authenticated-testclusters' dependencies { diff --git a/x-pack/qa/kerberos-tests/build.gradle b/x-pack/qa/kerberos-tests/build.gradle index b04cdc8078d8..536758dcfb71 100644 --- a/x-pack/qa/kerberos-tests/build.gradle +++ b/x-pack/qa/kerberos-tests/build.gradle @@ -1,7 +1,7 @@ import java.nio.file.Path import java.nio.file.Paths -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.test.fixtures' testFixtures.useFixture ":test:fixtures:krb5kdc-fixture", "peppa" diff --git a/x-pack/qa/mixed-tier-cluster/build.gradle b/x-pack/qa/mixed-tier-cluster/build.gradle index c94175dcfc75..862b33a5402b 100644 --- a/x-pack/qa/mixed-tier-cluster/build.gradle +++ b/x-pack/qa/mixed-tier-cluster/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.bwc-test' import org.elasticsearch.gradle.VersionProperties diff --git a/x-pack/qa/multi-node/build.gradle b/x-pack/qa/multi-node/build.gradle index 16aa1fc50053..f4af52b99459 100644 --- a/x-pack/qa/multi-node/build.gradle +++ b/x-pack/qa/multi-node/build.gradle @@ -2,16 +2,5 @@ apply plugin: 'elasticsearch.internal-java-rest-test' dependencies { javaRestTestImplementation project(':x-pack:qa') -} - -testClusters.matching { it.name == "javaRestTest" }.configureEach { - testDistribution = 'DEFAULT' - numberOfNodes = 2 - setting 'xpack.security.enabled', 'true' - setting 'xpack.watcher.enabled', 'false' - setting 'xpack.ml.enabled', 'false' - setting 'xpack.license.self_generated.type', 'trial' - rolesFile file('roles.yml') - user username: "test-user", password: "x-pack-test-password", role: "test" - user username: "super-user", password: "x-pack-super-password" + clusterModules project(xpackModule('rollup')) } diff --git a/x-pack/qa/multi-node/src/javaRestTest/java/org/elasticsearch/multi_node/GlobalCheckpointSyncActionIT.java b/x-pack/qa/multi-node/src/javaRestTest/java/org/elasticsearch/multi_node/GlobalCheckpointSyncActionIT.java index f42b4681ed19..aa04d1cbeca9 100644 --- a/x-pack/qa/multi-node/src/javaRestTest/java/org/elasticsearch/multi_node/GlobalCheckpointSyncActionIT.java +++ b/x-pack/qa/multi-node/src/javaRestTest/java/org/elasticsearch/multi_node/GlobalCheckpointSyncActionIT.java @@ -12,14 +12,28 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.cluster.util.resource.TextResource; import org.elasticsearch.test.rest.ESRestTestCase; import org.elasticsearch.test.rest.ObjectPath; import org.elasticsearch.xcontent.XContentBuilder; +import org.junit.ClassRule; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.equalTo; public class GlobalCheckpointSyncActionIT extends ESRestTestCase { + @ClassRule + public static ElasticsearchCluster cluster = ElasticsearchCluster.local() + .nodes(2) + .setting("xpack.security.enabled", "true") + .setting("xpack.watcher.enabled", "false") + .setting("xpack.ml.enabled", "false") + .setting("xpack.license.self_generated.type", "trial") + .rolesFile(TextResource.fromClasspath("roles.yml")) + .user("test-user", "x-pack-test-password", "test") + .user("super-user", "x-pack-super-password") + .build(); @Override protected Settings restClientSettings() { @@ -31,6 +45,11 @@ protected Settings restAdminSettings() { return getClientSettings("super-user", "x-pack-super-password"); } + @Override + protected String getTestRestCluster() { + return cluster.getHttpAddresses(); + } + private Settings getClientSettings(final String username, final String password) { final String token = basicAuthHeaderValue(username, new SecureString(password.toCharArray())); return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build(); diff --git a/x-pack/qa/multi-node/src/javaRestTest/java/org/elasticsearch/multi_node/RollupIT.java b/x-pack/qa/multi-node/src/javaRestTest/java/org/elasticsearch/multi_node/RollupIT.java index 0e4fea305cfb..0e3a5c0c0c15 100644 --- a/x-pack/qa/multi-node/src/javaRestTest/java/org/elasticsearch/multi_node/RollupIT.java +++ b/x-pack/qa/multi-node/src/javaRestTest/java/org/elasticsearch/multi_node/RollupIT.java @@ -16,9 +16,11 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.support.XContentMapValues; import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.test.cluster.ElasticsearchCluster; import org.elasticsearch.test.rest.ESRestTestCase; import org.elasticsearch.xcontent.ObjectPath; import org.elasticsearch.xcontent.XContentBuilder; +import org.junit.ClassRule; import java.io.IOException; import java.time.Instant; @@ -35,6 +37,16 @@ import static org.hamcrest.Matchers.oneOf; public class RollupIT extends ESRestTestCase { + @ClassRule + public static ElasticsearchCluster cluster = ElasticsearchCluster.local() + .nodes(2) + .module("x-pack-rollup") + .setting("xpack.security.enabled", "true") + .setting("xpack.watcher.enabled", "false") + .setting("xpack.ml.enabled", "false") + .setting("xpack.license.self_generated.type", "trial") + .user("super-user", "x-pack-super-password") + .build(); @Override protected Settings restClientSettings() { @@ -46,6 +58,11 @@ protected Settings restAdminSettings() { return getClientSettings("super-user", "x-pack-super-password"); } + @Override + protected String getTestRestCluster() { + return cluster.getHttpAddresses(); + } + private Settings getClientSettings(final String username, final String password) { final String token = basicAuthHeaderValue(username, new SecureString(password.toCharArray())); return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build(); diff --git a/x-pack/qa/multi-node/roles.yml b/x-pack/qa/multi-node/src/javaRestTest/resources/roles.yml similarity index 100% rename from x-pack/qa/multi-node/roles.yml rename to x-pack/qa/multi-node/src/javaRestTest/resources/roles.yml diff --git a/x-pack/qa/oidc-op-tests/build.gradle b/x-pack/qa/oidc-op-tests/build.gradle index d9394a8b3ce3..aa4c1e2d0d22 100644 --- a/x-pack/qa/oidc-op-tests/build.gradle +++ b/x-pack/qa/oidc-op-tests/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.test.fixtures' dependencies { diff --git a/x-pack/qa/password-protected-keystore/build.gradle b/x-pack/qa/password-protected-keystore/build.gradle index 7e6dd4bade15..61d373b5a42e 100644 --- a/x-pack/qa/password-protected-keystore/build.gradle +++ b/x-pack/qa/password-protected-keystore/build.gradle @@ -3,7 +3,7 @@ * is using a password protected keystore in its nodes. */ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' dependencies { javaRestTestImplementation project(path: xpackModule('core')) diff --git a/x-pack/qa/reindex-tests-with-security/build.gradle b/x-pack/qa/reindex-tests-with-security/build.gradle index bec7c8b1b0be..fdb61e02f818 100644 --- a/x-pack/qa/reindex-tests-with-security/build.gradle +++ b/x-pack/qa/reindex-tests-with-security/build.gradle @@ -1,6 +1,6 @@ import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' apply plugin: 'elasticsearch.rest-resources' dependencies { diff --git a/x-pack/qa/runtime-fields/build.gradle b/x-pack/qa/runtime-fields/build.gradle index 4e9eb2d23905..d89ea041dda4 100644 --- a/x-pack/qa/runtime-fields/build.gradle +++ b/x-pack/qa/runtime-fields/build.gradle @@ -21,7 +21,7 @@ tasks.named("test").configure { enabled = false } subprojects { if (project.name.startsWith('core-with-')) { - apply plugin: 'elasticsearch.internal-yaml-rest-test' + apply plugin: 'elasticsearch.legacy-yaml-rest-test' dependencies { yamlRestTestImplementation project(":x-pack:qa:runtime-fields") diff --git a/x-pack/qa/runtime-fields/with-security/build.gradle b/x-pack/qa/runtime-fields/with-security/build.gradle index 41ae03d6692d..e664ee888a31 100644 --- a/x-pack/qa/runtime-fields/with-security/build.gradle +++ b/x-pack/qa/runtime-fields/with-security/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.authenticated-testclusters' dependencies { diff --git a/x-pack/qa/saml-idp-tests/build.gradle b/x-pack/qa/saml-idp-tests/build.gradle index 813733c0b4aa..10e6848dfb73 100644 --- a/x-pack/qa/saml-idp-tests/build.gradle +++ b/x-pack/qa/saml-idp-tests/build.gradle @@ -2,7 +2,7 @@ import org.elasticsearch.gradle.LazyPropertyMap Project idpFixtureProject = project(':x-pack:test:idp-fixture') -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.test.fixtures' dependencies { diff --git a/x-pack/qa/security-example-spi-extension/build.gradle b/x-pack/qa/security-example-spi-extension/build.gradle index cac57d78344b..3fb73d615b8c 100644 --- a/x-pack/qa/security-example-spi-extension/build.gradle +++ b/x-pack/qa/security-example-spi-extension/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' apply plugin: 'elasticsearch.base-internal-es-plugin' esplugin { diff --git a/x-pack/qa/security-setup-password-tests/build.gradle b/x-pack/qa/security-setup-password-tests/build.gradle index 29455fe41dd9..8f17e6d92291 100644 --- a/x-pack/qa/security-setup-password-tests/build.gradle +++ b/x-pack/qa/security-setup-password-tests/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.legacy-java-rest-test' dependencies { javaRestTestImplementation project(path: xpackModule('security')) diff --git a/x-pack/qa/smoke-test-plugins-ssl/build.gradle b/x-pack/qa/smoke-test-plugins-ssl/build.gradle index bd0f9112e2a4..1e8e88b91c33 100644 --- a/x-pack/qa/smoke-test-plugins-ssl/build.gradle +++ b/x-pack/qa/smoke-test-plugins-ssl/build.gradle @@ -2,7 +2,7 @@ import org.apache.tools.ant.filters.ReplaceTokens import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' dependencies { yamlRestTestImplementation project(':x-pack:plugin:core') diff --git a/x-pack/qa/smoke-test-plugins/build.gradle b/x-pack/qa/smoke-test-plugins/build.gradle index 46ae4b9015db..427aa39f02e4 100644 --- a/x-pack/qa/smoke-test-plugins/build.gradle +++ b/x-pack/qa/smoke-test-plugins/build.gradle @@ -1,7 +1,7 @@ import org.apache.tools.ant.filters.ReplaceTokens import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' apply plugin: 'elasticsearch.rest-resources' dependencies { diff --git a/x-pack/qa/smoke-test-security-with-mustache/build.gradle b/x-pack/qa/smoke-test-security-with-mustache/build.gradle index c3bebeb17817..373e6966df51 100644 --- a/x-pack/qa/smoke-test-security-with-mustache/build.gradle +++ b/x-pack/qa/smoke-test-security-with-mustache/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' dependencies { yamlRestTestImplementation project(':x-pack:qa') diff --git a/x-pack/qa/third-party/jira/build.gradle b/x-pack/qa/third-party/jira/build.gradle index 84e0d7dc50bb..b7268af80753 100644 --- a/x-pack/qa/third-party/jira/build.gradle +++ b/x-pack/qa/third-party/jira/build.gradle @@ -5,7 +5,7 @@ import java.nio.charset.StandardCharsets import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' dependencies { yamlRestTestImplementation project(':x-pack:plugin:core') diff --git a/x-pack/qa/third-party/pagerduty/build.gradle b/x-pack/qa/third-party/pagerduty/build.gradle index b7adeb9c6e66..4b5a0bbeeeb4 100644 --- a/x-pack/qa/third-party/pagerduty/build.gradle +++ b/x-pack/qa/third-party/pagerduty/build.gradle @@ -1,6 +1,6 @@ import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' dependencies { yamlRestTestImplementation project(':x-pack:plugin:core') diff --git a/x-pack/qa/third-party/slack/build.gradle b/x-pack/qa/third-party/slack/build.gradle index a298a1531a8d..b2b0478da047 100644 --- a/x-pack/qa/third-party/slack/build.gradle +++ b/x-pack/qa/third-party/slack/build.gradle @@ -1,6 +1,6 @@ import org.elasticsearch.gradle.internal.info.BuildParams -apply plugin: 'elasticsearch.internal-yaml-rest-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-test' dependencies { yamlRestTestImplementation project(':x-pack:plugin:core') diff --git a/x-pack/qa/xpack-prefix-rest-compat/build.gradle b/x-pack/qa/xpack-prefix-rest-compat/build.gradle index b17463183bd2..d609ede0efd2 100644 --- a/x-pack/qa/xpack-prefix-rest-compat/build.gradle +++ b/x-pack/qa/xpack-prefix-rest-compat/build.gradle @@ -13,7 +13,7 @@ import org.elasticsearch.gradle.internal.info.BuildParams import org.elasticsearch.gradle.internal.test.rest.CopyRestTestsTask import org.elasticsearch.gradle.util.GradleUtils -apply plugin: 'elasticsearch.yaml-rest-compat-test' +apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' /** * This project exists to test the _xpack prefix for REST compatibility. The _xpack prefix was removed from the specification, but still supported From c78808ded402722f5dc23ecca9900cb1be99854b Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Wed, 21 Dec 2022 20:51:10 -0800 Subject: [PATCH 338/919] Download full zip for compatibility with older branches --- .ci/jobs.t/elastic+elasticsearch+pull-request+release-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.ci/jobs.t/elastic+elasticsearch+pull-request+release-tests.yml b/.ci/jobs.t/elastic+elasticsearch+pull-request+release-tests.yml index 54242eee118b..370fcce8a911 100644 --- a/.ci/jobs.t/elastic+elasticsearch+pull-request+release-tests.yml +++ b/.ci/jobs.t/elastic+elasticsearch+pull-request+release-tests.yml @@ -52,6 +52,7 @@ mkdir -p ${ML_IVY_REPO}/maven/org/elasticsearch/ml/ml-cpp/${ES_VERSION} curl --fail -o "${ML_IVY_REPO}/maven/org/elasticsearch/ml/ml-cpp/${ES_VERSION}/ml-cpp-${ES_VERSION}-deps.zip" https://artifacts-snapshot.elastic.co/ml-cpp/${ES_VERSION}-SNAPSHOT/downloads/ml-cpp/ml-cpp-${ES_VERSION}-SNAPSHOT-deps.zip curl --fail -o "${ML_IVY_REPO}/maven/org/elasticsearch/ml/ml-cpp/${ES_VERSION}/ml-cpp-${ES_VERSION}-nodeps.zip" https://artifacts-snapshot.elastic.co/ml-cpp/${ES_VERSION}-SNAPSHOT/downloads/ml-cpp/ml-cpp-${ES_VERSION}-SNAPSHOT-nodeps.zip + curl --fail -o "${ML_IVY_REPO}/maven/org/elasticsearch/ml/ml-cpp/${ES_VERSION}/ml-cpp-${ES_VERSION}.zip" https://artifacts-snapshot.elastic.co/ml-cpp/${ES_VERSION}-SNAPSHOT/downloads/ml-cpp/ml-cpp-${ES_VERSION}-SNAPSHOT.zip $WORKSPACE/.ci/scripts/run-gradle.sh -Dbwc.checkout.align=true -Dbuild.snapshot=false -Dbuild.ml_cpp.repo=file://${ML_IVY_REPO} \ -Dtests.jvm.argline=-Dbuild.snapshot=false -Dlicense.key=${WORKSPACE}/x-pack/license-tools/src/test/resources/public.key -Dbuild.id=deadbeef build From 1daf314a5dfeca1efe51127d5ca13a73463c950b Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 22 Dec 2022 15:54:57 +1100 Subject: [PATCH 339/919] JWT realm - Simplify token principal calculation (#92315) Token principal is an implementation detail because it is useful only for realm ordering cache and logging. Hence it should *not* be exposed as a user-configurable setting. This PR removes the setting and related support classes. The token principal is now computed by the realms which also has the advantage of working correctly with fallback claims. --- docs/changelog/92315.yaml | 5 + .../authc/jwt/JwtRealmsServiceSettings.java | 38 --- .../plugin/security/qa/jwt-realm/build.gradle | 2 - .../xpack/security/authc/jwt/JwtRestIT.java | 21 +- .../authc/jwt/JwtRealmSingleNodeTests.java | 224 ++++++++++++++++++ .../xpack/security/Security.java | 2 - .../xpack/security/authc/InternalRealms.java | 5 +- .../authc/jwt/JwtAuthenticationToken.java | 143 ++++------- .../security/authc/jwt/JwtAuthenticator.java | 25 +- .../xpack/security/authc/jwt/JwtRealm.java | 158 ++++++++---- .../security/authc/jwt/JwtRealmsService.java | 88 ------- .../jwt/JwtAuthenticationTokenTests.java | 97 -------- .../authc/jwt/JwtAuthenticatorTests.java | 16 +- .../xpack/security/authc/jwt/JwtIssuer.java | 7 +- ...RealmAuthenticateAccessTokenTypeTests.java | 11 +- .../authc/jwt/JwtRealmAuthenticateTests.java | 28 +-- .../authc/jwt/JwtRealmGenerateTests.java | 31 +-- .../security/authc/jwt/JwtRealmTestCase.java | 49 +--- 18 files changed, 440 insertions(+), 510 deletions(-) create mode 100644 docs/changelog/92315.yaml delete mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/jwt/JwtRealmsServiceSettings.java create mode 100644 x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmSingleNodeTests.java delete mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmsService.java delete mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticationTokenTests.java diff --git a/docs/changelog/92315.yaml b/docs/changelog/92315.yaml new file mode 100644 index 000000000000..9180467f5db0 --- /dev/null +++ b/docs/changelog/92315.yaml @@ -0,0 +1,5 @@ +pr: 92315 +summary: JWT realm - Simplify token principal calculation +area: Authentication +type: enhancement +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/jwt/JwtRealmsServiceSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/jwt/JwtRealmsServiceSettings.java deleted file mode 100644 index e5b98619b0c2..000000000000 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/jwt/JwtRealmsServiceSettings.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -package org.elasticsearch.xpack.core.security.authc.jwt; - -import org.elasticsearch.common.settings.Setting; - -import java.util.Collection; -import java.util.List; -import java.util.function.Function; - -/** - * Settings used by JwtRealmsService for common handling of JWT realm instances. - */ -public class JwtRealmsServiceSettings { - - public static final List DEFAULT_PRINCIPAL_CLAIMS = List.of("sub", "oid", "client_id", "appid", "azp", "email"); - - public static final Setting> PRINCIPAL_CLAIMS_SETTING = Setting.listSetting( - "xpack.security.authc.jwt.principal_claims", - DEFAULT_PRINCIPAL_CLAIMS, - Function.identity(), - Setting.Property.NodeScope - ); - - /** - * Get all settings shared by all JWT Realms. - * @return All settings shared by all JWT Realms. - */ - public static Collection> getSettings() { - return List.of(PRINCIPAL_CLAIMS_SETTING); - } - - private JwtRealmsServiceSettings() {} -} diff --git a/x-pack/plugin/security/qa/jwt-realm/build.gradle b/x-pack/plugin/security/qa/jwt-realm/build.gradle index 9de0f5aabbc4..d561f0c8ceee 100644 --- a/x-pack/plugin/security/qa/jwt-realm/build.gradle +++ b/x-pack/plugin/security/qa/jwt-realm/build.gradle @@ -46,8 +46,6 @@ testClusters.matching { it.name == 'javaRestTest' }.configureEach { setting 'xpack.security.http.ssl.certificate_authorities', 'ca.crt' setting 'xpack.security.http.ssl.client_authentication', 'optional' - setting 'xpack.security.authc.jwt.principal_claims', 'sub,oid,client_id,azp,appid,email' - setting 'xpack.security.authc.realms.file.admin_file.order', '0' // These realm settings are generated by JwtRealmGenerateTests diff --git a/x-pack/plugin/security/qa/jwt-realm/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/jwt/JwtRestIT.java b/x-pack/plugin/security/qa/jwt-realm/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/jwt/JwtRestIT.java index a51557a4e785..173c257cb083 100644 --- a/x-pack/plugin/security/qa/jwt-realm/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/jwt/JwtRestIT.java +++ b/x-pack/plugin/security/qa/jwt-realm/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/jwt/JwtRestIT.java @@ -350,7 +350,7 @@ public void testFailureOnRequiredClaims() throws JOSEException, IOException { if (randomBoolean()) { data.put("token_use", randomValueOtherThan("access", () -> randomAlphaOfLengthBetween(3, 10))); } - final JWTClaimsSet claimsSet = buildJwt(data, Instant.now(), false); + final JWTClaimsSet claimsSet = buildJwt(data, Instant.now(), false, false); final SignedJWT jwt = signHmacJwt(claimsSet, "test-HMAC/secret passphrase-value"); final TestSecurityClient client = getSecurityClient(jwt, VALID_SHARED_SECRET); final ResponseException exception = expectThrows(ResponseException.class, client::authenticate); @@ -529,15 +529,16 @@ private SignedJWT buildAndSignJwtForRealm2(String principal, Instant issueTime) private JWTClaimsSet buildJwtForRealm2(String principal, Instant issueTime) { // The "jwt2" realm, supports 3 audiences (es01/02/03) final String audience = "es0" + randomIntBetween(1, 3); - final Map data = new HashMap<>( - Map.of("iss", "my-issuer", "aud", audience, "email", principal, "token_use", "access") - ); - // scope (fallback audience) is ignored since aud exists + final Map data = new HashMap<>(Map.of("iss", "my-issuer", "email", principal, "token_use", "access")); if (randomBoolean()) { + data.put("aud", audience); + // scope (fallback audience) is ignored since aud exists data.put("scope", randomAlphaOfLength(20)); + } else { + data.put("scope", audience); } - final JWTClaimsSet claimsSet = buildJwt(data, issueTime, false); + final JWTClaimsSet claimsSet = buildJwt(data, issueTime, false, false); return claimsSet; } @@ -597,16 +598,18 @@ private SignedJWT signHmacJwt(JWTClaimsSet claimsSet, String hmacPassphrase) thr // JWT construction private JWTClaimsSet buildJwt(Map claims, Instant issueTime) { - return buildJwt(claims, issueTime, true); + return buildJwt(claims, issueTime, true, true); } - private JWTClaimsSet buildJwt(Map claims, Instant issueTime, boolean includeSub) { + private JWTClaimsSet buildJwt(Map claims, Instant issueTime, boolean includeSub, boolean includeAud) { final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder(); builder.issuer(randomAlphaOfLengthBetween(4, 24)); if (includeSub) { builder.subject(randomAlphaOfLengthBetween(4, 24)); } - builder.audience(randomList(1, 6, () -> randomAlphaOfLengthBetween(4, 12))); + if (includeAud) { + builder.audience(randomList(1, 6, () -> randomAlphaOfLengthBetween(4, 12))); + } if (randomBoolean()) { builder.jwtID(UUIDs.randomBase64UUID(random())); } diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmSingleNodeTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmSingleNodeTests.java new file mode 100644 index 000000000000..26e08fd34bde --- /dev/null +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmSingleNodeTests.java @@ -0,0 +1,224 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authc.jwt; + +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.util.Base64URL; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.core.Strings; +import org.elasticsearch.test.SecuritySettingsSource; +import org.elasticsearch.test.SecuritySingleNodeTestCase; +import org.elasticsearch.xpack.security.authc.Realms; + +import java.text.ParseException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; + +public class JwtRealmSingleNodeTests extends SecuritySingleNodeTestCase { + + @Override + protected Settings nodeSettings() { + final Settings.Builder builder = Settings.builder() + .put(super.nodeSettings()) + // 1st JWT realm + .put("xpack.security.authc.realms.jwt.jwt0.order", 10) + .put( + randomBoolean() + ? Settings.builder().put("xpack.security.authc.realms.jwt.jwt0.token_type", "id_token").build() + : Settings.EMPTY + ) + .put("xpack.security.authc.realms.jwt.jwt0.allowed_issuer", "my-issuer-01") + .put("xpack.security.authc.realms.jwt.jwt0.allowed_audiences", "es-01") + .put("xpack.security.authc.realms.jwt.jwt0.claims.principal", "sub") + .put("xpack.security.authc.realms.jwt.jwt0.claims.groups", "groups") + .put("xpack.security.authc.realms.jwt.jwt0.client_authentication.type", "shared_secret") + .putList("xpack.security.authc.realms.jwt.jwt0.allowed_signature_algorithms", "HS256", "HS384") + // 2nd JWT realm + .put("xpack.security.authc.realms.jwt.jwt1.order", 20) + .put("xpack.security.authc.realms.jwt.jwt1.token_type", "access_token") + .put("xpack.security.authc.realms.jwt.jwt1.allowed_issuer", "my-issuer-02") + .put("xpack.security.authc.realms.jwt.jwt1.allowed_subjects", "user-02") + .put("xpack.security.authc.realms.jwt.jwt1.allowed_audiences", "es-02") + .put("xpack.security.authc.realms.jwt.jwt1.fallback_claims.sub", "client_id") + .put("xpack.security.authc.realms.jwt.jwt1.claims.principal", "appid") + .put("xpack.security.authc.realms.jwt.jwt1.claims.groups", "groups") + .put("xpack.security.authc.realms.jwt.jwt1.client_authentication.type", "shared_secret") + .putList("xpack.security.authc.realms.jwt.jwt1.allowed_signature_algorithms", "HS256", "HS384") + // 3rd JWT realm + .put("xpack.security.authc.realms.jwt.jwt2.order", 30) + .put("xpack.security.authc.realms.jwt.jwt2.token_type", "access_token") + .put("xpack.security.authc.realms.jwt.jwt2.allowed_issuer", "my-issuer-03") + .put("xpack.security.authc.realms.jwt.jwt2.allowed_subjects", "user-03") + .put("xpack.security.authc.realms.jwt.jwt2.allowed_audiences", "es-03") + .put("xpack.security.authc.realms.jwt.jwt2.fallback_claims.sub", "oid") + .put("xpack.security.authc.realms.jwt.jwt2.claims.principal", "email") + .put("xpack.security.authc.realms.jwt.jwt2.claims.groups", "groups") + .put("xpack.security.authc.realms.jwt.jwt2.client_authentication.type", "shared_secret") + .putList("xpack.security.authc.realms.jwt.jwt2.allowed_signature_algorithms", "HS256", "HS384"); + + SecuritySettingsSource.addSecureSettings(builder, secureSettings -> { + secureSettings.setString("xpack.security.authc.realms.jwt.jwt0.hmac_key", "jwt0_hmac_key"); + secureSettings.setString("xpack.security.authc.realms.jwt.jwt0.client_authentication.shared_secret", "jwt0_shared_secret"); + secureSettings.setString("xpack.security.authc.realms.jwt.jwt1.hmac_key", "jwt1_hmac_key"); + secureSettings.setString("xpack.security.authc.realms.jwt.jwt1.client_authentication.shared_secret", "jwt1_shared_secret"); + secureSettings.setString("xpack.security.authc.realms.jwt.jwt2.hmac_key", "jwt2_hmac_key"); + secureSettings.setString("xpack.security.authc.realms.jwt.jwt2.client_authentication.shared_secret", "jwt2_shared_secret"); + }); + + return builder.build(); + } + + public void testAnyJwtRealmWillExtractTheToken() throws ParseException { + final List jwtRealms = getJwtRealms(); + final JwtRealm jwtRealm = randomFrom(jwtRealms); + + final String sharedSecret = randomBoolean() ? randomAlphaOfLengthBetween(10, 20) : null; + final String iss = randomAlphaOfLengthBetween(5, 18); + final String aud = randomAlphaOfLengthBetween(5, 18); + final String sub = randomAlphaOfLengthBetween(5, 18); + + // Realm 1 will extract the token because the JWT has all iss, sub, aud, principal claims. + // Their values do not match what realm 1 expects but that does not matter when extracting the token + final SignedJWT signedJWT1 = getSignedJWT(Map.of("iss", iss, "aud", aud, "sub", sub)); + final ThreadContext threadContext1 = prepareThreadContext(signedJWT1, sharedSecret); + final var token1 = (JwtAuthenticationToken) jwtRealm.token(threadContext1); + final String principal1 = Strings.format("%s/%s/%s/%s", iss, aud, sub, sub); + assertJwtToken(token1, principal1, sharedSecret, signedJWT1); + + // Realm 2 for extracting the token from the following JWT + // Because it does not have the sub claim but client_id, which is configured as fallback by realm 2 + final String appId = randomAlphaOfLengthBetween(5, 18); + final SignedJWT signedJWT2 = getSignedJWT(Map.of("iss", iss, "aud", aud, "client_id", sub, "appid", appId)); + final ThreadContext threadContext2 = prepareThreadContext(signedJWT2, sharedSecret); + final var token2 = (JwtAuthenticationToken) jwtRealm.token(threadContext2); + final String principal2 = Strings.format("%s/%s/%s/%s", iss, aud, sub, appId); + assertJwtToken(token2, principal2, sharedSecret, signedJWT2); + + // Realm 3 will extract the token from the following JWT + // Because it has the oid claim which is configured as a fallback by realm 3 + final String email = randomAlphaOfLengthBetween(5, 18) + "@example.com"; + final SignedJWT signedJWT3 = getSignedJWT(Map.of("iss", iss, "aud", aud, "oid", sub, "email", email)); + final ThreadContext threadContext3 = prepareThreadContext(signedJWT3, sharedSecret); + final var token3 = (JwtAuthenticationToken) jwtRealm.token(threadContext3); + final String principal3 = Strings.format("%s/%s/%s/%s", iss, aud, sub, email); + assertJwtToken(token3, principal3, sharedSecret, signedJWT3); + + // The JWT does not match any realm's configuration, a token with generic token principal will be extracted + final SignedJWT signedJWT4 = getSignedJWT(Map.of("iss", iss, "aud", aud, "azp", sub, "email", email)); + final ThreadContext threadContext4 = prepareThreadContext(signedJWT4, sharedSecret); + final var token4 = (JwtAuthenticationToken) jwtRealm.token(threadContext4); + final String principal4 = Strings.format(" by %s", iss); + assertJwtToken(token4, principal4, sharedSecret, signedJWT4); + + // The JWT does not have an issuer, a token with generic token principal will be extracted + final SignedJWT signedJWT5 = getSignedJWT(Map.of("aud", aud, "sub", sub)); + final ThreadContext threadContext5 = prepareThreadContext(signedJWT5, sharedSecret); + final var token5 = (JwtAuthenticationToken) jwtRealm.token(threadContext5); + final String principal5 = ""; + assertJwtToken(token5, principal5, sharedSecret, signedJWT5); + } + + public void testJwtRealmReturnsNullTokenWhenJwtCredentialIsAbsent() { + final List jwtRealms = getJwtRealms(); + final JwtRealm jwtRealm = randomFrom(jwtRealms); + final String sharedSecret = randomBoolean() ? randomAlphaOfLengthBetween(10, 20) : null; + + // Authorization header is absent + final ThreadContext threadContext1 = prepareThreadContext(null, sharedSecret); + assertThat(jwtRealm.token(threadContext1), nullValue()); + + // Scheme is not Bearer + final ThreadContext threadContext2 = prepareThreadContext(null, sharedSecret); + threadContext2.putHeader("Authorization", "Basic foobar"); + assertThat(jwtRealm.token(threadContext2), nullValue()); + } + + public void testJwtRealmThrowsErrorOnJwtParsingFailure() throws ParseException { + final List jwtRealms = getJwtRealms(); + final JwtRealm jwtRealm = randomFrom(jwtRealms); + final String sharedSecret = randomBoolean() ? randomAlphaOfLengthBetween(10, 20) : null; + + // Not a JWT + final ThreadContext threadContext1 = prepareThreadContext(null, sharedSecret); + threadContext1.putHeader("Authorization", "Bearer " + randomAlphaOfLengthBetween(40, 60)); + final IllegalArgumentException e1 = expectThrows(IllegalArgumentException.class, () -> jwtRealm.token(threadContext1)); + assertThat(e1.getMessage(), containsString("Failed to parse JWT bearer token")); + + // Payload is not JSON + final SignedJWT signedJWT2 = new SignedJWT( + JWSHeader.parse(Map.of("alg", randomAlphaOfLengthBetween(5, 10))).toBase64URL(), + Base64URL.encode("payload"), + Base64URL.encode("signature") + ); + final ThreadContext threadContext2 = prepareThreadContext(null, sharedSecret); + threadContext2.putHeader("Authorization", "Bearer " + signedJWT2.serialize()); + final IllegalArgumentException e2 = expectThrows(IllegalArgumentException.class, () -> jwtRealm.token(threadContext2)); + assertThat(e2.getMessage(), containsString("Failed to parse JWT claims set")); + } + + private void assertJwtToken(JwtAuthenticationToken token, String tokenPrincipal, String sharedSecret, SignedJWT signedJWT) + throws ParseException { + assertThat(token.principal(), equalTo(tokenPrincipal)); + assertThat(token.getClientAuthenticationSharedSecret(), equalTo(sharedSecret)); + assertThat(token.getJWTClaimsSet(), equalTo(signedJWT.getJWTClaimsSet())); + assertThat(token.getSignedJWT().getHeader().toJSONObject(), equalTo(signedJWT.getHeader().toJSONObject())); + assertThat(token.getSignedJWT().getSignature(), equalTo(signedJWT.getSignature())); + assertThat(token.getSignedJWT().getJWTClaimsSet(), equalTo(token.getJWTClaimsSet())); + } + + private List getJwtRealms() { + final Realms realms = getInstanceFromNode(Realms.class); + final List jwtRealms = realms.getActiveRealms() + .stream() + .filter(realm -> realm instanceof JwtRealm) + .map(JwtRealm.class::cast) + .toList(); + return jwtRealms; + } + + private SignedJWT getSignedJWT(Map m) throws ParseException { + final HashMap claimsMap = new HashMap<>(m); + final Instant now = Instant.now(); + // timestamp does not matter for tokenExtraction + claimsMap.put("iat", now.minus(randomIntBetween(-1, 1), ChronoUnit.DAYS).getEpochSecond()); + claimsMap.put("exp", now.plus(randomIntBetween(-1, 1), ChronoUnit.DAYS).getEpochSecond()); + + final JWTClaimsSet claimsSet = JWTClaimsSet.parse(claimsMap); + final SignedJWT signedJWT = new SignedJWT( + JWSHeader.parse(Map.of("alg", randomAlphaOfLengthBetween(5, 10))).toBase64URL(), + claimsSet.toPayload().toBase64URL(), + Base64URL.encode("signature") + ); + return signedJWT; + } + + private ThreadContext prepareThreadContext(SignedJWT signedJWT, String clientSecret) { + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + if (signedJWT != null) { + threadContext.putHeader("Authorization", "Bearer " + signedJWT.serialize()); + } + if (clientSecret != null) { + threadContext.putHeader( + JwtRealm.HEADER_CLIENT_AUTHENTICATION, + JwtRealm.HEADER_SHARED_SECRET_AUTHENTICATION_SCHEME + " " + clientSecret + ); + } + return threadContext; + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index e3606b1b2775..7621b7297b8c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -160,7 +160,6 @@ import org.elasticsearch.xpack.core.security.authc.Realm; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; -import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmsServiceSettings; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine; import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; @@ -1094,7 +1093,6 @@ public static List> getSettings(List securityExten // authentication and authorization settings AnonymousUser.addSettings(settingsList); settingsList.addAll(InternalRealmsSettings.getSettings()); - settingsList.addAll(JwtRealmsServiceSettings.getSettings()); ReservedRealm.addSettings(settingsList); AuthenticationService.addSettings(settingsList); AuthorizationService.addSettings(settingsList); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java index 9e697baa76de..2cc45791286f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/InternalRealms.java @@ -32,7 +32,7 @@ import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.elasticsearch.xpack.security.authc.file.FileRealm; -import org.elasticsearch.xpack.security.authc.jwt.JwtRealmsService; +import org.elasticsearch.xpack.security.authc.jwt.JwtRealm; import org.elasticsearch.xpack.security.authc.kerberos.KerberosRealm; import org.elasticsearch.xpack.security.authc.ldap.LdapRealm; import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm; @@ -137,7 +137,6 @@ public static Map getFactories( NativeRoleMappingStore nativeRoleMappingStore, SecurityIndexManager securityIndex ) { - final JwtRealmsService jwtRealmsService = new JwtRealmsService(settings); // parse shared settings needed by all JwtRealm instances return Map.of( // file realm FileRealmSettings.TYPE, @@ -169,7 +168,7 @@ public static Map getFactories( config -> new OpenIdConnectRealm(config, sslService, nativeRoleMappingStore, resourceWatcherService), // JWT realm JwtRealmSettings.TYPE, - config -> jwtRealmsService.createJwtRealm(config, sslService, nativeRoleMappingStore) + config -> new JwtRealm(config, sslService, nativeRoleMappingStore) ); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticationToken.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticationToken.java index 4adc33450463..a80865428e59 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticationToken.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticationToken.java @@ -9,121 +9,51 @@ import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.core.Nullable; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; -import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmsServiceSettings; import java.text.ParseException; -import java.util.List; -import java.util.Map; -import java.util.TreeSet; -import java.util.stream.Collectors; +import java.util.Arrays; +import java.util.Objects; /** * An {@link AuthenticationToken} to hold JWT authentication related content. */ public class JwtAuthenticationToken implements AuthenticationToken { - private static final Logger LOGGER = LogManager.getLogger(JwtAuthenticationToken.class); - - // Stored members - protected SecureString endUserSignedJwt; // required - protected SecureString clientAuthenticationSharedSecret; // optional, nullable - protected String principal; // Defaults to "iss/aud/sub", with an ordered "aud" list + private final String principal; + private SignedJWT signedJWT; + private final byte[] userCredentialsHash; + @Nullable + private final SecureString clientAuthenticationSharedSecret; /** - * Store a mandatory JWT and optional Shared Secret. Parse the JWT, and extract the header, claims set, and signature. - * Compute a token principal, for use as a realm order cache key. For OIDC ID Tokens, cache key is iss/aud/sub. - * For other JWTs, {@link JwtRealmsServiceSettings#PRINCIPAL_CLAIMS_SETTING} supports alternative claims for sub. - * Throws IllegalArgumentException if principalClaimNames is empty, JWT is missing, or if JWT parsing fails. - * @param principalClaimNames Ordered list of string claims to use for principalClaimValue. The first one found is used (ex: sub). - * @param endUserSignedJwt Base64Url-encoded JWT for End-user authentication. Required by all JWT realms. + * Store a mandatory JWT and optional Shared Secret. + * @param principal The token's principal, useful as a realm order cache key + * @param signedJWT The JWT parsed from the end-user credentials + * @param userCredentialsHash The hash of the end-user credentials is used to compute the key for user cache at the realm level. + * See also {@link JwtRealm#authenticate}. * @param clientAuthenticationSharedSecret URL-safe Shared Secret for Client authentication. Required by some JWT realms. */ public JwtAuthenticationToken( - final List principalClaimNames, - final SecureString endUserSignedJwt, + String principal, + SignedJWT signedJWT, + byte[] userCredentialsHash, @Nullable final SecureString clientAuthenticationSharedSecret ) { - if (principalClaimNames.isEmpty()) { - throw new IllegalArgumentException("JWT token principal claim names list must be non-empty"); - } else if (endUserSignedJwt.isEmpty()) { - throw new IllegalArgumentException("JWT bearer token must be non-empty"); - } else if ((clientAuthenticationSharedSecret != null) && (clientAuthenticationSharedSecret.isEmpty())) { - throw new IllegalArgumentException("Client shared secret must be non-empty"); - } - this.endUserSignedJwt = endUserSignedJwt; // required - this.clientAuthenticationSharedSecret = clientAuthenticationSharedSecret; // optional, nullable - - JWTClaimsSet jwtClaimsSet; - try { - jwtClaimsSet = SignedJWT.parse(this.endUserSignedJwt.toString()).getJWTClaimsSet(); - } catch (ParseException e) { - throw new IllegalArgumentException("Failed to parse JWT bearer token", e); - } - - // get and validate iss and aud claims - final String issuer = jwtClaimsSet.getIssuer(); - final List audiences = jwtClaimsSet.getAudience(); - if (Strings.hasText(issuer) == false) { - throw new IllegalArgumentException("Issuer claim 'iss' is missing."); - } else if ((audiences == null) || (audiences.isEmpty())) { - throw new IllegalArgumentException("Audiences claim 'aud' is missing."); - } - - // get and validate sub claim, or the first configured backup claim (if sub is absent) - final String principalClaimValue = this.resolvePrincipalClaimName(jwtClaimsSet, principalClaimNames); - this.principal = issuer + "/" + String.join(",", new TreeSet<>(audiences)) + "/" + principalClaimValue; - } + this.principal = Objects.requireNonNull(principal); + this.signedJWT = Objects.requireNonNull(signedJWT); + this.userCredentialsHash = Objects.requireNonNull(userCredentialsHash); - private String resolvePrincipalClaimName(final JWTClaimsSet jwtClaimsSet, final List principalClaimNames) { - for (final String principalClaimName : principalClaimNames) { - final Object claimValue = jwtClaimsSet.getClaim(principalClaimName); - if (claimValue instanceof String principalClaimValue) { - // found an allowed string claim name - if (principalClaimValue.isEmpty()) { - throw new IllegalArgumentException( - "Allowed principal claim name '" - + principalClaimName - + "' exists but cannot be used because the value of that claim is an empty string" - ); - } - LOGGER.trace("Found allowed principal claim name [{}] with value [{}]", principalClaimName, principalClaimValue); - return principalClaimValue; - } else if (claimValue != null) { - throw new IllegalArgumentException( - "Allowed principal claim name '" - + principalClaimName - + "' exists but cannot be used because the value of that claim must be a string, but instead it was a [" - + claimValue.getClass().getSimpleName() - + "]" - ); - } + if ((clientAuthenticationSharedSecret != null) && (clientAuthenticationSharedSecret.isEmpty())) { + throw new IllegalArgumentException("Client shared secret must be non-empty"); } - - // at this point, none of the principalClaimNames were found - // throw an exception with a detailed log message about which string claims were available in the JWT - final String allClaimNamesWithStringValues = jwtClaimsSet.getClaims() - .entrySet() - .stream() - .filter(e -> e.getValue() instanceof String) - .map(Map.Entry::getKey) - .collect(Collectors.joining(",")); - throw new IllegalArgumentException( - "None of these configured principal claim names were found in the JWT Claims Set [" - + String.join(",", principalClaimNames) - + "] - available claims in the JWT with potential compatible string values are [" - + allClaimNamesWithStringValues - + "]" - ); + this.clientAuthenticationSharedSecret = clientAuthenticationSharedSecret; } @Override public String principal() { - return this.principal; + return principal; } @Override @@ -131,23 +61,34 @@ public SecureString credentials() { return null; } - public SecureString getEndUserSignedJwt() { - return this.endUserSignedJwt; + public SignedJWT getSignedJWT() { + return signedJWT; + } + + public JWTClaimsSet getJWTClaimsSet() { + try { + return signedJWT.getJWTClaimsSet(); + } catch (ParseException e) { + assert false : "The JWT claims set should have already been successfully parsed before building the JWT authentication token"; + throw new IllegalArgumentException(e); + } + } + + public byte[] getUserCredentialsHash() { + return userCredentialsHash; } public SecureString getClientAuthenticationSharedSecret() { - return this.clientAuthenticationSharedSecret; + return clientAuthenticationSharedSecret; } @Override public void clearCredentials() { - this.endUserSignedJwt.close(); - this.endUserSignedJwt = null; - if (this.clientAuthenticationSharedSecret != null) { - this.clientAuthenticationSharedSecret.close(); - this.clientAuthenticationSharedSecret = null; + signedJWT = null; + Arrays.fill(userCredentialsHash, (byte) 0); + if (clientAuthenticationSharedSecret != null) { + clientAuthenticationSharedSecret.close(); } - this.principal = null; } @Override diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticator.java index d086248d7b53..17d6a22bdb43 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticator.java @@ -14,7 +14,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.Releasable; import org.elasticsearch.core.TimeValue; @@ -22,7 +21,6 @@ import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings; import org.elasticsearch.xpack.core.ssl.SSLService; -import java.text.ParseException; import java.time.Clock; import java.util.ArrayList; import java.util.List; @@ -67,28 +65,11 @@ public JwtAuthenticator( public void authenticate(JwtAuthenticationToken jwtAuthenticationToken, ActionListener listener) { final String tokenPrincipal = jwtAuthenticationToken.principal(); - // JWT cache - final SecureString serializedJwt = jwtAuthenticationToken.getEndUserSignedJwt(); - final SignedJWT signedJWT; - try { - signedJWT = SignedJWT.parse(serializedJwt.toString()); - } catch (ParseException e) { - // TODO: No point to continue to another realm since parsing failed - listener.onFailure(e); - return; - } - - final JWTClaimsSet jwtClaimsSet; - try { - jwtClaimsSet = signedJWT.getJWTClaimsSet(); - } catch (ParseException e) { - // TODO: No point to continue to another realm since get claimset failed - listener.onFailure(e); - return; - } - + final SignedJWT signedJWT = jwtAuthenticationToken.getSignedJWT(); + final JWTClaimsSet jwtClaimsSet = jwtAuthenticationToken.getJWTClaimsSet(); final JWSHeader jwsHeader = signedJWT.getHeader(); + if (logger.isDebugEnabled()) { logger.debug( "Realm [{}] successfully parsed JWT token [{}] with header [{}] and claimSet [{}]", diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealm.java index 495fd23ac23f..9d5c8daed681 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealm.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.security.authc.jwt; import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.Strings; @@ -34,6 +35,8 @@ import org.elasticsearch.xpack.security.authc.support.ClaimParser; import org.elasticsearch.xpack.security.authc.support.DelegatedAuthorizationSupport; +import java.text.ParseException; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -41,6 +44,8 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.TreeSet; +import java.util.function.Function; import static java.lang.String.join; import static org.elasticsearch.core.Strings.format; @@ -58,7 +63,6 @@ public class JwtRealm extends Realm implements CachingRealm, Releasable { private final Cache jwtCache; private final CacheIteratorHelper jwtCacheHelper; - private final JwtRealmsService jwtRealmsService; private final UserRoleMapper userRoleMapper; private final Boolean populateUserMetadata; private final ClaimParser claimParserPrincipal; @@ -71,15 +75,11 @@ public class JwtRealm extends Realm implements CachingRealm, Releasable { private final JwtAuthenticator jwtAuthenticator; private final TimeValue allowedClockSkew; DelegatedAuthorizationSupport delegatedAuthorizationSupport = null; + private List> tokenPrincipalFunctions; - JwtRealm( - final RealmConfig realmConfig, - final JwtRealmsService jwtRealmsService, - final SSLService sslService, - final UserRoleMapper userRoleMapper - ) throws SettingsException { + public JwtRealm(final RealmConfig realmConfig, final SSLService sslService, final UserRoleMapper userRoleMapper) + throws SettingsException { super(realmConfig); - this.jwtRealmsService = jwtRealmsService; // common configuration settings shared by all JwtRealm instances this.userRoleMapper = userRoleMapper; this.userRoleMapper.refreshRealmOnChange(this); this.allowedClockSkew = realmConfig.getSetting(JwtRealmSettings.ALLOWED_CLOCK_SKEW); @@ -140,6 +140,14 @@ public void initialize(final Iterable allRealms, final XPackLicenseState } // extract list of realms referenced by config.settings() value for DelegatedAuthorizationSettings.ROLES_REALMS delegatedAuthorizationSupport = new DelegatedAuthorizationSupport(allRealms, config, xpackLicenseState); + + final List> tokenPrincipalFunctions = new ArrayList<>(); + for (var realm : allRealms) { + if (realm instanceof final JwtRealm jwtRealm) { + tokenPrincipalFunctions.add(jwtRealm::buildTokenPrincipal); + } + } + this.tokenPrincipalFunctions = List.copyOf(tokenPrincipalFunctions); } /** @@ -176,10 +184,66 @@ public void expireAll() { @Override public AuthenticationToken token(final ThreadContext threadContext) { ensureInitialized(); - // Token parsing is common code for all realms - // First JWT realm will parse in a way that is compatible with all JWT realms, - // taking into consideration each JWT realm might have a different principal claim name - return jwtRealmsService.token(threadContext); + + final SecureString userCredentials = JwtUtil.getHeaderValue( + threadContext, + JwtRealm.HEADER_END_USER_AUTHENTICATION, + JwtRealm.HEADER_END_USER_AUTHENTICATION_SCHEME, + false + ); + if (userCredentials == null) { + return null; + } + if (userCredentials.isEmpty()) { + throw new IllegalArgumentException("JWT bearer token must be non-empty"); + } + + final SecureString clientCredentials = JwtUtil.getHeaderValue( + threadContext, + JwtRealm.HEADER_CLIENT_AUTHENTICATION, + JwtRealm.HEADER_SHARED_SECRET_AUTHENTICATION_SCHEME, + true + ); + + // No point to fall through the realm chain if JWT parsing fails, so we throw error here on failure. + final SignedJWT signedJWT; + try { + signedJWT = SignedJWT.parse(userCredentials.toString()); + } catch (ParseException e) { + throw new IllegalArgumentException("Failed to parse JWT bearer token", e); + } + + final JWTClaimsSet jwtClaimsSet; + try { + jwtClaimsSet = signedJWT.getJWTClaimsSet(); + } catch (ParseException e) { + throw new IllegalArgumentException("Failed to parse JWT claims set", e); + } + + // If Issuer is not found, still return a JWT token since it is after still a JWT, authentication + // will fail later because issuer is mandated + final String issuer = jwtClaimsSet.getIssuer(); + if (Strings.hasText(issuer) == false) { + logger.warn("Issuer claim 'iss' is missing."); + return new JwtAuthenticationToken("", signedJWT, JwtUtil.sha256(userCredentials), clientCredentials); + } + + // Try all known extraction functions to build the token principal + for (Function func : tokenPrincipalFunctions) { + final String tokenPrincipalSuffix = func.apply(jwtClaimsSet); + if (tokenPrincipalSuffix != null) { + return new JwtAuthenticationToken( + issuer + "/" + tokenPrincipalSuffix, + signedJWT, + JwtUtil.sha256(userCredentials), + clientCredentials + ); + } + } + + // Token principal cannot be extracted even after trying all functions, but this is + // still a JWT token so that we should return as one. + return new JwtAuthenticationToken(" by " + issuer, signedJWT, JwtUtil.sha256(userCredentials), clientCredentials); } @Override @@ -205,8 +269,7 @@ public void authenticate(final AuthenticationToken authenticationToken, final Ac return; // FAILED (secret is missing or mismatched) } - final SecureString serializedJwt = jwtAuthenticationToken.getEndUserSignedJwt(); - final BytesArray jwtCacheKey = isCacheEnabled() ? new BytesArray(JwtUtil.sha256(serializedJwt)) : null; + final BytesArray jwtCacheKey = isCacheEnabled() ? new BytesArray(jwtAuthenticationToken.getUserCredentialsHash()) : null; if (jwtCacheKey != null) { final User cachedUser = tryAuthenticateWithCache(tokenPrincipal, jwtCacheKey); if (cachedUser != null) { @@ -264,40 +327,21 @@ private User tryAuthenticateWithCache(final String tokenPrincipal, final BytesAr final Date exp = expiringUser.exp; // claimsSet.getExpirationTime().getTime() + allowedClockSkew.getMillis() final String principal = user.principal(); final Date now = new Date(); - if (now.getTime() < exp.getTime()) { - logger.trace( - "Realm [" - + name() - + "] JWT cache hit token=[" - + tokenPrincipal - + "] key=[" - + jwtCacheKey - + "] principal=[" - + principal - + "] exp=[" - + exp - + "] now=[" - + now - + "]." - ); + final boolean cacheEntryNotExpired = now.getTime() < exp.getTime(); + logger.trace( + "Realm [{}] JWT cache {} token=[{}] key=[{}] principal=[{}] exp=[{}] now=[{}].", + name(), + cacheEntryNotExpired ? "hit" : "exp", + tokenPrincipal, + jwtCacheKey, + principal, + exp, + now + ); + if (cacheEntryNotExpired) { return user; } // TODO: evict the entry - logger.trace( - "Realm [" - + name() - + "] JWT cache exp token=[" - + tokenPrincipal - + "] key=[" - + jwtCacheKey - + "] principal=[" - + principal - + "] exp=[" - + exp - + "] now=[" - + now - + "]." - ); } return null; } @@ -410,6 +454,30 @@ private Map buildUserMetadata(JWTClaimsSet claimsSet) { return Map.copyOf(metadata); } + private String buildTokenPrincipal(JWTClaimsSet jwtClaimsSet) { + final Map fallbackClaimNames = jwtAuthenticator.getFallbackClaimNames(); + final FallbackableClaim subClaim = new FallbackableClaim("sub", fallbackClaimNames, jwtClaimsSet); + final String subject = subClaim.getStringClaimValue(); + if (false == Strings.hasText(subject)) { + logger.debug("claim [{}] is missing for building token principal for realm [{}]", subClaim, name()); + return null; + } + + final FallbackableClaim audClaim = new FallbackableClaim("aud", fallbackClaimNames, jwtClaimsSet); + final List audiences = audClaim.getStringListClaimValue(); + if (audiences == null || audiences.isEmpty()) { + logger.debug("claim [{}] is missing for building token principal for realm [{}]", audClaim, name()); + return null; + } + + final String userPrincipal = claimParserPrincipal.getClaimValue(jwtClaimsSet); + if (false == Strings.hasText(userPrincipal)) { + logger.debug("No user principal can be extracted with [{}] for realm [{}]", claimParserPrincipal, name()); + return null; + } + return String.join(",", new TreeSet<>(audiences)) + "/" + subject + "/" + userPrincipal; + } + /** * JWTClaimsSet values are only allowed to be String, Boolean, Number, or Collection. * Collections are only allowed to contain String, Boolean, or Number. diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmsService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmsService.java deleted file mode 100644 index 1be9e86de3dc..000000000000 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmsService.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -package org.elasticsearch.xpack.security.authc.jwt; - -import org.elasticsearch.common.settings.SecureString; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; -import org.elasticsearch.xpack.core.security.authc.RealmConfig; -import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmsServiceSettings; -import org.elasticsearch.xpack.core.ssl.SSLService; -import org.elasticsearch.xpack.security.authc.InternalRealms; -import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; - -import java.util.Collections; -import java.util.List; - -/** - * Parse common settings shared by all JwtRealm instances on behalf of InternalRealms. - * Construct JwtRealm instances on behalf of lambda defined in InternalRealms. - * Construct AuthenticationToken instances on behalf of JwtRealm instances. - * @see InternalRealms - * @see JwtRealm - */ -public class JwtRealmsService { - - private final List principalClaimNames; - - /** - * Parse all xpack settings passed in from {@link InternalRealms#getFactories} - * @param settings All xpack settings - */ - public JwtRealmsService(final Settings settings) { - this.principalClaimNames = Collections.unmodifiableList(JwtRealmsServiceSettings.PRINCIPAL_CLAIMS_SETTING.get(settings)); - } - - /** - * Return prioritized list of principal claim names to use for computing realm cache keys for all JWT realms. - * @return Prioritized list of principal claim names (ex: sub, oid, client_id, azp, appid, client_id, email). - */ - public List getPrincipalClaimNames() { - return this.principalClaimNames; - } - - /** - * Construct JwtRealm instance using settings passed in via lambda defined in {@link InternalRealms#getFactories} - * @param config Realm config - * @param sslService SSL service settings - * @param nativeRoleMappingStore Native role mapping store - */ - public JwtRealm createJwtRealm( - final RealmConfig config, - final SSLService sslService, - final NativeRoleMappingStore nativeRoleMappingStore - ) { - return new JwtRealm(config, this, sslService, nativeRoleMappingStore); - } - - /** - * Construct JwtAuthenticationToken instance using request passed in via JwtRealm.token. - * @param threadContext Request headers and parameters - * @return JwtAuthenticationToken contains mandatory JWT header, optional client secret, and a realm order cache key - */ - AuthenticationToken token(final ThreadContext threadContext) { - // extract value from Authorization header with Bearer scheme prefix - final SecureString authenticationParameterValue = JwtUtil.getHeaderValue( - threadContext, - JwtRealm.HEADER_END_USER_AUTHENTICATION, - JwtRealm.HEADER_END_USER_AUTHENTICATION_SCHEME, - false - ); - if (authenticationParameterValue == null) { - return null; - } - // extract value from ES-Client-Authentication header with SharedSecret scheme prefix - final SecureString clientAuthenticationSharedSecretValue = JwtUtil.getHeaderValue( - threadContext, - JwtRealm.HEADER_CLIENT_AUTHENTICATION, - JwtRealm.HEADER_SHARED_SECRET_AUTHENTICATION_SCHEME, - true - ); - return new JwtAuthenticationToken(this.principalClaimNames, authenticationParameterValue, clientAuthenticationSharedSecretValue); - } -} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticationTokenTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticationTokenTests.java deleted file mode 100644 index 120040b30444..000000000000 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticationTokenTests.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -package org.elasticsearch.xpack.security.authc.jwt; - -import com.nimbusds.jose.JOSEObjectType; -import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jwt.SignedJWT; - -import org.elasticsearch.common.settings.SecureString; -import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings; -import org.junit.Assert; - -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Date; -import java.util.List; -import java.util.Map; - -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; - -public class JwtAuthenticationTokenTests extends JwtTestCase { - - public void testJwtAuthenticationTokenParse() throws Exception { - final String signatureAlgorithm = randomFrom(JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS); - final JWK jwk = JwtTestCase.randomJwk(signatureAlgorithm, randomBoolean()); - - final SecureString jwt = JwtTestCase.randomBespokeJwt(jwk, signatureAlgorithm); // bespoke JWT, not tied to any JWT realm - final SecureString clientSharedSecret = randomBoolean() ? null : new SecureString(randomAlphaOfLengthBetween(10, 20).toCharArray()); - - final List principalClaimNames = List.of(randomAlphaOfLength(4), "sub", randomAlphaOfLength(4)); - final JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(principalClaimNames, jwt, clientSharedSecret); - final SecureString endUserSignedJwt = jwtAuthenticationToken.getEndUserSignedJwt(); - final SecureString clientAuthenticationSharedSecret = jwtAuthenticationToken.getClientAuthenticationSharedSecret(); - - Assert.assertEquals(jwt, endUserSignedJwt); - Assert.assertEquals(clientSharedSecret, clientAuthenticationSharedSecret); - - jwtAuthenticationToken.clearCredentials(); - - // verify references to SecureString throw exception when calling their methods - final Exception exception1 = expectThrows(IllegalStateException.class, endUserSignedJwt::length); - assertThat(exception1.getMessage(), equalTo("SecureString has already been closed")); - if (clientAuthenticationSharedSecret != null) { - final Exception exception2 = expectThrows(IllegalStateException.class, clientAuthenticationSharedSecret::length); - assertThat(exception2.getMessage(), equalTo("SecureString has already been closed")); - } - - // verify token returns nulls - assertThat(jwtAuthenticationToken.principal(), is(nullValue())); - assertThat(jwtAuthenticationToken.credentials(), is(nullValue())); - assertThat(jwtAuthenticationToken.getEndUserSignedJwt(), is(nullValue())); - assertThat(jwtAuthenticationToken.getClientAuthenticationSharedSecret(), is(nullValue())); - } - - public void testPrincipalForJwtWithoutSub() throws Exception { - final String issuer = randomAlphaOfLengthBetween(8, 24); - final String audience = randomAlphaOfLengthBetween(6, 12); - - final String principalClaimName = randomValueOtherThan("sub", () -> randomAlphaOfLength(3)); - final String principalClaimValue = randomAlphaOfLengthBetween(8, 32); - - final String signatureAlgorithm = randomFrom(JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS); - final JWK jwk = JwtTestCase.randomJwk(signatureAlgorithm, randomBoolean()); - - final Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); - final SignedJWT unsignedJwt = JwtTestCase.buildUnsignedJwt( - randomBoolean() ? null : JOSEObjectType.JWT.toString(), // kty - randomBoolean() ? null : jwk.getKeyID(), // kid - signatureAlgorithm, // alg - null, // jwtID - issuer, // iss - List.of(audience), // aud - null, // sub claim value - principalClaimName, // principal claim name - principalClaimValue, // principal claim value - null, // groups claim - List.of(), // groups - Date.from(now.minusSeconds(60 * randomLongBetween(10, 20))), // auth_time - Date.from(now.minusSeconds(randomBoolean() ? 0 : 60 * randomLongBetween(5, 10))), // iat - Date.from(now), // nbf - Date.from(now.plusSeconds(60 * randomLongBetween(3600, 7200))), // exp - null, // nonce - Map.of() // other claims - ); - final SecureString jwt = JwtValidateUtil.signJwt(jwk, unsignedJwt); - - final List principalClaimNames = List.of(randomAlphaOfLength(4), "sub", principalClaimName); - final JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(principalClaimNames, jwt, null); - Assert.assertEquals(issuer + "/" + audience + "/" + principalClaimValue, jwtAuthenticationToken.principal()); - } -} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorTests.java index 6960eaa927ed..4d1673210423 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtAuthenticatorTests.java @@ -15,7 +15,6 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.settings.MockSecureSettings; -import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.core.Nullable; @@ -102,7 +101,8 @@ public void testRequiredClaims() throws ParseException { ); final JwtAuthenticationToken jwtAuthenticationToken = mock(JwtAuthenticationToken.class); - when(jwtAuthenticationToken.getEndUserSignedJwt()).thenReturn(new SecureString(signedJWT.serialize().toCharArray())); + when(jwtAuthenticationToken.getSignedJWT()).thenReturn(signedJWT); + when(jwtAuthenticationToken.getJWTClaimsSet()).thenReturn(signedJWT.getJWTClaimsSet()); final PlainActionFuture future = new PlainActionFuture<>(); final JwtAuthenticator jwtAuthenticator = buildJwtAuthenticator(); @@ -139,7 +139,8 @@ public void testMismatchedRequiredClaims() throws ParseException { ); final JwtAuthenticationToken jwtAuthenticationToken = mock(JwtAuthenticationToken.class); - when(jwtAuthenticationToken.getEndUserSignedJwt()).thenReturn(new SecureString(signedJWT.serialize().toCharArray())); + when(jwtAuthenticationToken.getSignedJWT()).thenReturn(signedJWT); + when(jwtAuthenticationToken.getJWTClaimsSet()).thenReturn(signedJWT.getJWTClaimsSet()); final PlainActionFuture future = new PlainActionFuture<>(); final JwtAuthenticator jwtAuthenticator = buildJwtAuthenticator(); @@ -182,7 +183,8 @@ public void testMissingRequiredClaims() throws ParseException { ); final JwtAuthenticationToken jwtAuthenticationToken = mock(JwtAuthenticationToken.class); - when(jwtAuthenticationToken.getEndUserSignedJwt()).thenReturn(new SecureString(signedJWT.serialize().toCharArray())); + when(jwtAuthenticationToken.getSignedJWT()).thenReturn(signedJWT); + when(jwtAuthenticationToken.getJWTClaimsSet()).thenReturn(signedJWT.getJWTClaimsSet()); // Required claim is mandatory when configured final PlainActionFuture future1 = new PlainActionFuture<>(); @@ -204,7 +206,8 @@ protected IllegalArgumentException doTestSubjectIsRequired(JwtAuthenticator jwtA Base64URL.encode("signature") ); final JwtAuthenticationToken jwtAuthenticationToken = mock(JwtAuthenticationToken.class); - when(jwtAuthenticationToken.getEndUserSignedJwt()).thenReturn(new SecureString(signedJWT.serialize().toCharArray())); + when(jwtAuthenticationToken.getSignedJWT()).thenReturn(signedJWT); + when(jwtAuthenticationToken.getJWTClaimsSet()).thenReturn(signedJWT.getJWTClaimsSet()); final PlainActionFuture future = new PlainActionFuture<>(); jwtAuthenticator.authenticate(jwtAuthenticationToken, future); @@ -221,7 +224,8 @@ protected void doTestInvalidIssuerIsCheckedBeforeAlgorithm(JwtAuthenticator jwtA Base64URL.encode("signature") ); final JwtAuthenticationToken jwtAuthenticationToken = mock(JwtAuthenticationToken.class); - when(jwtAuthenticationToken.getEndUserSignedJwt()).thenReturn(new SecureString(signedJWT.serialize().toCharArray())); + when(jwtAuthenticationToken.getSignedJWT()).thenReturn(signedJWT); + when(jwtAuthenticationToken.getJWTClaimsSet()).thenReturn(signedJWT.getJWTClaimsSet()); final PlainActionFuture future = new PlainActionFuture<>(); jwtAuthenticator.authenticate(jwtAuthenticationToken, future); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtIssuer.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtIssuer.java index 008f48681855..1191e834b5c2 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtIssuer.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtIssuer.java @@ -23,7 +23,9 @@ import java.util.List; import java.util.Map; +import static org.elasticsearch.test.ESTestCase.randomAlphaOfLengthBetween; import static org.elasticsearch.test.ESTestCase.randomBoolean; +import static org.elasticsearch.test.ESTestCase.randomFrom; /** * Test class with settings for a JWT issuer to sign JWTs for users. @@ -37,7 +39,7 @@ record AlgJwkPair(String alg, JWK jwk) {} // input parameters final String issuerClaimValue; // claim name is hard-coded to `iss` for OIDC ID Token compatibility final List audiencesClaimValue; // claim name is hard-coded to `aud` for OIDC ID Token compatibility - final String principalClaimName; // claim name is configurable, EX: Users (sub, oid, email, dn, uid), Clients (azp, appid, client_id) + final String principalClaimName; final Map principals; // principals with roles, for sending encoded JWTs into JWT realms for authc/authz verification final JwtIssuerHttpsServer httpsServer; @@ -56,15 +58,14 @@ record AlgJwkPair(String alg, JWK jwk) {} JwtIssuer( final String issuerClaimValue, final List audiencesClaimValue, - final String principalClaimName, final Map principals, final boolean createHttpsServer ) throws Exception { this.issuerClaimValue = issuerClaimValue; this.audiencesClaimValue = audiencesClaimValue; - this.principalClaimName = principalClaimName; this.principals = principals; this.httpsServer = createHttpsServer ? new JwtIssuerHttpsServer(null) : null; + this.principalClaimName = randomFrom("sub", "oid", "client_id", "appid", "azp", "email", randomAlphaOfLengthBetween(12, 18)); } // The flag areHmacJwksOidcSafe indicates if all provided HMAC JWKs are UTF8, for HMAC OIDC JWK encoding compatibility. diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateAccessTokenTypeTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateAccessTokenTypeTests.java index b0dd07c67fbb..b7e5d45c5fa2 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateAccessTokenTypeTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateAccessTokenTypeTests.java @@ -38,7 +38,6 @@ public void testAccessTokenTypeWorksWithNoFallback() throws Exception { noFallback(); jwtIssuerAndRealms = generateJwtIssuerRealmPairs( - createJwtRealmsSettingsBuilder(), randomIntBetween(1, 1), // realms randomIntBetween(0, 1), // authz randomIntBetween(1, JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS.size()), // algorithms @@ -60,7 +59,6 @@ public void testAccessTokenTypeWorksWithFallbacks() throws Exception { randomFallbacks(); jwtIssuerAndRealms = generateJwtIssuerRealmPairs( - createJwtRealmsSettingsBuilder(), randomIntBetween(1, 1), // realms randomIntBetween(0, 1), // authz randomIntBetween(1, JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS.size()), // algorithms @@ -122,8 +120,15 @@ protected SecureString randomJwt(JwtIssuerAndRealm jwtIssuerAndRealm, User user) otherClaims.put(fallbackSub, randomValueOtherThan(subClaimValue, () -> randomAlphaOfLength(15))); } } - // TODO: fallback aud List audClaimValue = JwtRealmInspector.getAllowedAudiences(jwtIssuerAndRealm.realm()); + if (fallbackAud != null) { + if (randomBoolean()) { + otherClaims.put(fallbackAud, audClaimValue); + audClaimValue = null; + } else { + otherClaims.put(fallbackAud, randomValueOtherThanMany(audClaimValue::contains, () -> randomAlphaOfLength(15))); + } + } // A bogus auth_time but access_token type does not check it if (randomBoolean()) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateTests.java index ad898fa3fc51..1e058f279d27 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmAuthenticateTests.java @@ -23,6 +23,7 @@ import org.elasticsearch.common.settings.SettingsException; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.authc.Realm; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings; @@ -49,7 +50,6 @@ public class JwtRealmAuthenticateTests extends JwtRealmTestCase { */ public void testJwtAuthcRealmAuthcAuthzWithEmptyRoles() throws Exception { this.jwtIssuerAndRealms = this.generateJwtIssuerRealmPairs( - this.createJwtRealmsSettingsBuilder(), randomIntBetween(1, 1), // realmsRange randomIntBetween(0, 1), // authzRange randomIntBetween(1, JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS.size()), // algsRange @@ -73,7 +73,6 @@ public void testJwtAuthcRealmAuthcAuthzWithEmptyRoles() throws Exception { */ public void testJwtAuthcRealmAuthcAuthzWithoutAuthzRealms() throws Exception { this.jwtIssuerAndRealms = this.generateJwtIssuerRealmPairs( - this.createJwtRealmsSettingsBuilder(), randomIntBetween(1, 3), // realmsRange randomIntBetween(0, 0), // authzRange randomIntBetween(1, JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS.size()), // algsRange @@ -99,7 +98,6 @@ public void testJwtAuthcRealmAuthcAuthzWithoutAuthzRealms() throws Exception { */ public void testJwkSetUpdates() throws Exception { this.jwtIssuerAndRealms = this.generateJwtIssuerRealmPairs( - this.createJwtRealmsSettingsBuilder(), randomIntBetween(1, 3), // realmsRange randomIntBetween(0, 0), // authzRange randomIntBetween(1, JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS.size()), // algsRange @@ -255,7 +253,6 @@ public void testJwkSetUpdates() throws Exception { */ public void testJwtAuthcRealmAuthcAuthzWithAuthzRealms() throws Exception { this.jwtIssuerAndRealms = this.generateJwtIssuerRealmPairs( - this.createJwtRealmsSettingsBuilder(), randomIntBetween(1, 3), // realmsRange randomIntBetween(1, 3), // authzRange randomIntBetween(1, JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS.size()), // algsRange @@ -284,11 +281,7 @@ public void testJwtAuthcRealmAuthcAuthzWithAuthzRealms() throws Exception { final User otherUser = new User(otherUsername); final SecureString otherJwt = this.randomJwt(jwtIssuerAndRealm, otherUser); - final JwtAuthenticationToken otherToken = new JwtAuthenticationToken( - List.of(JwtRealmInspector.getPrincipalClaimName(jwtIssuerAndRealm.realm())), - otherJwt, - clientSecret - ); + final AuthenticationToken otherToken = jwtIssuerAndRealm.realm().token(createThreadContext(otherJwt, clientSecret)); final PlainActionFuture> otherFuture = new PlainActionFuture<>(); jwtIssuerAndRealm.realm().authenticate(otherToken, otherFuture); final AuthenticationResult otherResult = otherFuture.actionGet(); @@ -306,12 +299,9 @@ public void testJwtAuthcRealmAuthcAuthzWithAuthzRealms() throws Exception { * @throws Exception Unexpected test failure */ public void testPkcJwkSetUrlNotFound() throws Exception { - final JwtRealmsService jwtRealmsService = this.generateJwtRealmsService(this.createJwtRealmsSettingsBuilder()); - final String principalClaimName = randomFrom(jwtRealmsService.getPrincipalClaimNames()); - final List allRealms = new ArrayList<>(); // authc and authz realms final boolean createHttpsServer = true; // force issuer to create HTTPS server for its PKC JWKSet - final JwtIssuer jwtIssuer = this.createJwtIssuer(0, principalClaimName, 12, 1, 1, 1, createHttpsServer); + final JwtIssuer jwtIssuer = this.createJwtIssuer(0, 12, 1, 1, 1, createHttpsServer); assertThat(jwtIssuer.httpsServer, is(notNullValue())); try { final JwtRealmSettingsBuilder jwtRealmSettingsBuilder = this.createJwtRealmSettingsBuilder(jwtIssuer, 0, 0); @@ -320,7 +310,7 @@ public void testPkcJwkSetUrlNotFound() throws Exception { jwtRealmSettingsBuilder.settingsBuilder().put(configKey, configValue); final Exception exception = expectThrows( SettingsException.class, - () -> this.createJwtRealm(allRealms, jwtRealmsService, jwtIssuer, jwtRealmSettingsBuilder) + () -> this.createJwtRealm(allRealms, jwtIssuer, jwtRealmSettingsBuilder) ); assertThat(exception.getMessage(), equalTo("Can't get contents for setting [" + configKey + "] value [" + configValue + "].")); assertThat(exception.getCause().getMessage(), equalTo("Get [" + configValue + "] failed, status [404], reason [Not Found].")); @@ -335,7 +325,6 @@ public void testPkcJwkSetUrlNotFound() throws Exception { */ public void testJwtValidationFailures() throws Exception { this.jwtIssuerAndRealms = this.generateJwtIssuerRealmPairs( - this.createJwtRealmsSettingsBuilder(), randomIntBetween(1, 1), // realmsRange randomIntBetween(0, 0), // authzRange randomIntBetween(1, JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS.size()), // algsRange @@ -481,12 +470,9 @@ public void testJwtValidationFailures() throws Exception { * @throws Exception Unexpected test failure */ public void testSameIssuerTwoRealmsDifferentClientSecrets() throws Exception { - final JwtRealmsService jwtRealmsService = this.generateJwtRealmsService(this.createJwtRealmsSettingsBuilder()); - final String principalClaimName = randomFrom(jwtRealmsService.getPrincipalClaimNames()); - final int realmsCount = 2; final List allRealms = new ArrayList<>(realmsCount); // two identical realms for same issuer, except different client secret - final JwtIssuer jwtIssuer = this.createJwtIssuer(0, principalClaimName, 12, 1, 1, 1, false); + final JwtIssuer jwtIssuer = this.createJwtIssuer(0, 12, 1, 1, 1, false); super.printJwtIssuer(jwtIssuer); this.jwtIssuerAndRealms = new ArrayList<>(realmsCount); for (int i = 0; i < realmsCount; i++) { @@ -501,7 +487,7 @@ public void testSameIssuerTwoRealmsDifferentClientSecrets() throws Exception { String.join(",", jwtIssuer.algorithmsAll) ) .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.ALLOWED_AUDIENCES), jwtIssuer.audiencesClaimValue.get(0)) - .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.CLAIMS_PRINCIPAL.getClaim()), principalClaimName) + .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.CLAIMS_PRINCIPAL.getClaim()), jwtIssuer.principalClaimName) .put( RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.CLIENT_AUTHENTICATION_TYPE), JwtRealmSettings.ClientAuthenticationType.SHARED_SECRET.value() @@ -532,7 +518,7 @@ public void testSameIssuerTwoRealmsDifferentClientSecrets() throws Exception { ); authcSettings.setSecureSettings(secureSettings); final JwtRealmSettingsBuilder jwtRealmSettingsBuilder = new JwtRealmSettingsBuilder(realmName, authcSettings); - final JwtRealm jwtRealm = this.createJwtRealm(allRealms, jwtRealmsService, jwtIssuer, jwtRealmSettingsBuilder); + final JwtRealm jwtRealm = this.createJwtRealm(allRealms, jwtIssuer, jwtRealmSettingsBuilder); jwtRealm.initialize(allRealms, super.licenseState); final JwtIssuerAndRealm jwtIssuerAndRealm = new JwtIssuerAndRealm(jwtIssuer, jwtRealm, jwtRealmSettingsBuilder); this.jwtIssuerAndRealms.add(jwtIssuerAndRealm); // add them so the test will clean them up diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmGenerateTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmGenerateTests.java index 7907d34301b2..8477be441375 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmGenerateTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmGenerateTests.java @@ -25,7 +25,6 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings; -import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmsServiceSettings; import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings; import org.elasticsearch.xpack.core.security.authc.support.UserRoleMapper; import org.elasticsearch.xpack.core.security.user.User; @@ -67,16 +66,11 @@ public void testCreateJwtSmokeTestRealm() throws Exception { ); final String principalClaimName = "sub"; - final Settings.Builder jwtRealmsServiceSettings = Settings.builder() - .put(this.globalSettings) - .put(JwtRealmsServiceSettings.PRINCIPAL_CLAIMS_SETTING.getKey(), String.join(",", principalClaimName)); - final JwtRealmsService jwtRealmsService = new JwtRealmsService(jwtRealmsServiceSettings.build()); // Create issuer final JwtIssuer jwtIssuer = new JwtIssuer( "iss8", // iss List.of("aud8"), // aud - principalClaimName, // sub Collections.singletonMap("security_test_user", new User("security_test_user", "security_test_role")), // users false // createHttpsServer ); @@ -115,7 +109,7 @@ public void testCreateJwtSmokeTestRealm() throws Exception { final RealmConfig config = super.buildRealmConfig(JwtRealmSettings.TYPE, realmName, configBuilder.build(), 8); final SSLService sslService = new SSLService(TestEnvironment.newEnvironment(configBuilder.build())); final UserRoleMapper userRoleMapper = super.buildRoleMapper(jwtIssuer.principals); - final JwtRealm jwtRealm = new JwtRealm(config, jwtRealmsService, sslService, userRoleMapper); + final JwtRealm jwtRealm = new JwtRealm(config, sslService, userRoleMapper); jwtRealm.initialize(Collections.singletonList(jwtRealm), super.licenseState); final JwtRealmSettingsBuilder jwtRealmSettingsBuilder = new JwtRealmSettingsBuilder(realmName, configBuilder); final JwtIssuerAndRealm jwtIssuerAndRealm = new JwtIssuerAndRealm(jwtIssuer, jwtRealm, jwtRealmSettingsBuilder); @@ -161,16 +155,11 @@ public void testCreateJwtIntegrationTestRealm1() throws Exception { final JwtIssuer.AlgJwkPair algJwkPairPkc = new JwtIssuer.AlgJwkPair("RS256", jwk); final String principalClaimName = "sub"; - final Settings.Builder jwtRealmsServiceSettings = Settings.builder() - .put(this.globalSettings) - .put(JwtRealmsServiceSettings.PRINCIPAL_CLAIMS_SETTING.getKey(), String.join(",", principalClaimName)); - final JwtRealmsService jwtRealmsService = new JwtRealmsService(jwtRealmsServiceSettings.build()); // Create issuer final JwtIssuer jwtIssuer = new JwtIssuer( "https://issuer.example.com/", // iss claim value List.of("https://audience.example.com/"), // aud claim value - principalClaimName, // principal claim name Collections.singletonMap("user1", new User("user1", "role1")), // users false // createHttpsServer ); @@ -207,7 +196,7 @@ public void testCreateJwtIntegrationTestRealm1() throws Exception { final RealmConfig config = super.buildRealmConfig(JwtRealmSettings.TYPE, realmName, configBuilder.build(), 2); final SSLService sslService = new SSLService(TestEnvironment.newEnvironment(configBuilder.build())); final UserRoleMapper userRoleMapper = super.buildRoleMapper(jwtIssuer.principals); - final JwtRealm jwtRealm = new JwtRealm(config, jwtRealmsService, sslService, userRoleMapper); + final JwtRealm jwtRealm = new JwtRealm(config, sslService, userRoleMapper); jwtRealm.initialize(Collections.singletonList(jwtRealm), super.licenseState); final JwtRealmSettingsBuilder jwtRealmSettingsBuilder = new JwtRealmSettingsBuilder(realmName, configBuilder); final JwtIssuerAndRealm jwtIssuerAndRealm = new JwtIssuerAndRealm(jwtIssuer, jwtRealm, jwtRealmSettingsBuilder); @@ -256,16 +245,11 @@ public void testCreateJwtIntegrationTestRealm2() throws Exception { ); final String principalClaimName = "email"; - final Settings.Builder jwtRealmsServiceSettings = Settings.builder() - .put(this.globalSettings) - .put(JwtRealmsServiceSettings.PRINCIPAL_CLAIMS_SETTING.getKey(), String.join(",", principalClaimName)); - final JwtRealmsService jwtRealmsService = new JwtRealmsService(jwtRealmsServiceSettings.build()); // Create issuer final JwtIssuer jwtIssuer = new JwtIssuer( "my-issuer", // iss claim value List.of("es01", "es02", "es03"), // aud claim value - principalClaimName, // principal claim name Collections.singletonMap("user2", new User("user2", "role2")), // users false // createHttpsServer ); @@ -282,7 +266,7 @@ public void testCreateJwtIntegrationTestRealm2() throws Exception { String.join(",", jwtIssuer.audiencesClaimValue) ) .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.ALLOWED_SIGNATURE_ALGORITHMS), "HS256,HS384") - .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.CLAIMS_PRINCIPAL.getClaim()), "email") + .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.CLAIMS_PRINCIPAL.getClaim()), principalClaimName) .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.CLAIMS_PRINCIPAL.getPattern()), "^(.*)@[^.]*[.]example[.]com$") .put(RealmSettings.getFullSettingKey(realmName, JwtRealmSettings.CLAIMS_MAIL.getClaim()), "email") .put( @@ -313,7 +297,7 @@ public void testCreateJwtIntegrationTestRealm2() throws Exception { final RealmConfig config = super.buildRealmConfig(JwtRealmSettings.TYPE, realmName, configBuilder.build(), 3); final SSLService sslService = new SSLService(TestEnvironment.newEnvironment(configBuilder.build())); final UserRoleMapper userRoleMapper = super.buildRoleMapper(Map.of()); // authc realm will not do role mapping - final JwtRealm jwtRealm = new JwtRealm(config, jwtRealmsService, sslService, userRoleMapper); + final JwtRealm jwtRealm = new JwtRealm(config, sslService, userRoleMapper); jwtRealm.initialize(List.of(authzRealm, jwtRealm), super.licenseState); final JwtRealmSettingsBuilder jwtRealmSettingsBuilder = new JwtRealmSettingsBuilder(realmName, configBuilder); final JwtIssuerAndRealm jwtIssuerAndRealm = new JwtIssuerAndRealm(jwtIssuer, jwtRealm, jwtRealmSettingsBuilder); @@ -361,16 +345,11 @@ public void testCreateJwtIntegrationTestRealm3() throws Exception { ); final String principalClaimName = "sub"; - final Settings.Builder jwtRealmsServiceSettings = Settings.builder() - .put(this.globalSettings) - .put(JwtRealmsServiceSettings.PRINCIPAL_CLAIMS_SETTING.getKey(), String.join(",", principalClaimName)); - final JwtRealmsService jwtRealmsService = new JwtRealmsService(jwtRealmsServiceSettings.build()); // Create issuer final JwtIssuer jwtIssuer = new JwtIssuer( "jwt3-issuer", // iss claim value List.of("jwt3-audience"), // aud claim value - principalClaimName, // principal claim name Collections.singletonMap("user3", new User("user3", "role3")), // users false // createHttpsServer ); @@ -409,7 +388,7 @@ public void testCreateJwtIntegrationTestRealm3() throws Exception { final RealmConfig config = super.buildRealmConfig(JwtRealmSettings.TYPE, realmName, configBuilder.build(), 4); final SSLService sslService = new SSLService(TestEnvironment.newEnvironment(configBuilder.build())); final UserRoleMapper userRoleMapper = super.buildRoleMapper(jwtIssuer.principals); - final JwtRealm jwtRealm = new JwtRealm(config, jwtRealmsService, sslService, userRoleMapper); + final JwtRealm jwtRealm = new JwtRealm(config, sslService, userRoleMapper); jwtRealm.initialize(List.of(jwtRealm), super.licenseState); final JwtRealmSettingsBuilder jwtRealmSettingsBuilder = new JwtRealmSettingsBuilder(realmName, configBuilder); final JwtIssuerAndRealm jwtIssuerAndRealm = new JwtIssuerAndRealm(jwtIssuer, jwtRealm, jwtRealmSettingsBuilder); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmTestCase.java index 4f7e7b57d9b9..0d64a9761cdb 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/jwt/JwtRealmTestCase.java @@ -31,7 +31,6 @@ import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings; import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings.ClientAuthenticationType; -import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmsServiceSettings; import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings; import org.elasticsearch.xpack.core.security.authc.support.UserRoleMapper; import org.elasticsearch.xpack.core.security.user.User; @@ -73,8 +72,6 @@ public abstract class JwtRealmTestCase extends JwtTestCase { private static final Logger LOGGER = LogManager.getLogger(JwtRealmTestCase.class); - record JwtRealmsServiceSettingsBuilder(Settings.Builder settingsBuilder) {} - record JwtRealmSettingsBuilder(String name, Settings.Builder settingsBuilder) {} record JwtIssuerAndRealm(JwtIssuer issuer, JwtRealm realm, JwtRealmSettingsBuilder realmSettingsBuilder) {} @@ -117,12 +114,7 @@ protected void verifyAuthenticateFailureHelper( assertThat(plainActionFuture.get().isAuthenticated(), is(false)); } - protected JwtRealmsService generateJwtRealmsService(final JwtRealmsServiceSettingsBuilder jwtRealmRealmsSettingsBuilder) { - return new JwtRealmsService(jwtRealmRealmsSettingsBuilder.settingsBuilder.build()); - } - protected List generateJwtIssuerRealmPairs( - final JwtRealmsServiceSettingsBuilder jwtRealmsServiceSettingsBuilder, final int realmsCount, final int authzCount, final int algsCount, @@ -133,20 +125,11 @@ protected List generateJwtIssuerRealmPairs( final boolean createHttpsServer ) throws Exception { // Create JWT authc realms and mocked authz realms. Initialize each JWT realm, and test ensureInitialized() before and after. - final JwtRealmsService jwtRealmsService = this.generateJwtRealmsService(jwtRealmsServiceSettingsBuilder); final List allRealms = new ArrayList<>(); // authc and authz realms this.jwtIssuerAndRealms = new ArrayList<>(realmsCount); for (int i = 0; i < realmsCount; i++) { - final JwtIssuer jwtIssuer = this.createJwtIssuer( - i, - randomFrom(jwtRealmsService.getPrincipalClaimNames()), - algsCount, - audiencesCount, - usersCount, - rolesCount, - createHttpsServer - ); + final JwtIssuer jwtIssuer = this.createJwtIssuer(i, algsCount, audiencesCount, usersCount, rolesCount, createHttpsServer); // If HTTPS server was created in JWT issuer, any exception after that point requires closing it to avoid a thread pool leak try { final JwtRealmSettingsBuilder realmSettingsBuilder = this.createJwtRealmSettingsBuilder( @@ -154,7 +137,7 @@ protected List generateJwtIssuerRealmPairs( authzCount, jwtCacheSize ); - final JwtRealm jwtRealm = this.createJwtRealm(allRealms, jwtRealmsService, jwtIssuer, realmSettingsBuilder); + final JwtRealm jwtRealm = this.createJwtRealm(allRealms, jwtIssuer, realmSettingsBuilder); // verify exception before initialize() final Exception exception = expectThrows(IllegalStateException.class, jwtRealm::ensureInitialized); @@ -174,7 +157,6 @@ protected List generateJwtIssuerRealmPairs( protected JwtIssuer createJwtIssuer( final int i, - final String principalClaimName, final int algsCount, final int audiencesCount, final int userCount, @@ -185,7 +167,7 @@ protected JwtIssuer createJwtIssuer( final List audiences = IntStream.range(0, audiencesCount).mapToObj(j -> issuer + "_aud" + (j + 1)).toList(); final Map users = JwtTestCase.generateTestUsersWithRoles(userCount, roleCount); // Allow algorithm repeats, to cover testing of multiple JWKs for same algorithm - final JwtIssuer jwtIssuer = new JwtIssuer(issuer, audiences, principalClaimName, users, createHttpsServer); + final JwtIssuer jwtIssuer = new JwtIssuer(issuer, audiences, users, createHttpsServer); final List algorithms = randomOfMinMaxNonUnique(algsCount, algsCount, JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS); final boolean areHmacJwksOidcSafe = randomBoolean(); final List algAndJwks = JwtRealmTestCase.randomJwks(algorithms, areHmacJwksOidcSafe); @@ -204,21 +186,6 @@ protected void copyIssuerJwksToRealmConfig(final JwtIssuerAndRealm jwtIssuerAndR // TODO If x-pack Security plug-in add supports for reloadable settings, update HMAC JWKSet and HMAC OIDC JWK in ES Keystore } - protected JwtRealmsServiceSettingsBuilder createJwtRealmsSettingsBuilder() throws Exception { - final List principalClaimNames = randomBoolean() - ? List.of("principalClaim_" + randomAlphaOfLength(6)) - : randomSubsetOf(randomIntBetween(1, 6), JwtRealmsServiceSettings.DEFAULT_PRINCIPAL_CLAIMS); - - final Settings.Builder jwtRealmsServiceSettings = Settings.builder() - .put(this.globalSettings) - .put(JwtRealmsServiceSettings.PRINCIPAL_CLAIMS_SETTING.getKey(), String.join(",", principalClaimNames)); - - final MockSecureSettings secureSettings = new MockSecureSettings(); // none for now, placeholder for future - jwtRealmsServiceSettings.setSecureSettings(secureSettings); - - return new JwtRealmsServiceSettingsBuilder(jwtRealmsServiceSettings); - } - protected JwtRealmSettingsBuilder createJwtRealmSettingsBuilder(final JwtIssuer jwtIssuer, final int authzCount, final int jwtCacheSize) throws Exception { final String authcRealmName = "realm_" + jwtIssuer.issuerClaimValue; @@ -367,7 +334,6 @@ protected JwtRealmSettingsBuilder createJwtRealmSettingsBuilder(final JwtIssuer protected JwtRealm createJwtRealm( final List allRealms, // JWT realms and authz realms - final JwtRealmsService jwtRealmsService, final JwtIssuer jwtIssuer, final JwtRealmSettingsBuilder realmSettingsBuilder ) { @@ -381,7 +347,7 @@ protected JwtRealm createJwtRealm( final UserRoleMapper userRoleMapper = super.buildRoleMapper(authzRealmNames.isEmpty() ? jwtIssuer.principals : Map.of()); // If authz names is not set, register the users here in the JWT authc realm. - final JwtRealm jwtRealm = new JwtRealm(authcConfig, jwtRealmsService, sslService, userRoleMapper); + final JwtRealm jwtRealm = new JwtRealm(authcConfig, sslService, userRoleMapper); allRealms.add(jwtRealm); // If authz names is set, register the users here in one of the authz realms. @@ -446,20 +412,15 @@ protected void doMultipleAuthcAuthzAndVerifySuccess( } assertThat(jwtAuthenticationToken, is(notNullValue())); final String tokenPrincipal = jwtAuthenticationToken.principal(); - final SecureString tokenJwt = jwtAuthenticationToken.getEndUserSignedJwt(); final SecureString tokenSecret = jwtAuthenticationToken.getClientAuthenticationSharedSecret(); assertThat(tokenPrincipal, is(notNullValue())); - if (tokenJwt.equals(jwt) == false) { - assertThat(tokenJwt, is(equalTo(jwt))); - } - assertThat(tokenJwt, is(equalTo(jwt))); if (tokenSecret != null) { if (tokenSecret.equals(sharedSecret) == false) { assertThat(tokenSecret, is(equalTo(sharedSecret))); } assertThat(tokenSecret, is(equalTo(sharedSecret))); } - LOGGER.info("GOT TOKEN: principal=[" + tokenPrincipal + "], jwt=[" + tokenJwt + "], secret=[" + tokenSecret + "]."); + LOGGER.info("GOT TOKEN: principal=[" + tokenPrincipal + "], jwt=[" + jwt + "], secret=[" + tokenSecret + "]."); // Loop through all authc/authz realms. Confirm user is returned with expected principal and roles. User authenticatedUser = null; From b11cbd43ddb6325323f7b32cb35018aa732cb573 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Thu, 22 Dec 2022 09:15:05 +0100 Subject: [PATCH 340/919] Move matrix stats to aggregations module (#92435) Running the `matrix_stats_multi_value_field.yaml` test in multi node test cluster showed a bug, see: 88758ab57783ca2ccfe0ca1f7ed4c0af200ff783 Also removes `MatrixStats` interface, removed usage of deprecated ValueType enum and removed unused generic usage. Relates to #90283 --- client/rest-high-level/build.gradle | 1 - .../src/main/java/module-info.java | 5 +++ .../aggregations/AggregationsPlugin.java | 7 ++++ .../metric}/ArrayValuesSource.java | 2 +- .../ArrayValuesSourceAggregationBuilder.java | 2 +- .../ArrayValuesSourceAggregatorFactory.java | 2 +- .../metric}/ArrayValuesSourceParser.java | 39 +++++++----------- .../metric}/InternalMatrixStats.java | 28 +++++-------- .../MatrixAggregationInspectionHelper.java | 2 +- .../MatrixStatsAggregationBuilder.java | 12 ++++-- .../metric}/MatrixStatsAggregator.java | 7 ++-- .../metric}/MatrixStatsAggregatorFactory.java | 3 +- .../MatrixStatsNamedXContentProvider.java | 4 +- .../metric}/MatrixStatsParser.java | 15 +++---- .../metric}/MatrixStatsResults.java | 2 +- .../metric}/ParsedMatrixStats.java | 12 +----- .../aggregations/metric}/RunningStats.java | 2 +- ...icsearch.plugins.spi.NamedXContentProvider | 9 +++++ .../metric}/BaseMatrixStatsTestCase.java | 2 +- .../metric}/InternalMatrixStatsTests.java | 40 +++++++++---------- .../metric}/MatrixStatsAggregatorTests.java | 14 ++----- .../aggregations/metric}/MultiPassStats.java | 2 +- .../metric}/RunningStatsTests.java | 2 +- .../test/aggregations/matrix_stats.yml} | 0 .../matrix_stats_multi_value_field.yml} | 3 ++ .../matrix_stats_single_value_field.yml} | 0 modules/aggs-matrix-stats/build.gradle | 20 ---------- .../src/main/java/module-info.java | 21 ---------- .../matrix/MatrixAggregationPlugin.java | 29 -------------- .../MatrixStatsAggregationBuilders.java | 20 ---------- .../matrix/stats/MatrixStats.java | 39 ------------------ ...icsearch.plugins.spi.NamedXContentProvider | 1 - .../MatrixStatsClientYamlTestSuiteIT.java | 25 ------------ .../rest-api-spec/test/stats/10_basic.yml | 16 -------- x-pack/plugin/sql/build.gradle | 1 - .../search/extractor/MetricAggExtractor.java | 4 +- .../sql/querydsl/agg/MatrixStatsAgg.java | 5 +-- x-pack/plugin/transform/build.gradle | 1 - .../pivot/TransformAggregationsTests.java | 4 +- x-pack/qa/runtime-fields/build.gradle | 7 ++++ 40 files changed, 111 insertions(+), 299 deletions(-) rename modules/{aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix => aggregations/src/main/java/org/elasticsearch/aggregations/metric}/ArrayValuesSource.java (98%) rename modules/{aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix => aggregations/src/main/java/org/elasticsearch/aggregations/metric}/ArrayValuesSourceAggregationBuilder.java (99%) rename modules/{aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix => aggregations/src/main/java/org/elasticsearch/aggregations/metric}/ArrayValuesSourceAggregatorFactory.java (98%) rename modules/{aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix => aggregations/src/main/java/org/elasticsearch/aggregations/metric}/ArrayValuesSourceParser.java (89%) rename modules/{aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats => aggregations/src/main/java/org/elasticsearch/aggregations/metric}/InternalMatrixStats.java (93%) rename modules/{aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats => aggregations/src/main/java/org/elasticsearch/aggregations/metric}/MatrixAggregationInspectionHelper.java (91%) rename modules/{aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats => aggregations/src/main/java/org/elasticsearch/aggregations/metric}/MatrixStatsAggregationBuilder.java (89%) rename modules/{aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats => aggregations/src/main/java/org/elasticsearch/aggregations/metric}/MatrixStatsAggregator.java (94%) rename modules/{aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats => aggregations/src/main/java/org/elasticsearch/aggregations/metric}/MatrixStatsAggregatorFactory.java (95%) rename modules/{aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/spi => aggregations/src/main/java/org/elasticsearch/aggregations/metric}/MatrixStatsNamedXContentProvider.java (84%) rename modules/{aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats => aggregations/src/main/java/org/elasticsearch/aggregations/metric}/MatrixStatsParser.java (68%) rename modules/{aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats => aggregations/src/main/java/org/elasticsearch/aggregations/metric}/MatrixStatsResults.java (99%) rename modules/{aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats => aggregations/src/main/java/org/elasticsearch/aggregations/metric}/ParsedMatrixStats.java (97%) rename modules/{aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats => aggregations/src/main/java/org/elasticsearch/aggregations/metric}/RunningStats.java (99%) create mode 100644 modules/aggregations/src/main/resources/META-INF/services/org.elasticsearch.plugins.spi.NamedXContentProvider rename modules/{aggs-matrix-stats/src/test/java/org/elasticsearch/search/aggregations/matrix/stats => aggregations/src/test/java/org/elasticsearch/aggregations/metric}/BaseMatrixStatsTestCase.java (95%) rename modules/{aggs-matrix-stats/src/test/java/org/elasticsearch/search/aggregations/matrix/stats => aggregations/src/test/java/org/elasticsearch/aggregations/metric}/InternalMatrixStatsTests.java (83%) rename modules/{aggs-matrix-stats/src/test/java/org/elasticsearch/search/aggregations/matrix/stats => aggregations/src/test/java/org/elasticsearch/aggregations/metric}/MatrixStatsAggregatorTests.java (91%) rename modules/{aggs-matrix-stats/src/test/java/org/elasticsearch/search/aggregations/matrix/stats => aggregations/src/test/java/org/elasticsearch/aggregations/metric}/MultiPassStats.java (99%) rename modules/{aggs-matrix-stats/src/test/java/org/elasticsearch/search/aggregations/matrix/stats => aggregations/src/test/java/org/elasticsearch/aggregations/metric}/RunningStatsTests.java (98%) rename modules/{aggs-matrix-stats/src/yamlRestTest/resources/rest-api-spec/test/stats/20_empty_bucket.yml => aggregations/src/yamlRestTest/resources/rest-api-spec/test/aggregations/matrix_stats.yml} (100%) rename modules/{aggs-matrix-stats/src/yamlRestTest/resources/rest-api-spec/test/stats/40_multi_value_field.yml => aggregations/src/yamlRestTest/resources/rest-api-spec/test/aggregations/matrix_stats_multi_value_field.yml} (98%) rename modules/{aggs-matrix-stats/src/yamlRestTest/resources/rest-api-spec/test/stats/30_single_value_field.yml => aggregations/src/yamlRestTest/resources/rest-api-spec/test/aggregations/matrix_stats_single_value_field.yml} (100%) delete mode 100644 modules/aggs-matrix-stats/build.gradle delete mode 100644 modules/aggs-matrix-stats/src/main/java/module-info.java delete mode 100644 modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/MatrixAggregationPlugin.java delete mode 100644 modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/MatrixStatsAggregationBuilders.java delete mode 100644 modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStats.java delete mode 100644 modules/aggs-matrix-stats/src/main/resources/META-INF/services/org.elasticsearch.plugins.spi.NamedXContentProvider delete mode 100644 modules/aggs-matrix-stats/src/yamlRestTest/java/org/elasticsearch/search/aggregations/matrix/MatrixStatsClientYamlTestSuiteIT.java delete mode 100644 modules/aggs-matrix-stats/src/yamlRestTest/resources/rest-api-spec/test/stats/10_basic.yml diff --git a/client/rest-high-level/build.gradle b/client/rest-high-level/build.gradle index be2be6f0461e..500870fb593c 100644 --- a/client/rest-high-level/build.gradle +++ b/client/rest-high-level/build.gradle @@ -25,7 +25,6 @@ restResources { dependencies { api project(':modules:mapper-extras') api project(':modules:parent-join') - api project(':modules:aggs-matrix-stats') api project(':modules:rank-eval') api project(':modules:lang-mustache') api project(':modules:aggregations') diff --git a/modules/aggregations/src/main/java/module-info.java b/modules/aggregations/src/main/java/module-info.java index 3e378d108255..b9f2cb834736 100644 --- a/modules/aggregations/src/main/java/module-info.java +++ b/modules/aggregations/src/main/java/module-info.java @@ -17,8 +17,13 @@ exports org.elasticsearch.aggregations.bucket.adjacency; exports org.elasticsearch.aggregations.bucket.timeseries; exports org.elasticsearch.aggregations.pipeline; + exports org.elasticsearch.aggregations.metric; opens org.elasticsearch.aggregations to org.elasticsearch.painless.spi; // whitelist resource access provides org.elasticsearch.painless.spi.PainlessExtension with org.elasticsearch.aggregations.AggregationsPainlessExtension; + + provides org.elasticsearch.plugins.spi.NamedXContentProvider + with + org.elasticsearch.aggregations.metric.MatrixStatsNamedXContentProvider; } diff --git a/modules/aggregations/src/main/java/org/elasticsearch/aggregations/AggregationsPlugin.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/AggregationsPlugin.java index c46f4b444dcf..8bf4785b96b4 100644 --- a/modules/aggregations/src/main/java/org/elasticsearch/aggregations/AggregationsPlugin.java +++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/AggregationsPlugin.java @@ -14,6 +14,9 @@ import org.elasticsearch.aggregations.bucket.histogram.InternalAutoDateHistogram; import org.elasticsearch.aggregations.bucket.timeseries.InternalTimeSeries; import org.elasticsearch.aggregations.bucket.timeseries.TimeSeriesAggregationBuilder; +import org.elasticsearch.aggregations.metric.InternalMatrixStats; +import org.elasticsearch.aggregations.metric.MatrixStatsAggregationBuilder; +import org.elasticsearch.aggregations.metric.MatrixStatsParser; import org.elasticsearch.aggregations.pipeline.BucketSelectorPipelineAggregationBuilder; import org.elasticsearch.aggregations.pipeline.BucketSortPipelineAggregationBuilder; import org.elasticsearch.aggregations.pipeline.Derivative; @@ -48,6 +51,10 @@ public List getAggregations() { ).addResultReader(InternalAutoDateHistogram::new) .setAggregatorRegistrar(AutoDateHistogramAggregationBuilder::registerAggregators) ); + specs.add( + new AggregationSpec(MatrixStatsAggregationBuilder.NAME, MatrixStatsAggregationBuilder::new, new MatrixStatsParser()) + .addResultReader(InternalMatrixStats::new) + ); if (IndexSettings.isTimeSeriesModeEnabled()) { specs.add( new AggregationSpec( diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/ArrayValuesSource.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/ArrayValuesSource.java similarity index 98% rename from modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/ArrayValuesSource.java rename to modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/ArrayValuesSource.java index d853c06ea3e7..9a46d7120501 100644 --- a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/ArrayValuesSource.java +++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/ArrayValuesSource.java @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -package org.elasticsearch.search.aggregations.matrix; +package org.elasticsearch.aggregations.metric; import org.apache.lucene.index.LeafReaderContext; import org.elasticsearch.index.fielddata.NumericDoubleValues; diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/ArrayValuesSourceAggregationBuilder.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/ArrayValuesSourceAggregationBuilder.java similarity index 99% rename from modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/ArrayValuesSourceAggregationBuilder.java rename to modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/ArrayValuesSourceAggregationBuilder.java index 03094ad8690e..b78707bbb1f8 100644 --- a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/ArrayValuesSourceAggregationBuilder.java +++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/ArrayValuesSourceAggregationBuilder.java @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -package org.elasticsearch.search.aggregations.matrix; +package org.elasticsearch.aggregations.metric; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/ArrayValuesSourceAggregatorFactory.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/ArrayValuesSourceAggregatorFactory.java similarity index 98% rename from modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/ArrayValuesSourceAggregatorFactory.java rename to modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/ArrayValuesSourceAggregatorFactory.java index 9249c31e156a..26e2110ecbb8 100644 --- a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/ArrayValuesSourceAggregatorFactory.java +++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/ArrayValuesSourceAggregatorFactory.java @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -package org.elasticsearch.search.aggregations.matrix; +package org.elasticsearch.aggregations.metric; import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorFactories; diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/ArrayValuesSourceParser.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/ArrayValuesSourceParser.java similarity index 89% rename from modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/ArrayValuesSourceParser.java rename to modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/ArrayValuesSourceParser.java index 68daf3e8dbb6..eab1c2482028 100644 --- a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/ArrayValuesSourceParser.java +++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/ArrayValuesSourceParser.java @@ -6,14 +6,13 @@ * Side Public License, v 1. */ -package org.elasticsearch.search.aggregations.matrix; +package org.elasticsearch.aggregations.metric; import org.elasticsearch.common.ParsingException; import org.elasticsearch.script.Script; import org.elasticsearch.search.aggregations.AggregationBuilder.CommonFields; import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.support.CoreValuesSourceType; -import org.elasticsearch.search.aggregations.support.ValueType; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregationBuilder; import org.elasticsearch.search.aggregations.support.ValuesSourceType; @@ -27,36 +26,34 @@ import java.util.List; import java.util.Map; -public abstract class ArrayValuesSourceParser implements Aggregator.Parser { +public abstract class ArrayValuesSourceParser implements Aggregator.Parser { - public abstract static class NumericValuesSourceParser extends ArrayValuesSourceParser { + public abstract static class NumericValuesSourceParser extends ArrayValuesSourceParser { protected NumericValuesSourceParser(boolean formattable) { - super(formattable, CoreValuesSourceType.NUMERIC, ValueType.NUMERIC); + super(formattable, CoreValuesSourceType.NUMERIC); } } - public abstract static class BytesValuesSourceParser extends ArrayValuesSourceParser { + public abstract static class BytesValuesSourceParser extends ArrayValuesSourceParser { protected BytesValuesSourceParser(boolean formattable) { - super(formattable, CoreValuesSourceType.KEYWORD, ValueType.STRING); + super(formattable, CoreValuesSourceType.KEYWORD); } } - public abstract static class GeoPointValuesSourceParser extends ArrayValuesSourceParser { + public abstract static class GeoPointValuesSourceParser extends ArrayValuesSourceParser { protected GeoPointValuesSourceParser(boolean formattable) { - super(formattable, CoreValuesSourceType.GEOPOINT, ValueType.GEOPOINT); + super(formattable, CoreValuesSourceType.GEOPOINT); } } - private boolean formattable = false; - private ValuesSourceType valuesSourceType = null; - private ValueType targetValueType = null; + private final boolean formattable; + private final ValuesSourceType valuesSourceType; - private ArrayValuesSourceParser(boolean formattable, ValuesSourceType valuesSourceType, ValueType targetValueType) { + private ArrayValuesSourceParser(boolean formattable, ValuesSourceType valuesSourceType) { this.valuesSourceType = valuesSourceType; - this.targetValueType = targetValueType; this.formattable = formattable; } @@ -159,12 +156,7 @@ public final ArrayValuesSourceAggregationBuilder parse(String aggregationName } } - ArrayValuesSourceAggregationBuilder factory = createFactory( - aggregationName, - this.valuesSourceType, - this.targetValueType, - otherOptions - ); + ArrayValuesSourceAggregationBuilder factory = createFactory(aggregationName, this.valuesSourceType, otherOptions); if (fields != null) { factory.fields(fields); } @@ -216,8 +208,6 @@ private void parseMissingAndAdd( * the name of the aggregation * @param valuesSourceType * the type of the {@link ValuesSource} - * @param targetValueType - * the target type of the final value output by the aggregation * @param otherOptions * a {@link Map} containing the extra options parsed by the * {@link #token(String, String, XContentParser.Token, XContentParser, Map)} @@ -227,14 +217,13 @@ private void parseMissingAndAdd( protected abstract ArrayValuesSourceAggregationBuilder createFactory( String aggregationName, ValuesSourceType valuesSourceType, - ValueType targetValueType, Map otherOptions ); /** * Allows subclasses of {@link ArrayValuesSourceParser} to parse extra * parameters and store them in a {@link Map} which will later be passed to - * {@link #createFactory(String, ValuesSourceType, ValueType, Map)}. + * {@link #createFactory(String, ValuesSourceType, Map)}. * * @param aggregationName * the name of the aggregation @@ -247,7 +236,7 @@ protected abstract ArrayValuesSourceAggregationBuilder createFactory( * @param otherOptions * a {@link Map} of options to be populated by successive calls * to this method which will then be passed to the - * {@link #createFactory(String, ValuesSourceType, ValueType, Map)} + * {@link #createFactory(String, ValuesSourceType, Map)} * method * @return true if the current token was correctly parsed, * false otherwise diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/InternalMatrixStats.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/InternalMatrixStats.java similarity index 93% rename from modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/InternalMatrixStats.java rename to modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/InternalMatrixStats.java index f3b1db353544..c7428393b275 100644 --- a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/InternalMatrixStats.java +++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/InternalMatrixStats.java @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -package org.elasticsearch.search.aggregations.matrix.stats; +package org.elasticsearch.aggregations.metric; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -27,7 +27,7 @@ /** * Computes distribution statistics over multiple fields */ -public class InternalMatrixStats extends InternalAggregation implements MatrixStats { +public class InternalMatrixStats extends InternalAggregation { /** per shard stats needed to compute stats */ private final RunningStats stats; /** final result */ @@ -67,8 +67,7 @@ public String getWriteableName() { return MatrixStatsAggregationBuilder.NAME; } - /** get the number of documents */ - @Override + /** return the total document count */ public long getDocCount() { if (results != null) { return results.getDocCount(); @@ -79,8 +78,7 @@ public long getDocCount() { return stats.docCount; } - /** get the number of samples for the given field. == docCount - numMissing */ - @Override + /** return total field count (differs from docCount if there are missing values) */ public long getFieldCount(String field) { if (results == null) { return 0; @@ -88,8 +86,7 @@ public long getFieldCount(String field) { return results.getFieldCount(field); } - /** get the mean for the given field */ - @Override + /** return the field mean */ public double getMean(String field) { if (results == null) { return Double.NaN; @@ -97,8 +94,7 @@ public double getMean(String field) { return results.getMean(field); } - /** get the variance for the given field */ - @Override + /** return the field variance */ public double getVariance(String field) { if (results == null) { return Double.NaN; @@ -106,8 +102,7 @@ public double getVariance(String field) { return results.getVariance(field); } - /** get the distribution skewness for the given field */ - @Override + /** return the skewness of the distribution */ public double getSkewness(String field) { if (results == null) { return Double.NaN; @@ -115,8 +110,7 @@ public double getSkewness(String field) { return results.getSkewness(field); } - /** get the distribution shape for the given field */ - @Override + /** return the kurtosis of the distribution */ public double getKurtosis(String field) { if (results == null) { return Double.NaN; @@ -124,8 +118,7 @@ public double getKurtosis(String field) { return results.getKurtosis(field); } - /** get the covariance between the two fields */ - @Override + /** return the covariance between field x and field y */ public double getCovariance(String fieldX, String fieldY) { if (results == null) { return Double.NaN; @@ -133,8 +126,7 @@ public double getCovariance(String fieldX, String fieldY) { return results.getCovariance(fieldX, fieldY); } - /** get the correlation between the two fields */ - @Override + /** return the correlation coefficient of field x and field y */ public double getCorrelation(String fieldX, String fieldY) { if (results == null) { return Double.NaN; diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixAggregationInspectionHelper.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixAggregationInspectionHelper.java similarity index 91% rename from modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixAggregationInspectionHelper.java rename to modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixAggregationInspectionHelper.java index de1aff8913b3..6befc7e1c87e 100644 --- a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixAggregationInspectionHelper.java +++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixAggregationInspectionHelper.java @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -package org.elasticsearch.search.aggregations.matrix.stats; +package org.elasticsearch.aggregations.metric; /** * Counterpart to {@link org.elasticsearch.search.aggregations.support.AggregationInspectionHelper}, providing diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStatsAggregationBuilder.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixStatsAggregationBuilder.java similarity index 89% rename from modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStatsAggregationBuilder.java rename to modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixStatsAggregationBuilder.java index c4c476ff65e9..700167054839 100644 --- a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStatsAggregationBuilder.java +++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixStatsAggregationBuilder.java @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -package org.elasticsearch.search.aggregations.matrix.stats; +package org.elasticsearch.aggregations.metric; import org.elasticsearch.Version; import org.elasticsearch.common.io.stream.StreamInput; @@ -14,7 +14,6 @@ import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.aggregations.AggregatorFactory; -import org.elasticsearch.search.aggregations.matrix.ArrayValuesSourceAggregationBuilder; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; import org.elasticsearch.xcontent.ToXContent; @@ -56,11 +55,16 @@ public boolean supportsSampling() { */ public MatrixStatsAggregationBuilder(StreamInput in) throws IOException { super(in); + if (in.getVersion().onOrAfter(Version.V_8_7_0)) { + multiValueMode = MultiValueMode.readMultiValueModeFrom(in); + } } @Override - protected void innerWriteTo(StreamOutput out) { - // Do nothing, no extra state to write to stream + protected void innerWriteTo(StreamOutput out) throws IOException { + if (out.getVersion().onOrAfter(Version.V_8_7_0)) { + multiValueMode.writeTo(out); + } } public MatrixStatsAggregationBuilder multiValueMode(MultiValueMode multiValueMode) { diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStatsAggregator.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixStatsAggregator.java similarity index 94% rename from modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStatsAggregator.java rename to modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixStatsAggregator.java index 6c3d41138ebc..c6ac2818f824 100644 --- a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStatsAggregator.java +++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixStatsAggregator.java @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -package org.elasticsearch.search.aggregations.matrix.stats; +package org.elasticsearch.aggregations.metric; import org.apache.lucene.search.ScoreMode; import org.elasticsearch.common.util.ObjectArray; @@ -17,7 +17,6 @@ import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.LeafBucketCollectorBase; -import org.elasticsearch.search.aggregations.matrix.ArrayValuesSource.NumericArrayValuesSource; import org.elasticsearch.search.aggregations.metrics.MetricsAggregator; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; @@ -30,7 +29,7 @@ **/ final class MatrixStatsAggregator extends MetricsAggregator { /** Multiple ValuesSource with field names */ - private final NumericArrayValuesSource valuesSources; + private final ArrayValuesSource.NumericArrayValuesSource valuesSources; /** array of descriptive stats, per shard, needed to compute the correlation */ ObjectArray stats; @@ -45,7 +44,7 @@ final class MatrixStatsAggregator extends MetricsAggregator { ) throws IOException { super(name, context, parent, metadata); if (valuesSources != null && valuesSources.isEmpty() == false) { - this.valuesSources = new NumericArrayValuesSource(valuesSources, multiValueMode); + this.valuesSources = new ArrayValuesSource.NumericArrayValuesSource(valuesSources, multiValueMode); stats = context.bigArrays().newObjectArray(1); } else { this.valuesSources = null; diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStatsAggregatorFactory.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixStatsAggregatorFactory.java similarity index 95% rename from modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStatsAggregatorFactory.java rename to modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixStatsAggregatorFactory.java index 0e14bad7a909..2418aada6c26 100644 --- a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStatsAggregatorFactory.java +++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixStatsAggregatorFactory.java @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -package org.elasticsearch.search.aggregations.matrix.stats; +package org.elasticsearch.aggregations.metric; import org.elasticsearch.common.util.Maps; import org.elasticsearch.search.MultiValueMode; @@ -14,7 +14,6 @@ import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.aggregations.AggregatorFactory; import org.elasticsearch.search.aggregations.CardinalityUpperBound; -import org.elasticsearch.search.aggregations.matrix.ArrayValuesSourceAggregatorFactory; import org.elasticsearch.search.aggregations.support.AggregationContext; import org.elasticsearch.search.aggregations.support.ValuesSource; import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/spi/MatrixStatsNamedXContentProvider.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixStatsNamedXContentProvider.java similarity index 84% rename from modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/spi/MatrixStatsNamedXContentProvider.java rename to modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixStatsNamedXContentProvider.java index ff3e6e06e22b..66670a3f6902 100644 --- a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/spi/MatrixStatsNamedXContentProvider.java +++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixStatsNamedXContentProvider.java @@ -6,12 +6,10 @@ * Side Public License, v 1. */ -package org.elasticsearch.search.aggregations.matrix.spi; +package org.elasticsearch.aggregations.metric; import org.elasticsearch.plugins.spi.NamedXContentProvider; import org.elasticsearch.search.aggregations.Aggregation; -import org.elasticsearch.search.aggregations.matrix.stats.MatrixStatsAggregationBuilder; -import org.elasticsearch.search.aggregations.matrix.stats.ParsedMatrixStats; import org.elasticsearch.xcontent.ContextParser; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.ParseField; diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStatsParser.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixStatsParser.java similarity index 68% rename from modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStatsParser.java rename to modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixStatsParser.java index c42cea2778b0..edf35947f07c 100644 --- a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStatsParser.java +++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixStatsParser.java @@ -5,11 +5,9 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -package org.elasticsearch.search.aggregations.matrix.stats; +package org.elasticsearch.aggregations.metric; import org.elasticsearch.search.MultiValueMode; -import org.elasticsearch.search.aggregations.matrix.ArrayValuesSourceParser.NumericValuesSourceParser; -import org.elasticsearch.search.aggregations.support.ValueType; import org.elasticsearch.search.aggregations.support.ValuesSourceType; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.XContentParser; @@ -17,9 +15,7 @@ import java.io.IOException; import java.util.Map; -import static org.elasticsearch.search.aggregations.matrix.ArrayValuesSourceAggregationBuilder.MULTIVALUE_MODE_FIELD; - -public class MatrixStatsParser extends NumericValuesSourceParser { +public class MatrixStatsParser extends ArrayValuesSourceParser.NumericValuesSourceParser { public MatrixStatsParser() { super(true); @@ -33,9 +29,9 @@ protected boolean token( XContentParser parser, Map otherOptions ) throws IOException { - if (MULTIVALUE_MODE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + if (ArrayValuesSourceAggregationBuilder.MULTIVALUE_MODE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { if (token == XContentParser.Token.VALUE_STRING) { - otherOptions.put(MULTIVALUE_MODE_FIELD, parser.text()); + otherOptions.put(ArrayValuesSourceAggregationBuilder.MULTIVALUE_MODE_FIELD, parser.text()); return true; } } @@ -46,11 +42,10 @@ protected boolean token( protected MatrixStatsAggregationBuilder createFactory( String aggregationName, ValuesSourceType valuesSourceType, - ValueType targetValueType, Map otherOptions ) { MatrixStatsAggregationBuilder builder = new MatrixStatsAggregationBuilder(aggregationName); - String mode = (String) otherOptions.get(MULTIVALUE_MODE_FIELD); + String mode = (String) otherOptions.get(ArrayValuesSourceAggregationBuilder.MULTIVALUE_MODE_FIELD); if (mode != null) { builder.multiValueMode(MultiValueMode.fromString(mode)); } diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStatsResults.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixStatsResults.java similarity index 99% rename from modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStatsResults.java rename to modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixStatsResults.java index 45dbe94e9ce8..b5b3fad8253e 100644 --- a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStatsResults.java +++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/MatrixStatsResults.java @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -package org.elasticsearch.search.aggregations.matrix.stats; +package org.elasticsearch.aggregations.metric; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.io.stream.StreamInput; diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/ParsedMatrixStats.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/ParsedMatrixStats.java similarity index 97% rename from modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/ParsedMatrixStats.java rename to modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/ParsedMatrixStats.java index a89e6c7bbc30..0c44946ac96a 100644 --- a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/ParsedMatrixStats.java +++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/ParsedMatrixStats.java @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -package org.elasticsearch.search.aggregations.matrix.stats; +package org.elasticsearch.aggregations.metric; import org.elasticsearch.common.util.Maps; import org.elasticsearch.search.aggregations.ParsedAggregation; @@ -21,7 +21,7 @@ import java.util.Map; import java.util.Objects; -public class ParsedMatrixStats extends ParsedAggregation implements MatrixStats { +public class ParsedMatrixStats extends ParsedAggregation { private final Map counts = new LinkedHashMap<>(); private final Map means = new HashMap<>(); @@ -42,12 +42,10 @@ private void setDocCount(long docCount) { this.docCount = docCount; } - @Override public long getDocCount() { return docCount; } - @Override public long getFieldCount(String field) { if (counts.containsKey(field) == false) { return 0; @@ -55,27 +53,22 @@ public long getFieldCount(String field) { return counts.get(field); } - @Override public double getMean(String field) { return checkedGet(means, field); } - @Override public double getVariance(String field) { return checkedGet(variances, field); } - @Override public double getSkewness(String field) { return checkedGet(skewness, field); } - @Override public double getKurtosis(String field) { return checkedGet(kurtosis, field); } - @Override public double getCovariance(String fieldX, String fieldY) { if (fieldX.equals(fieldY)) { return checkedGet(variances, fieldX); @@ -83,7 +76,6 @@ public double getCovariance(String fieldX, String fieldY) { return MatrixStatsResults.getValFromUpperTriangularMatrix(covariances, fieldX, fieldY); } - @Override public double getCorrelation(String fieldX, String fieldY) { if (fieldX.equals(fieldY)) { return 1.0; diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/RunningStats.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/RunningStats.java similarity index 99% rename from modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/RunningStats.java rename to modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/RunningStats.java index 5ec08c68ed98..e4b8e15cd5e1 100644 --- a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/RunningStats.java +++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/metric/RunningStats.java @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -package org.elasticsearch.search.aggregations.matrix.stats; +package org.elasticsearch.aggregations.metric; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.io.stream.StreamInput; diff --git a/modules/aggregations/src/main/resources/META-INF/services/org.elasticsearch.plugins.spi.NamedXContentProvider b/modules/aggregations/src/main/resources/META-INF/services/org.elasticsearch.plugins.spi.NamedXContentProvider new file mode 100644 index 000000000000..c44951e05c94 --- /dev/null +++ b/modules/aggregations/src/main/resources/META-INF/services/org.elasticsearch.plugins.spi.NamedXContentProvider @@ -0,0 +1,9 @@ +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0 and the Server Side Public License, v 1; you may not use this file except +# in compliance with, at your election, the Elastic License 2.0 or the Server +# Side Public License, v 1. +# + +org.elasticsearch.aggregations.metric.MatrixStatsNamedXContentProvider diff --git a/modules/aggs-matrix-stats/src/test/java/org/elasticsearch/search/aggregations/matrix/stats/BaseMatrixStatsTestCase.java b/modules/aggregations/src/test/java/org/elasticsearch/aggregations/metric/BaseMatrixStatsTestCase.java similarity index 95% rename from modules/aggs-matrix-stats/src/test/java/org/elasticsearch/search/aggregations/matrix/stats/BaseMatrixStatsTestCase.java rename to modules/aggregations/src/test/java/org/elasticsearch/aggregations/metric/BaseMatrixStatsTestCase.java index 2e46dd436fa5..9cbebb4dab1e 100644 --- a/modules/aggs-matrix-stats/src/test/java/org/elasticsearch/search/aggregations/matrix/stats/BaseMatrixStatsTestCase.java +++ b/modules/aggregations/src/test/java/org/elasticsearch/aggregations/metric/BaseMatrixStatsTestCase.java @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -package org.elasticsearch.search.aggregations.matrix.stats; +package org.elasticsearch.aggregations.metric; import org.elasticsearch.test.ESTestCase; import org.junit.Before; diff --git a/modules/aggs-matrix-stats/src/test/java/org/elasticsearch/search/aggregations/matrix/stats/InternalMatrixStatsTests.java b/modules/aggregations/src/test/java/org/elasticsearch/aggregations/metric/InternalMatrixStatsTests.java similarity index 83% rename from modules/aggs-matrix-stats/src/test/java/org/elasticsearch/search/aggregations/matrix/stats/InternalMatrixStatsTests.java rename to modules/aggregations/src/test/java/org/elasticsearch/aggregations/metric/InternalMatrixStatsTests.java index 0dfbf854bc00..9ca315b5a7b7 100644 --- a/modules/aggs-matrix-stats/src/test/java/org/elasticsearch/search/aggregations/matrix/stats/InternalMatrixStatsTests.java +++ b/modules/aggregations/src/test/java/org/elasticsearch/aggregations/metric/InternalMatrixStatsTests.java @@ -5,8 +5,10 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -package org.elasticsearch.search.aggregations.matrix.stats; +package org.elasticsearch.aggregations.metric; +import org.elasticsearch.aggregations.AggregationsPlugin; +import org.elasticsearch.aggregations.metric.InternalMatrixStats.Fields; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.common.util.Maps; @@ -20,8 +22,6 @@ import org.elasticsearch.search.aggregations.AggregationReduceContext; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.ParsedAggregation; -import org.elasticsearch.search.aggregations.matrix.MatrixAggregationPlugin; -import org.elasticsearch.search.aggregations.matrix.stats.InternalMatrixStats.Fields; import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator.PipelineTree; import org.elasticsearch.test.InternalAggregationTestCase; import org.elasticsearch.xcontent.ContextParser; @@ -46,7 +46,7 @@ public class InternalMatrixStatsTests extends InternalAggregationTestCase randomAlphaOfLength(3)); + String unknownField = randomAlphaOfLength(3); + String other = randomValueOtherThan(unknownField, () -> randomAlphaOfLength(3)); + // getFieldCount returns 0 for unknown fields + assertEquals(0.0, actual.getFieldCount(unknownField), 0.0); - for (MatrixStats matrix : Arrays.asList(actual)) { + expectThrows(IllegalArgumentException.class, () -> actual.getMean(unknownField)); + expectThrows(IllegalArgumentException.class, () -> actual.getVariance(unknownField)); + expectThrows(IllegalArgumentException.class, () -> actual.getSkewness(unknownField)); + expectThrows(IllegalArgumentException.class, () -> actual.getKurtosis(unknownField)); - // getFieldCount returns 0 for unknown fields - assertEquals(0.0, matrix.getFieldCount(unknownField), 0.0); + expectThrows(IllegalArgumentException.class, () -> actual.getCovariance(unknownField, unknownField)); + expectThrows(IllegalArgumentException.class, () -> actual.getCovariance(unknownField, other)); + expectThrows(IllegalArgumentException.class, () -> actual.getCovariance(other, unknownField)); - expectThrows(IllegalArgumentException.class, () -> matrix.getMean(unknownField)); - expectThrows(IllegalArgumentException.class, () -> matrix.getVariance(unknownField)); - expectThrows(IllegalArgumentException.class, () -> matrix.getSkewness(unknownField)); - expectThrows(IllegalArgumentException.class, () -> matrix.getKurtosis(unknownField)); - - expectThrows(IllegalArgumentException.class, () -> matrix.getCovariance(unknownField, unknownField)); - expectThrows(IllegalArgumentException.class, () -> matrix.getCovariance(unknownField, other)); - expectThrows(IllegalArgumentException.class, () -> matrix.getCovariance(other, unknownField)); - - assertEquals(1.0, matrix.getCorrelation(unknownField, unknownField), 0.0); - expectThrows(IllegalArgumentException.class, () -> matrix.getCorrelation(unknownField, other)); - expectThrows(IllegalArgumentException.class, () -> matrix.getCorrelation(other, unknownField)); - } + assertEquals(1.0, actual.getCorrelation(unknownField, unknownField), 0.0); + expectThrows(IllegalArgumentException.class, () -> actual.getCorrelation(unknownField, other)); + expectThrows(IllegalArgumentException.class, () -> actual.getCorrelation(other, unknownField)); } @Override diff --git a/modules/aggs-matrix-stats/src/test/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStatsAggregatorTests.java b/modules/aggregations/src/test/java/org/elasticsearch/aggregations/metric/MatrixStatsAggregatorTests.java similarity index 91% rename from modules/aggs-matrix-stats/src/test/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStatsAggregatorTests.java rename to modules/aggregations/src/test/java/org/elasticsearch/aggregations/metric/MatrixStatsAggregatorTests.java index d40aa1de5385..92dfb03ff301 100644 --- a/modules/aggs-matrix-stats/src/test/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStatsAggregatorTests.java +++ b/modules/aggregations/src/test/java/org/elasticsearch/aggregations/metric/MatrixStatsAggregatorTests.java @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -package org.elasticsearch.search.aggregations.matrix.stats; +package org.elasticsearch.aggregations.metric; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; @@ -16,17 +16,14 @@ import org.apache.lucene.store.Directory; import org.apache.lucene.tests.index.RandomIndexWriter; import org.apache.lucene.util.NumericUtils; +import org.elasticsearch.aggregations.bucket.AggregationTestCase; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.NumberFieldMapper; -import org.elasticsearch.plugins.SearchPlugin; -import org.elasticsearch.search.aggregations.AggregatorTestCase; -import org.elasticsearch.search.aggregations.matrix.MatrixAggregationPlugin; import java.util.Arrays; import java.util.Collections; -import java.util.List; -public class MatrixStatsAggregatorTests extends AggregatorTestCase { +public class MatrixStatsAggregatorTests extends AggregationTestCase { public void testNoData() throws Exception { MappedFieldType ft = new NumberFieldMapper.NumberFieldType("field", NumberFieldMapper.NumberType.DOUBLE); @@ -100,9 +97,4 @@ public void testTwoFields() throws Exception { } } } - - @Override - protected List getSearchPlugins() { - return Collections.singletonList(new MatrixAggregationPlugin()); - } } diff --git a/modules/aggs-matrix-stats/src/test/java/org/elasticsearch/search/aggregations/matrix/stats/MultiPassStats.java b/modules/aggregations/src/test/java/org/elasticsearch/aggregations/metric/MultiPassStats.java similarity index 99% rename from modules/aggs-matrix-stats/src/test/java/org/elasticsearch/search/aggregations/matrix/stats/MultiPassStats.java rename to modules/aggregations/src/test/java/org/elasticsearch/aggregations/metric/MultiPassStats.java index 0bc3685f8cf8..6a43f02697e2 100644 --- a/modules/aggs-matrix-stats/src/test/java/org/elasticsearch/search/aggregations/matrix/stats/MultiPassStats.java +++ b/modules/aggregations/src/test/java/org/elasticsearch/aggregations/metric/MultiPassStats.java @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -package org.elasticsearch.search.aggregations.matrix.stats; +package org.elasticsearch.aggregations.metric; import org.elasticsearch.common.util.Maps; diff --git a/modules/aggs-matrix-stats/src/test/java/org/elasticsearch/search/aggregations/matrix/stats/RunningStatsTests.java b/modules/aggregations/src/test/java/org/elasticsearch/aggregations/metric/RunningStatsTests.java similarity index 98% rename from modules/aggs-matrix-stats/src/test/java/org/elasticsearch/search/aggregations/matrix/stats/RunningStatsTests.java rename to modules/aggregations/src/test/java/org/elasticsearch/aggregations/metric/RunningStatsTests.java index 36d29be98c35..76eda2a8dca1 100644 --- a/modules/aggs-matrix-stats/src/test/java/org/elasticsearch/search/aggregations/matrix/stats/RunningStatsTests.java +++ b/modules/aggregations/src/test/java/org/elasticsearch/aggregations/metric/RunningStatsTests.java @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -package org.elasticsearch.search.aggregations.matrix.stats; +package org.elasticsearch.aggregations.metric; import java.util.Arrays; import java.util.HashSet; diff --git a/modules/aggs-matrix-stats/src/yamlRestTest/resources/rest-api-spec/test/stats/20_empty_bucket.yml b/modules/aggregations/src/yamlRestTest/resources/rest-api-spec/test/aggregations/matrix_stats.yml similarity index 100% rename from modules/aggs-matrix-stats/src/yamlRestTest/resources/rest-api-spec/test/stats/20_empty_bucket.yml rename to modules/aggregations/src/yamlRestTest/resources/rest-api-spec/test/aggregations/matrix_stats.yml diff --git a/modules/aggs-matrix-stats/src/yamlRestTest/resources/rest-api-spec/test/stats/40_multi_value_field.yml b/modules/aggregations/src/yamlRestTest/resources/rest-api-spec/test/aggregations/matrix_stats_multi_value_field.yml similarity index 98% rename from modules/aggs-matrix-stats/src/yamlRestTest/resources/rest-api-spec/test/stats/40_multi_value_field.yml rename to modules/aggregations/src/yamlRestTest/resources/rest-api-spec/test/aggregations/matrix_stats_multi_value_field.yml index 295ac2160f23..2e2b9a94ee46 100644 --- a/modules/aggs-matrix-stats/src/yamlRestTest/resources/rest-api-spec/test/stats/40_multi_value_field.yml +++ b/modules/aggregations/src/yamlRestTest/resources/rest-api-spec/test/aggregations/matrix_stats_multi_value_field.yml @@ -1,5 +1,8 @@ --- setup: + - skip: + version: " - 8.6.99" + reason: serialization bug fixed in 8.7.0 - do: indices.create: diff --git a/modules/aggs-matrix-stats/src/yamlRestTest/resources/rest-api-spec/test/stats/30_single_value_field.yml b/modules/aggregations/src/yamlRestTest/resources/rest-api-spec/test/aggregations/matrix_stats_single_value_field.yml similarity index 100% rename from modules/aggs-matrix-stats/src/yamlRestTest/resources/rest-api-spec/test/stats/30_single_value_field.yml rename to modules/aggregations/src/yamlRestTest/resources/rest-api-spec/test/aggregations/matrix_stats_single_value_field.yml diff --git a/modules/aggs-matrix-stats/build.gradle b/modules/aggs-matrix-stats/build.gradle deleted file mode 100644 index 669739070caf..000000000000 --- a/modules/aggs-matrix-stats/build.gradle +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -apply plugin: 'elasticsearch.legacy-yaml-rest-test' -apply plugin: 'elasticsearch.legacy-yaml-rest-compat-test' - -esplugin { - description 'Adds aggregations whose input are a list of numeric fields and output includes a matrix.' - classname 'org.elasticsearch.search.aggregations.matrix.MatrixAggregationPlugin' -} - -restResources { - restApi { - include '_common', 'indices', 'cluster', 'index', 'search', 'nodes' - } -} diff --git a/modules/aggs-matrix-stats/src/main/java/module-info.java b/modules/aggs-matrix-stats/src/main/java/module-info.java deleted file mode 100644 index fa711a3b2b05..000000000000 --- a/modules/aggs-matrix-stats/src/main/java/module-info.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -module org.elasticsearch.aggs.matrix { - requires org.elasticsearch.base; - requires org.elasticsearch.server; - requires org.elasticsearch.xcontent; - requires org.apache.lucene.core; - - exports org.elasticsearch.search.aggregations.matrix; - exports org.elasticsearch.search.aggregations.matrix.stats; - - provides org.elasticsearch.plugins.spi.NamedXContentProvider - with - org.elasticsearch.search.aggregations.matrix.spi.MatrixStatsNamedXContentProvider; -} diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/MatrixAggregationPlugin.java b/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/MatrixAggregationPlugin.java deleted file mode 100644 index 57ff7d227cee..000000000000 --- a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/MatrixAggregationPlugin.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.search.aggregations.matrix; - -import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.plugins.SearchPlugin; -import org.elasticsearch.search.aggregations.matrix.stats.InternalMatrixStats; -import org.elasticsearch.search.aggregations.matrix.stats.MatrixStatsAggregationBuilder; -import org.elasticsearch.search.aggregations.matrix.stats.MatrixStatsParser; - -import java.util.List; - -import static java.util.Collections.singletonList; - -public class MatrixAggregationPlugin extends Plugin implements SearchPlugin { - @Override - public List getAggregations() { - return singletonList( - new AggregationSpec(MatrixStatsAggregationBuilder.NAME, MatrixStatsAggregationBuilder::new, new MatrixStatsParser()) - .addResultReader(InternalMatrixStats::new) - ); - } -} diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/MatrixStatsAggregationBuilders.java b/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/MatrixStatsAggregationBuilders.java deleted file mode 100644 index 735a839bf357..000000000000 --- a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/MatrixStatsAggregationBuilders.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -package org.elasticsearch.search.aggregations.matrix; - -import org.elasticsearch.search.aggregations.matrix.stats.MatrixStats; -import org.elasticsearch.search.aggregations.matrix.stats.MatrixStatsAggregationBuilder; - -public class MatrixStatsAggregationBuilders { - /** - * Create a new {@link MatrixStats} aggregation with the given name. - */ - public static MatrixStatsAggregationBuilder matrixStats(String name) { - return new MatrixStatsAggregationBuilder(name); - } -} diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStats.java b/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStats.java deleted file mode 100644 index b423fa2e5caf..000000000000 --- a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/stats/MatrixStats.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -package org.elasticsearch.search.aggregations.matrix.stats; - -import org.elasticsearch.search.aggregations.Aggregation; - -/** - * Interface for MatrixStats Metric Aggregation - */ -public interface MatrixStats extends Aggregation { - /** return the total document count */ - long getDocCount(); - - /** return total field count (differs from docCount if there are missing values) */ - long getFieldCount(String field); - - /** return the field mean */ - double getMean(String field); - - /** return the field variance */ - double getVariance(String field); - - /** return the skewness of the distribution */ - double getSkewness(String field); - - /** return the kurtosis of the distribution */ - double getKurtosis(String field); - - /** return the covariance between field x and field y */ - double getCovariance(String fieldX, String fieldY); - - /** return the correlation coefficient of field x and field y */ - double getCorrelation(String fieldX, String fieldY); -} diff --git a/modules/aggs-matrix-stats/src/main/resources/META-INF/services/org.elasticsearch.plugins.spi.NamedXContentProvider b/modules/aggs-matrix-stats/src/main/resources/META-INF/services/org.elasticsearch.plugins.spi.NamedXContentProvider deleted file mode 100644 index a2d706a39a60..000000000000 --- a/modules/aggs-matrix-stats/src/main/resources/META-INF/services/org.elasticsearch.plugins.spi.NamedXContentProvider +++ /dev/null @@ -1 +0,0 @@ -org.elasticsearch.search.aggregations.matrix.spi.MatrixStatsNamedXContentProvider \ No newline at end of file diff --git a/modules/aggs-matrix-stats/src/yamlRestTest/java/org/elasticsearch/search/aggregations/matrix/MatrixStatsClientYamlTestSuiteIT.java b/modules/aggs-matrix-stats/src/yamlRestTest/java/org/elasticsearch/search/aggregations/matrix/MatrixStatsClientYamlTestSuiteIT.java deleted file mode 100644 index 6f29a3fb765f..000000000000 --- a/modules/aggs-matrix-stats/src/yamlRestTest/java/org/elasticsearch/search/aggregations/matrix/MatrixStatsClientYamlTestSuiteIT.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -package org.elasticsearch.search.aggregations.matrix; - -import com.carrotsearch.randomizedtesting.annotations.Name; -import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; - -import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate; -import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase; - -public class MatrixStatsClientYamlTestSuiteIT extends ESClientYamlSuiteTestCase { - public MatrixStatsClientYamlTestSuiteIT(@Name("yaml") ClientYamlTestCandidate testCandidate) { - super(testCandidate); - } - - @ParametersFactory - public static Iterable parameters() throws Exception { - return ESClientYamlSuiteTestCase.createParameters(); - } -} diff --git a/modules/aggs-matrix-stats/src/yamlRestTest/resources/rest-api-spec/test/stats/10_basic.yml b/modules/aggs-matrix-stats/src/yamlRestTest/resources/rest-api-spec/test/stats/10_basic.yml deleted file mode 100644 index 2416d2b2b314..000000000000 --- a/modules/aggs-matrix-stats/src/yamlRestTest/resources/rest-api-spec/test/stats/10_basic.yml +++ /dev/null @@ -1,16 +0,0 @@ -# Integration tests for Matrix Aggs Plugin -# -"Matrix stats aggs loaded": - - skip: - reason: "contains is a newly added assertion" - features: contains - - do: - cluster.state: {} - - # Get master node id - - set: { master_node: master } - - - do: - nodes.info: {} - - - contains: { nodes.$master.modules: { name: aggs-matrix-stats } } diff --git a/x-pack/plugin/sql/build.gradle b/x-pack/plugin/sql/build.gradle index 2cc09acabeec..79a82589c07f 100644 --- a/x-pack/plugin/sql/build.gradle +++ b/x-pack/plugin/sql/build.gradle @@ -33,7 +33,6 @@ dependencies { compileOnly(project(':modules:lang-painless:spi')) api project('sql-action') api project(':modules:aggregations') - api project(':modules:aggs-matrix-stats') compileOnly project(path: xpackModule('ql')) testImplementation project(':test:framework') testImplementation(testArtifact(project(xpackModule('core')))) diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/extractor/MetricAggExtractor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/extractor/MetricAggExtractor.java index eb6288061fca..f76702f5ffe5 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/extractor/MetricAggExtractor.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/extractor/MetricAggExtractor.java @@ -6,13 +6,13 @@ */ package org.elasticsearch.xpack.sql.execution.search.extractor; +import org.elasticsearch.aggregations.metric.InternalMatrixStats; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.core.Nullable; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation.Bucket; import org.elasticsearch.search.aggregations.bucket.filter.InternalFilter; -import org.elasticsearch.search.aggregations.matrix.stats.InternalMatrixStats; import org.elasticsearch.search.aggregations.metrics.InternalAvg; import org.elasticsearch.search.aggregations.metrics.InternalCardinality; import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; @@ -35,7 +35,7 @@ import java.util.Map; import java.util.Objects; -import static org.elasticsearch.search.aggregations.matrix.stats.MatrixAggregationInspectionHelper.hasValue; +import static org.elasticsearch.aggregations.metric.MatrixAggregationInspectionHelper.hasValue; import static org.elasticsearch.search.aggregations.support.AggregationInspectionHelper.hasValue; import static org.elasticsearch.xpack.sql.type.SqlDataTypeConverter.convert; import static org.elasticsearch.xpack.sql.type.SqlDataTypes.isDateBased; diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/MatrixStatsAgg.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/MatrixStatsAgg.java index da4bea6ab89e..546dd06f287a 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/MatrixStatsAgg.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/MatrixStatsAgg.java @@ -6,12 +6,11 @@ */ package org.elasticsearch.xpack.sql.querydsl.agg; +import org.elasticsearch.aggregations.metric.MatrixStatsAggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilder; import java.util.List; -import static org.elasticsearch.search.aggregations.matrix.MatrixStatsAggregationBuilders.matrixStats; - public class MatrixStatsAgg extends LeafAgg { private final List fields; @@ -23,6 +22,6 @@ public MatrixStatsAgg(String id, List fields) { @Override AggregationBuilder toBuilder() { - return matrixStats(id()).fields(fields); + return new MatrixStatsAggregationBuilder(id()).fields(fields); } } diff --git a/x-pack/plugin/transform/build.gradle b/x-pack/plugin/transform/build.gradle index 7115cb0c3450..9e50aea3a8fc 100644 --- a/x-pack/plugin/transform/build.gradle +++ b/x-pack/plugin/transform/build.gradle @@ -13,7 +13,6 @@ dependencies { testImplementation(testArtifact(project(xpackModule('core')))) testImplementation project(path: xpackModule('analytics')) - testImplementation project(path: ':modules:aggs-matrix-stats') testImplementation project(path: ':modules:aggregations') testImplementation project(path: xpackModule('spatial')) testImplementation project(":test:x-content") diff --git a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/pivot/TransformAggregationsTests.java b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/pivot/TransformAggregationsTests.java index 3b187cb277b3..bd487c30bd0a 100644 --- a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/pivot/TransformAggregationsTests.java +++ b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/pivot/TransformAggregationsTests.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.transform.transforms.pivot; +import org.elasticsearch.aggregations.AggregationsPlugin; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.Tuple; @@ -16,7 +17,6 @@ import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.range.RangeAggregationBuilder; -import org.elasticsearch.search.aggregations.matrix.MatrixAggregationPlugin; import org.elasticsearch.search.aggregations.metrics.MaxAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.MinAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.PercentilesAggregationBuilder; @@ -141,7 +141,7 @@ public void testResolveTargetMapping() { public void testAggregationsVsTransforms() { // Note: if a new plugin is added, it must be added here - SearchModule searchModule = new SearchModule(Settings.EMPTY, Arrays.asList((new AnalyticsPlugin()), new MatrixAggregationPlugin())); + SearchModule searchModule = new SearchModule(Settings.EMPTY, Arrays.asList((new AnalyticsPlugin()), new AggregationsPlugin())); List namedWriteables = searchModule.getNamedWriteables(); List aggregationNames = namedWriteables.stream() diff --git a/x-pack/qa/runtime-fields/build.gradle b/x-pack/qa/runtime-fields/build.gradle index d89ea041dda4..5bee9f08c2a7 100644 --- a/x-pack/qa/runtime-fields/build.gradle +++ b/x-pack/qa/runtime-fields/build.gradle @@ -114,6 +114,13 @@ subprojects { 'search/400_synthetic_source/*', 'search.highlight/50_synthetic_source/*', 'aggregations/top_hits/synthetic _source', + // Runtime fields produce a slightly different result for multi valued matrix_stats yaml tests: + // (this appears to always have been the case. muted when matrix_stats was moved from its own + // module to aggregations module) + 'aggregations/matrix_stats_multi_value_field/Multi value field Min', + 'aggregations/matrix_stats_multi_value_field/Multi value field Max', + 'aggregations/matrix_stats_multi_value_field/Partially unmapped', + 'aggregations/matrix_stats_multi_value_field/Partially unmapped with missing defaults', /////// NOT SUPPORTED /////// ].join(',') } From 22977007b81154d6c9ed42620be2851a683238eb Mon Sep 17 00:00:00 2001 From: Pooya Salehi Date: Thu, 22 Dec 2022 11:07:53 +0100 Subject: [PATCH 341/919] Use MockDirectoryWrapper in MultiFileWriterTests (#92519) The underlying Lucene test setup `rarely()` chooses a `RawDirectoryWrapper` which is a final class. Closes #92497. --- .../elasticsearch/indices/recovery/MultiFileWriterTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/MultiFileWriterTests.java b/server/src/test/java/org/elasticsearch/indices/recovery/MultiFileWriterTests.java index 56fe971ce6d8..3d59bda185ff 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/MultiFileWriterTests.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/MultiFileWriterTests.java @@ -42,7 +42,7 @@ public class MultiFileWriterTests extends IndexShardTestCase { public void setUp() throws Exception { super.setUp(); indexShard = newShard(true); - directory = newFSDirectory(indexShard.shardPath().resolveIndex()); + directory = newMockFSDirectory(indexShard.shardPath().resolveIndex()); directorySpy = spy(directory); store = createStore(indexShard.shardId(), indexShard.indexSettings(), directorySpy); } From 0f9750381cf40a003b41ef5308b37cb4a77febad Mon Sep 17 00:00:00 2001 From: Salvatore Campagna <93581129+salvatore-campagna@users.noreply.github.com> Date: Thu, 22 Dec 2022 12:53:21 +0100 Subject: [PATCH 342/919] Prevent usage of time series aggregations with some parent aggs (#92499) Fail composite aggregation query execution when in time series mode. TimeSeriesIndexSearcher does not support it, so we fail with a proper exception rather than using a NO_OP_COLLECTOR triggering a CollectionTerminatedException. --- .../test/aggregations/time_series.yml | 88 +++++++++++++++++++ .../CompositeAggregationBuilder.java | 3 + 2 files changed, 91 insertions(+) diff --git a/modules/aggregations/src/yamlRestTest/resources/rest-api-spec/test/aggregations/time_series.yml b/modules/aggregations/src/yamlRestTest/resources/rest-api-spec/test/aggregations/time_series.yml index 8a8ee0a0f0e0..613106257c93 100644 --- a/modules/aggregations/src/yamlRestTest/resources/rest-api-spec/test/aggregations/time_series.yml +++ b/modules/aggregations/src/yamlRestTest/resources/rest-api-spec/test/aggregations/time_series.yml @@ -79,3 +79,91 @@ setup: - match: { aggregations.ts.buckets.0.key: { "key": "foo" } } - match: { aggregations.ts.buckets.0.doc_count: 1 } +--- +"Sampler aggregation with nested time series aggregation failure": + - skip: + version: " - 8.6.99" + reason: "Handling for time series aggregation failures introduced in 8.7.0" + + - do: + catch: '/\[random_sampler\] aggregation \[sample\] does not support sampling \[time_series\] aggregation \[ts\]/' + search: + index: tsdb + body: + aggs: + sample: + random_sampler: + probability: 1.0 + aggs: + by_timestamp: + date_histogram: + field: "@timestamp" + fixed_interval: 1h + aggs: + ts: + time_series: + keyed: false + aggs: + sum: + sum: + field: val + +--- +"Composite aggregation with nested time series aggregation failure": + - skip: + version: " - 8.6.99" + reason: "Handling for time series aggregation failures introduced in 8.7.0" + + - do: + catch: '/\[composite\] aggregation is incompatible with time series execution mode/' + search: + index: tsdb + body: + aggs: + by_key: + composite: + sources: [ + { + "key": { + "terms": { + "field": "key" + } + } + } + ] + aggs: + date: + date_histogram: + field: "@timestamp" + fixed_interval: "1h" + aggs: + ts: + time_series: + keyed: false + aggs: + sum: + sum: + field: val + +--- +"Global aggregation with nested time series aggregation failure": + - skip: + version: " - 8.6.99" + reason: "Handling for time series aggregation failures introduced in 8.7.0" + + - do: + catch: '/Time series aggregations cannot be used inside global aggregation./' + search: + index: tsdb + body: + aggs: + global: + global: {} + aggs: + ts: + time_series: + keyed: false + aggs: + sum: + sum: + field: val diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregationBuilder.java index 9257e28edd1f..c52a4475e9cc 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/composite/CompositeAggregationBuilder.java @@ -211,6 +211,9 @@ protected AggregatorFactory doBuild( AggregatorFactory parent, AggregatorFactories.Builder subfactoriesBuilder ) throws IOException { + if (context.isInSortOrderExecutionRequired()) { + throw new IllegalArgumentException("[composite] aggregation is incompatible with time series execution mode"); + } AggregatorFactory invalid = validateParentAggregations(parent); if (invalid != null) { throw new IllegalArgumentException( From 85155356523d1ba086cfdec35e903ce295552433 Mon Sep 17 00:00:00 2001 From: Pooya Salehi Date: Thu, 22 Dec 2022 13:34:38 +0100 Subject: [PATCH 343/919] Upgrade GCS SDK to 2.13.1 (#92327) Upgrade to the latest version of the SDK that doesn't have known CVEs and builds w/o complaining. Since 2.2.0 the automatic retry behaviour has changed and the old behaviour can still be used as LegacyStorageRetryStrategy. The default retry strategy would cause some test failures, and therefore we'd need to explicitly set a retry strategy. Relates #92474 --- docs/changelog/92327.yaml | 5 + gradle/verification-metadata.xml | 125 +++++++++++------- modules/repository-gcs/build.gradle | 50 ++++--- ...eCloudStorageBlobStoreRepositoryTests.java | 2 + .../gcs/GoogleCloudStorageService.java | 2 + ...CloudStorageBlobContainerRetriesTests.java | 2 + 6 files changed, 119 insertions(+), 67 deletions(-) create mode 100644 docs/changelog/92327.yaml diff --git a/docs/changelog/92327.yaml b/docs/changelog/92327.yaml new file mode 100644 index 000000000000..4c42885f98e0 --- /dev/null +++ b/docs/changelog/92327.yaml @@ -0,0 +1,5 @@ +pr: 92327 +summary: Upgrade GCS SDK to 2.13.1 +area: Snapshot/Restore +type: upgrade +issues: [] diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 6876b8996d9a..da64511e875e 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -429,19 +429,19 @@ - - - + + + - - - + + + - - - + + + @@ -449,19 +449,19 @@ - - - + + + - - - + + + - - - + + + @@ -469,34 +469,39 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + + + + + + @@ -509,6 +514,11 @@ + + + + + @@ -614,9 +624,14 @@ - - - + + + + + + + + @@ -629,6 +644,11 @@ + + + + + @@ -639,6 +659,11 @@ + + + + + @@ -1204,9 +1229,9 @@ - - - + + + @@ -1344,6 +1369,11 @@ + + + + + @@ -1354,6 +1384,11 @@ + + + + + @@ -3804,9 +3839,9 @@ - - - + + + diff --git a/modules/repository-gcs/build.gradle b/modules/repository-gcs/build.gradle index b98a97cb34be..d7376bb273e9 100644 --- a/modules/repository-gcs/build.gradle +++ b/modules/repository-gcs/build.gradle @@ -26,36 +26,36 @@ esplugin { } dependencies { - api 'com.google.cloud:google-cloud-storage:1.118.1' - api 'com.google.cloud:google-cloud-core:2.0.2' - api 'com.google.cloud:google-cloud-core-http:2.0.2' - runtimeOnly 'com.google.guava:guava:30.1.1-jre' + api 'com.google.cloud:google-cloud-storage:2.13.1' + api 'com.google.cloud:google-cloud-core:2.8.28' + api 'com.google.cloud:google-cloud-core-http:2.8.28' + runtimeOnly 'com.google.guava:guava:31.1-jre' runtimeOnly 'com.google.guava:failureaccess:1.0.1' api "commons-logging:commons-logging:${versions.commonslogging}" api "org.apache.logging.log4j:log4j-1.2-api:${versions.log4j}" api "commons-codec:commons-codec:${versions.commonscodec}" - api 'com.google.api:api-common:2.2.1' - api 'com.google.api:gax:2.0.0' - api 'org.threeten:threetenbp:1.5.1' + api 'com.google.api:api-common:2.3.1' + api 'com.google.api:gax:2.20.1' + api 'org.threeten:threetenbp:1.6.5' api "com.google.protobuf:protobuf-java-util:${versions.protobuf}" api "com.google.protobuf:protobuf-java:${versions.protobuf}" - api 'com.google.code.gson:gson:2.8.9' - api 'com.google.api.grpc:proto-google-common-protos:2.3.2' - api 'com.google.api.grpc:proto-google-iam-v1:1.0.14' - api 'com.google.auth:google-auth-library-credentials:1.0.0' - api 'com.google.auth:google-auth-library-oauth2-http:1.0.0' + api 'com.google.code.gson:gson:2.10' + api 'com.google.api.grpc:proto-google-common-protos:2.9.6' + api 'com.google.api.grpc:proto-google-iam-v1:1.6.2' + api 'com.google.auth:google-auth-library-credentials:1.11.0' + api 'com.google.auth:google-auth-library-oauth2-http:1.11.0' api 'com.google.oauth-client:google-oauth-client:1.34.1' - api 'com.google.api-client:google-api-client:1.35.1' - api 'com.google.http-client:google-http-client:1.39.2' - api 'com.google.http-client:google-http-client-gson:1.39.2' - api 'com.google.http-client:google-http-client-appengine:1.39.2' - api 'com.google.http-client:google-http-client-jackson2:1.39.2' + api 'com.google.api-client:google-api-client:2.1.1' + api 'com.google.http-client:google-http-client:1.42.3' + api 'com.google.http-client:google-http-client-gson:1.42.3' + api 'com.google.http-client:google-http-client-appengine:1.42.3' + api 'com.google.http-client:google-http-client-jackson2:1.42.3' api "com.fasterxml.jackson.core:jackson-core:${versions.jackson}" - api 'com.google.api:gax-httpjson:0.85.0' - api 'io.grpc:grpc-context:1.39.0' - api 'io.opencensus:opencensus-api:0.28.0' - api 'io.opencensus:opencensus-contrib-http-util:0.28.0' - api 'com.google.apis:google-api-services-storage:v1-rev20210127-1.32.1' + api 'com.google.api:gax-httpjson:0.105.1' + api 'io.grpc:grpc-context:1.49.2' + api 'io.opencensus:opencensus-api:0.31.1' + api 'io.opencensus:opencensus-contrib-http-util:0.31.1' + api 'com.google.apis:google-api-services-storage:v1-rev20220705-2.0.0' testImplementation project(':test:fixtures:gcs-fixture') } @@ -180,6 +180,12 @@ tasks.named("thirdPartyAudit").configure { 'org.apache.http.protocol.HttpContext', 'org.apache.http.protocol.HttpProcessor', 'org.apache.http.protocol.HttpRequestExecutor', + // com.google.api.gax optional dependencies + 'org.graalvm.nativeimage.hosted.Feature', + 'org.graalvm.nativeimage.hosted.Feature$BeforeAnalysisAccess', + 'org.graalvm.nativeimage.hosted.Feature$DuringAnalysisAccess', + 'org.graalvm.nativeimage.hosted.Feature$FeatureAccess', + 'org.graalvm.nativeimage.hosted.RuntimeReflection', // commons-logging provided dependencies 'javax.servlet.ServletContextEvent', 'javax.servlet.ServletContextListener' diff --git a/modules/repository-gcs/src/internalClusterTest/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java b/modules/repository-gcs/src/internalClusterTest/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java index b658b036ffed..8a4fb8eb41bd 100644 --- a/modules/repository-gcs/src/internalClusterTest/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java +++ b/modules/repository-gcs/src/internalClusterTest/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java @@ -15,6 +15,7 @@ import com.google.api.gax.retrying.RetrySettings; import com.google.cloud.http.HttpTransportOptions; import com.google.cloud.storage.StorageOptions; +import com.google.cloud.storage.StorageRetryStrategy; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; @@ -227,6 +228,7 @@ StorageOptions createStorageOptions( ) { StorageOptions options = super.createStorageOptions(gcsClientSettings, httpTransportOptions); return options.toBuilder() + .setStorageRetryStrategy(StorageRetryStrategy.getLegacyStorageRetryStrategy()) .setHost(options.getHost()) .setCredentials(options.getCredentials()) .setRetrySettings( diff --git a/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageService.java b/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageService.java index 4743eb71f832..e65f391667d7 100644 --- a/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageService.java +++ b/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageService.java @@ -19,6 +19,7 @@ import com.google.cloud.http.HttpTransportOptions; import com.google.cloud.storage.Storage; import com.google.cloud.storage.StorageOptions; +import com.google.cloud.storage.StorageRetryStrategy; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -176,6 +177,7 @@ StorageOptions createStorageOptions( final HttpTransportOptions httpTransportOptions ) { final StorageOptions.Builder storageOptionsBuilder = StorageOptions.newBuilder() + .setStorageRetryStrategy(StorageRetryStrategy.getLegacyStorageRetryStrategy()) .setTransportOptions(httpTransportOptions) .setHeaderProvider(() -> { return Strings.hasLength(gcsClientSettings.getApplicationName()) diff --git a/modules/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobContainerRetriesTests.java b/modules/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobContainerRetriesTests.java index 5079ef71a218..dd08be44d54a 100644 --- a/modules/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobContainerRetriesTests.java +++ b/modules/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobContainerRetriesTests.java @@ -13,6 +13,7 @@ import com.google.cloud.http.HttpTransportOptions; import com.google.cloud.storage.StorageException; import com.google.cloud.storage.StorageOptions; +import com.google.cloud.storage.StorageRetryStrategy; import com.sun.net.httpserver.HttpHandler; import org.apache.http.HttpStatus; @@ -140,6 +141,7 @@ StorageOptions createStorageOptions( retrySettingsBuilder.setMaxAttempts(maxRetries + 1); } return options.toBuilder() + .setStorageRetryStrategy(StorageRetryStrategy.getLegacyStorageRetryStrategy()) .setHost(options.getHost()) .setCredentials(options.getCredentials()) .setRetrySettings(retrySettingsBuilder.build()) From 08515eac09ca549036c963df5670ca0956aaa047 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Thu, 22 Dec 2022 12:51:24 +0000 Subject: [PATCH 344/919] [ML] Fix hang if renormalization is retrying writes during feature reset (#92524) This is basically a test issue, as feature reset isn't usable in production due to reseting security. Fixes #92521 --- .../normalizer/ShortCircuitingRenormalizer.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/ShortCircuitingRenormalizer.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/ShortCircuitingRenormalizer.java index c2ac0055daf6..1371221cb16b 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/ShortCircuitingRenormalizer.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/normalizer/ShortCircuitingRenormalizer.java @@ -8,6 +8,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.util.CancellableThreads; import org.elasticsearch.xpack.core.ml.job.process.autodetect.state.Quantiles; import java.util.Date; @@ -94,8 +95,15 @@ public void waitUntilIdle() throws InterruptedException { try { taskToWaitFor.get(); } catch (ExecutionException e) { - // This shouldn't happen, because we catch normalization errors inside the normalization loop + if (e.getCause() instanceof CancellableThreads.ExecutionCancelledException) { + // This happens if reset mode is enabled while result writes are being retried. + // Reset mode means the job whose results were being normalized will shortly + // cease to exist, so it's fine to consider the wait for renormalization complete. + break; + } logger.error("[" + jobId + "] Error propagated from normalization", e); + // Don't loop again for the same task that caused the error + taskToWaitFor = null; } catch (CancellationException e) { // Convert cancellations to interruptions to simplify the interface throw new InterruptedException("Normalization cancelled"); From df8ccceb5767cd8a38514d81cf1396d09922f580 Mon Sep 17 00:00:00 2001 From: Christos Soulios <1561376+csoulios@users.noreply.github.com> Date: Thu, 22 Dec 2022 16:29:03 +0200 Subject: [PATCH 345/919] [TSDB] Improve downsampling performance by removing map lookups (#92494) This PR improves downsampling performance by removing all the hashmap lookups. Also, it modifies metrics/label producers so that they extract the doc_values directly from the leaves. This allows for extra optimizations for cases such as labels/counters that do not extract doc_values unless they are consumed. Finally, this PR separates loading and computing of aggregate_metric_double fields Co-authored-by: Nik Everett --- .../AbstractRollupFieldProducer.java | 23 +-- .../AggregateMetricFieldSerializer.java | 65 ++++++++ .../AggregateMetricFieldValueFetcher.java | 65 ++++++++ .../xpack/downsample/FieldValueFetcher.java | 54 +++--- .../xpack/downsample/LabelFieldProducer.java | 156 +++++------------- .../xpack/downsample/MetricFieldProducer.java | 121 ++++---------- .../downsample/RollupFieldSerializer.java | 20 +++ .../xpack/downsample/RollupShardIndexer.java | 130 ++++++--------- .../downsample/LabelFieldProducerTests.java | 30 +++- .../downsample/MetricFieldProducerTests.java | 98 ++--------- 10 files changed, 342 insertions(+), 420 deletions(-) create mode 100644 x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/AggregateMetricFieldSerializer.java create mode 100644 x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/AggregateMetricFieldValueFetcher.java create mode 100644 x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/RollupFieldSerializer.java diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/AbstractRollupFieldProducer.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/AbstractRollupFieldProducer.java index 44bf433ada13..935eb0735775 100644 --- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/AbstractRollupFieldProducer.java +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/AbstractRollupFieldProducer.java @@ -7,16 +7,16 @@ package org.elasticsearch.xpack.downsample; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.index.fielddata.FormattedDocValues; import java.io.IOException; /** - * Base class for classes that read metric and label fields. + * Base class that reads fields from the source index and produces their downsampled values */ -abstract class AbstractRollupFieldProducer { +abstract class AbstractRollupFieldProducer implements RollupFieldSerializer { - protected final String name; + private final String name; protected boolean isEmpty; AbstractRollupFieldProducer(String name) { @@ -24,14 +24,6 @@ abstract class AbstractRollupFieldProducer { this.isEmpty = true; } - /** - * Collect a value for the field applying the specific subclass collection strategy. - * - * @param field the name of the field to collect - * @param value the value to collect. - */ - public abstract void collect(String field, T value); - /** * @return the name of the field. */ @@ -44,15 +36,12 @@ public String name() { */ public abstract void reset(); - /** - * Serialize the downsampled value of the field. - */ - public abstract void write(XContentBuilder builder) throws IOException; - /** * @return true if the field has not collected any value. */ public boolean isEmpty() { return isEmpty; } + + public abstract void collect(FormattedDocValues docValues, int docId) throws IOException; } diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/AggregateMetricFieldSerializer.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/AggregateMetricFieldSerializer.java new file mode 100644 index 000000000000..50014f304a7a --- /dev/null +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/AggregateMetricFieldSerializer.java @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.downsample; + +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Collection; + +public class AggregateMetricFieldSerializer implements RollupFieldSerializer { + private final Collection producers; + private final String name; + + /** + * @param name the name of the aggregate_metric_double field as it will be serialized + * in the downsampled index + * @param producers a collection of {@link AbstractRollupFieldProducer} instances with the subfields + * of the aggregate_metric_double field. + */ + public AggregateMetricFieldSerializer(String name, Collection producers) { + this.name = name; + this.producers = producers; + } + + @Override + public void write(XContentBuilder builder) throws IOException { + if (isEmpty()) { + return; + } + + builder.startObject(name); + for (AbstractRollupFieldProducer rollupFieldProducer : producers) { + assert name.equals(rollupFieldProducer.name()) : "producer has a different name"; + if (rollupFieldProducer.isEmpty() == false) { + if (rollupFieldProducer instanceof MetricFieldProducer metricFieldProducer) { + for (MetricFieldProducer.Metric metric : metricFieldProducer.metrics()) { + if (metric.get() != null) { + builder.field(metric.name(), metric.get()); + } + } + } else if (rollupFieldProducer instanceof LabelFieldProducer labelFieldProducer) { + LabelFieldProducer.Label label = labelFieldProducer.label(); + if (label.get() != null) { + builder.field(label.name(), label.get()); + } + } + } + } + builder.endObject(); + } + + private boolean isEmpty() { + for (AbstractRollupFieldProducer p : producers) { + if (p.isEmpty() == false) { + return false; + } + } + return true; + } +} diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/AggregateMetricFieldValueFetcher.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/AggregateMetricFieldValueFetcher.java new file mode 100644 index 000000000000..19abd6add235 --- /dev/null +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/AggregateMetricFieldValueFetcher.java @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.downsample; + +import org.elasticsearch.index.fielddata.IndexFieldData; +import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.mapper.NumberFieldMapper; +import org.elasticsearch.xpack.aggregatemetric.mapper.AggregateDoubleMetricFieldMapper; +import org.elasticsearch.xpack.aggregatemetric.mapper.AggregateDoubleMetricFieldMapper.AggregateDoubleMetricFieldType; + +import java.util.List; + +public class AggregateMetricFieldValueFetcher extends FieldValueFetcher { + + private AggregateDoubleMetricFieldType aggMetricFieldType; + + private final AbstractRollupFieldProducer rollupFieldProducer; + + protected AggregateMetricFieldValueFetcher( + MappedFieldType fieldType, + AggregateDoubleMetricFieldType aggMetricFieldType, + IndexFieldData fieldData + ) { + super(fieldType, fieldData); + this.aggMetricFieldType = aggMetricFieldType; + this.rollupFieldProducer = createRollupFieldProducer(); + } + + public AbstractRollupFieldProducer rollupFieldProducer() { + return rollupFieldProducer; + } + + private AbstractRollupFieldProducer createRollupFieldProducer() { + AggregateDoubleMetricFieldMapper.Metric metric = null; + for (var e : aggMetricFieldType.getMetricFields().entrySet()) { + NumberFieldMapper.NumberFieldType metricSubField = e.getValue(); + if (metricSubField.name().equals(name())) { + metric = e.getKey(); + break; + } + } + assert metric != null : "Cannot resolve metric type for field " + name(); + + if (aggMetricFieldType.getMetricType() != null) { + // If the field is an aggregate_metric_double field, we should use the correct subfields + // for each aggregation. This is a rollup-of-rollup case + MetricFieldProducer.Metric metricOperation = switch (metric) { + case max -> new MetricFieldProducer.Max(); + case min -> new MetricFieldProducer.Min(); + case sum -> new MetricFieldProducer.Sum(); + // To compute value_count summary, we must sum all field values + case value_count -> new MetricFieldProducer.Sum(AggregateDoubleMetricFieldMapper.Metric.value_count.name()); + }; + return new MetricFieldProducer.GaugeMetricFieldProducer(aggMetricFieldType.name(), List.of(metricOperation)); + } else { + // If field is not a metric, we downsample it as a label + return new LabelFieldProducer.AggregateMetricFieldProducer.AggregateMetricFieldProducer(aggMetricFieldType.name(), metric); + } + } +} diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/FieldValueFetcher.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/FieldValueFetcher.java index 5c99d6135b3e..f3103374df0c 100644 --- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/FieldValueFetcher.java +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/FieldValueFetcher.java @@ -16,51 +16,59 @@ import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.xpack.aggregatemetric.mapper.AggregateDoubleMetricFieldMapper; +import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.List; /** * Utility class used for fetching field values by reading field data */ class FieldValueFetcher { - private final String name; + private final MappedFieldType fieldType; - private final DocValueFormat format; private final IndexFieldData fieldData; + private final AbstractRollupFieldProducer rollupFieldProducer; - protected FieldValueFetcher(String name, MappedFieldType fieldType, IndexFieldData fieldData) { - this.name = name; + protected FieldValueFetcher(MappedFieldType fieldType, IndexFieldData fieldData) { this.fieldType = fieldType; - this.format = fieldType.docValueFormat(null, null); this.fieldData = fieldData; + this.rollupFieldProducer = createRollupFieldProducer(); } public String name() { - return name; + return fieldType().name(); } public MappedFieldType fieldType() { return fieldType; } - public DocValueFormat format() { - return format; + public FormattedDocValues getLeaf(LeafReaderContext context) { + DocValueFormat format = fieldType.docValueFormat(null, null); + return fieldData.load(context).getFormattedValues(format); } - public IndexFieldData fieldData() { - return fieldData; + public AbstractRollupFieldProducer rollupFieldProducer() { + return rollupFieldProducer; } - FormattedDocValues getLeaf(LeafReaderContext context) { - return fieldData.load(context).getFormattedValues(format); + private AbstractRollupFieldProducer createRollupFieldProducer() { + if (fieldType.getMetricType() != null) { + return switch (fieldType.getMetricType()) { + case gauge -> new MetricFieldProducer.GaugeMetricFieldProducer(name()); + case counter -> new MetricFieldProducer.CounterMetricFieldProducer(name()); + }; + } else { + // If field is not a metric, we downsample it as a label + return new LabelFieldProducer.LabelLastValueFieldProducer(name()); + } } /** * Retrieve field value fetchers for a list of fields. */ - static Map create(SearchExecutionContext context, String[] fields) { - Map fetchers = new LinkedHashMap<>(); + static List create(SearchExecutionContext context, String[] fields) { + List fetchers = new ArrayList<>(); for (String field : fields) { MappedFieldType fieldType = context.getFieldType(field); assert fieldType != null : "Unknown field type for field: [" + field + "]"; @@ -71,24 +79,16 @@ static Map create(SearchExecutionContext context, Str for (NumberFieldMapper.NumberFieldType metricSubField : aggMetricFieldType.getMetricFields().values()) { if (context.fieldExistsInIndex(metricSubField.name())) { IndexFieldData fieldData = context.getForField(metricSubField, MappedFieldType.FielddataOperation.SEARCH); - fetchers.put(metricSubField.name(), new FieldValueFetcher(metricSubField.name(), fieldType, fieldData)); + fetchers.add(new AggregateMetricFieldValueFetcher(metricSubField, aggMetricFieldType, fieldData)); } } } else { if (context.fieldExistsInIndex(field)) { IndexFieldData fieldData = context.getForField(fieldType, MappedFieldType.FielddataOperation.SEARCH); - fetchers.put(field, new FieldValueFetcher(field, fieldType, fieldData)); + fetchers.add(new FieldValueFetcher(fieldType, fieldData)); } } } - return Collections.unmodifiableMap(fetchers); - } - - static Map docValuesFetchers(LeafReaderContext ctx, Map fieldValueFetchers) { - final Map docValuesFetchers = new LinkedHashMap<>(fieldValueFetchers.size()); - for (FieldValueFetcher fetcher : fieldValueFetchers.values()) { - docValuesFetchers.put(fetcher.name(), fetcher.getLeaf(ctx)); - } - return Collections.unmodifiableMap(docValuesFetchers); + return Collections.unmodifiableList(fetchers); } } diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/LabelFieldProducer.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/LabelFieldProducer.java index 8affcbdc0c1e..d16b2f6373c4 100644 --- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/LabelFieldProducer.java +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/LabelFieldProducer.java @@ -7,58 +7,25 @@ package org.elasticsearch.xpack.downsample; -import org.elasticsearch.index.mapper.MappedFieldType; -import org.elasticsearch.index.mapper.NumberFieldMapper; -import org.elasticsearch.index.query.SearchExecutionContext; +import org.elasticsearch.index.fielddata.FormattedDocValues; import org.elasticsearch.xcontent.XContentBuilder; -import org.elasticsearch.xpack.aggregatemetric.mapper.AggregateDoubleMetricFieldMapper; +import org.elasticsearch.xpack.aggregatemetric.mapper.AggregateDoubleMetricFieldMapper.Metric; import java.io.IOException; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; /** * Class that produces values for a label field. */ -abstract class LabelFieldProducer extends AbstractRollupFieldProducer { +abstract class LabelFieldProducer extends AbstractRollupFieldProducer { - private final Label label; - - LabelFieldProducer(String name, Label label) { + LabelFieldProducer(String name) { super(name); - this.label = label; - } - - public String name() { - return name; - } - - /** Collect the value of a raw field */ - @Override - public void collect(String field, Object value) { - label.collect(value); - isEmpty = false; - } - - public Label label() { - return this.label; } - public void reset() { - label.reset(); - isEmpty = true; - } - - /** - * Return the downsampled value as computed after collecting all raw values. - * @return - */ - public abstract Object value(); + abstract Label label(); abstract static class Label { - final String name; + private final String name; /** * Abstract class that defines how a label is downsampled. @@ -94,14 +61,7 @@ static class LastValueLabel extends Label { } LastValueLabel() { - super("last_value"); - } - - @Override - void collect(Object value) { - if (lastValue == null) { - lastValue = value; - } + this("last_value"); } @Override @@ -113,101 +73,75 @@ Object get() { void reset() { lastValue = null; } + + void collect(Object value) { + if (lastValue == null) { + lastValue = value; + } + } } /** * {@link LabelFieldProducer} implementation for a last value label */ static class LabelLastValueFieldProducer extends LabelFieldProducer { + private final LastValueLabel label; + + LabelLastValueFieldProducer(String name, LastValueLabel label) { + super(name); + this.label = label; + } LabelLastValueFieldProducer(String name) { - super(name, new LastValueLabel()); + this(name, new LastValueLabel()); } @Override - public Object value() { - return label().get(); + Label label() { + return label; } @Override public void write(XContentBuilder builder) throws IOException { if (isEmpty() == false) { - builder.field(name(), value()); + builder.field(name(), label.get()); } } - } - - static class AggregateMetricFieldProducer extends LabelFieldProducer { - - private Map labelsByField = new LinkedHashMap<>(); - - AggregateMetricFieldProducer(String name) { - super(name, null); - } - - public void addLabel(String field, Label label) { - labelsByField.put(field, label); - } - - @Override - public void collect(String field, Object value) { - labelsByField.get(field).collect(value); - isEmpty = false; - } @Override - public void write(XContentBuilder builder) throws IOException { + public void collect(FormattedDocValues docValues, int docId) throws IOException { if (isEmpty() == false) { - builder.startObject(name()); - for (Label label : labels()) { - if (label.get() != null) { - builder.field(label.name(), label.get()); - } - } - builder.endObject(); + return; + } + if (docValues.advanceExact(docId) == false) { + return; } - } - - public Collection

  • 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 diff --git a/docs/reference/ml/images/ml-rule.png b/docs/reference/ml/images/ml-rule.png new file mode 100644 index 0000000000000000000000000000000000000000..1008dadf2b664f954fa562ef9c3856cd8c2e4bb7 GIT binary patch literal 325559 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
    M}hs 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ف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 z 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≪{f{*PZU3o9zr+ujVi`9Zu&$#B7U`T1>y!Hf+)h zDQ~JE;Z_8^GfK2ZnbfEBTGHGvWRLTrP_A80V)&gL*PRa=%1w6S#Y+~$^|^`(DK;4T z@3>Dx-*+ZGs-$1+zCX6DjBiT(8MlWG+^1Z9R-^{k-|~!Wp7u{UXiO-R&%O?|3d0lM z09mXNvu@IRCjk|nmAACjTpFCH>7LA&g8!ItxbSnGb09S9Pf0kgVl@Wi3V^eR79q9u z*5M2AGsNSGwl^nVL35BcEO6!$ZoCOEBxYkIR;)dJB0m9*={GfLE=AT`l9wickW^@Rq z)IF(96gC!`+zYf!BZxwZhy}4f+xNjOIDJqf@GMen)P=n?T*kee?qBXzIU{Ay8#5(= z+uNkg#r1xs<0HLi#~8pk1iwJG!+9Wk<+X$Iv-s8Zt>Dll>%oih+f{8T=25Ax8@;pi zua1_5%_m+sCS+k+ck@jAg?=(Cg-|{HLltIqcwa#DN7Z0Ptwpeh{7nmiVkcq5MN|Ua z)V68%k7<7cn_#Xx56{5Qdr;y^$hI=7P_T&H__@6L>yA%)i|C)A`N*qQlELaj1C({%Urr9Ui!A#KH@W< z#8xw=3pYDc__R=ZfGKLhUjsEl`bUU|V+%n|6Twt%WB#$M<)KY!uP)P9{Ui163uXEJS}`V`m!xnY!l-+_Cx$+ ztIC*m9&%5L>Wfd$$R8MEcQcqzZhyj-Kymm%GsE*fKgVl<#UIcb1I@TStMl}sLXBs8 zw2l;CG?%J;rh6*yx0Nms5o;1FJf1yTXdJE*V1kujF^^h@E?l?}Yv;irYQKSEeRx-f zy=KdPOQ0m7ZwjK2|08!7^}Rd&u#N77>}|_dy4P2~hjBe_#@mg0R;S9F`OKlG??w_f znj0N%_@4e8M#U2Ua@b!Mj6juE!R^MEtZR#T={ym7?iRz41y?sBuplvLwUVr;ls4y6 zXB2ax)L9elW@Y*5Uq}4l#nlH&LzTg-c_vQkzoA$v5fe%dO-%KTIveAMkiSv$S-_5PrtM1i^J zsQ3H}H6@Ck)f&a#n9@h85Aa7-WzTj)?xV*jlCLa=nBTLRxbH>wVqCg>5lT8s;^T1g zny6?%LN-?A>2fS zM)CPP$BDqtLswaPc2@8Oo5o@2Cumg00D4#3*WKR8|KPxc$2;nM2aK`jsQe*EXo26p z@8s2?>_}47ZlB`oho!}ms&J|mrXf+!egqKY1(#GGWqI_m)OC!&{}RgyCvn=WAqga< z_hMF<$v3;ka2&P^p^J@BvpKVS2TzO;&-|HZi5SEub=yg z#tO%uO(B85Vq3L8;0M7&n1Uo#NBH4)RyCz#VH-^#S|T%qE*)|ZZjTFMieu9dbt!ES zw@na(U>%z>6o)XU6Ru_6S&4IH9w(8nla^?I6=27h5fK0>KO{;qZ5RCpYhvf9k2^RZYIA+$@wk!TY457{%>e zYE)!Bff7v<8$LpAdOc|XG^&DQ^LJ(V4&0aIMNKRlE{+x+xLq{5e3aC129tbK{6B8t zC<*_Ng%WV&c*+FIAbiiok_fs7zGb#ZShs1U*$D8zi=FdGBUvOKn zGM@9pAak3Z9!dA-zP3^+A8WF)IK22EWz2nNa>pLtb$*}Y6W;RmRd@-8LJm!4%atO0 zjH1``=yp9g%|oobJ7(M_Div6}%Hr|Vy4JJkLExkxd(uvK!P)s3*>E*JT}V@{8$C-S zJ;-8kt)g}6=XQ@n+B1p=(LvWa*K!%wDp;lV&|yP>MwD?UhtaETA3RX#(GUl2xm^ls z5e!f(7MfV=)LFv7c3Twpc$d2{EB$w*&L(d~sbc*GDcRR+MlhM$(XQf^8r(G3{Uj$2 z_eS=;6Pt&InwVAZgD9C*D!@M727W`5hKui!xoXB-i|q#%!?H4(5>e^z;QfXA3)D^2<=4~UrSgO*i#tiNa;a76 za_$E~D)BH18-A}Y3Cl8PqZYIHQ2ukP-8mauiNvAuwl8+Ui1g`QMryxjs<&HMw#MCh#t49q`;2r7->F-L|%v5sFbo)wRq&a^$w817ER4lY4KR`V&#Ihe8@)O?|Z&EE{OwBNGUE)n`ufWaomL zhvg>a9Sowo#bAED38Q5vCgWX~Ca)LUu_dmVI)Fh|8gJ!uq2I^LTkg%BM&-k9)AP@h z`=xK?%I`JqgdW04eVDfB^PjfgGq+^$7oT}N_Z_^J9y9Gx9A$Y16lK4hFyQEsB&Tkb2HWJ?tg_tlU|%GWVZo(+UZ!p7HbM zY$=Qt5Q6&yTK9avJ6whp|K{8Fin6nni)~TDlWG-j4jdnMVIdXPngP zq!8Q{D5Gq(Wp>)rF6Kos44R#he6+eH!v`*V+nmbs{+%AcZYo2M%z%*4o!JQ7TG4w} z;PXN4eh9heyo>vUWCce7Yr6?loRW%kbcwuHqHJ1@Xm(L-vKag!PtQZDx5zU0)KG;= zLyVgCAXPxcLmAdK#1Or%$@OO5!@Q7QoH}7B#Jo@%SwE0owJ!;M5O^=Ty*QRSNbvED zACT1~JLovv*yBaYAt?U+$`qyFMopP!bC9U0W4b31D^1M0Gbn~MNkGC4hbWoRVDLZx zjIiHtLMDGpe=z*#eD7*1J4PaN61#r`X@Xfo@hS;QBrNdm4Ma zjC#iT>=TpdA2HqS6(nJ&W_El93N_j2^R(I5Fc#gfqw=x(tU5z0?2IU<_wpsVRzG#X zKu1a2`z>uCFK9Xc4#lEpm>MtY$svV6x&+AKdDtgY7W9skD!slGb2JlXaRr^5bz3JM zx92hgK#?SDNFI9oCvVfeJ51wmBc_JdJd0wpQh1~0WmVyVngxpIntEl*Sl-1!?(EvU@C}>8L zah669(G1=%{oL8XtvmLYg(XHaBpbu>B$J z5wc26qP0_qYV3G#-hcrd;9(*7q2ndcPz~dyRL@L=PKGGO^F$FcQQ!iA04glK14-hP z{&&Rsu2SYZCp0FhFxbuObuZPDjN{okH0jcD6iX!EHI2!-8pqr?SDcb=97Ne@=H$ScWS`B;&rI0V2U9@|VK&h`g{J)Z|j^=HSRmwz!$BfNU?V z3N$GxwZ^`g%Q#8VHDj_+%ROvXlEX1P4SL6J%7OM)tSxjWf!Fl_QEen0j7Hc_< z9Q|GLk@Z-eVf+IT$i*|APX%BJxi(Ivmv{OzSdF^Ng1t;JE%rP14`P#Al)aO-%ok3_YxY`HTycM@Uy4Q^*0=!U zi;!A^m;sW|jzwDm#SUE(x6idQ`pA@C)iyV#1>4O?7Bm3b`dI!Q2)PshCgE3jaM)7M zVNzNg9p+QK{d599^VpFgGd4sZi$A_>=DzD2KKp&hR>4fx)#*(hLreYSy^mu(=%1|xG)G;;@OiBue;!_v zx6YUm4;@@D4w7J4O(Fr&4EHPr+wDbhC;6W@Cccwe-!l7gb3zJ1nJYg(R#PGm5Zn$t z^j4}~y0s+Kv`N3xmfLp_R)685p%&C4&PD3=jdfudP>W5~#DwgpFxuZPX2a0m!#U++ zHWE&gI5OSE$J=YgYNy00a9nQs===_UEc!qSDVW8Dlur-J8p+b2G+sE9L%k2KlJIJU0L zu<87eXZkZVVo0Qul5m^^Y)`Wc7wpm%Pq(4930hed>Ds4lpHJe+mWlV{u~NwM+UlAw&^;+pAfNh7|L#5t5gPJl4o_bERasm;RO>TpVb@p zA9-1Dp&ld!DZY6Sizmj5X_jnvBK20Yny6R3EWO0mf3+Z!Y{)5u3ih_wR)DJ18tmyd zPypxxAm&(nj|tX^aXvK%x^moBB+>ZgONcUs%z77<)p_5G5wF^87HH`XMR!*5n(+`> zv}n3zaCj;s{oU+RLctDY=gHa3Q1v5A@63r$kA@B{Hq&bPDEczS1%biu7tKrOjXc@5 zZ{kw+>eb(FI9jOe9AUpffD3k=lOfIC^>4Ff%SmHezOGJ%v9iR6dNcwyau#c*4!e>* z9*QA4F-GEt6vP~T9;$w~ta-*)$Sntt0Gw-{QsLfGmTjrFFHqqsd6QdsT*sP;P&Pgl z^DdRDX1yt)uDoe}s3C&PQpzflus^?iXidHwrfEcLwjN9V@65mtz?FxLUwiv)wky{W z5u71RNJ=p~BVr7ev{LrYPSVdv34TsHO>g7iy4Te4!PIcB1YTyG*$$7I^Sk*Gm}m|5 ztN2&EX|SPo4F$;IhQ{7NlGi&0%V}hq&Du-#>bFy&kwX#N{_p2Q$PFd^&Ok;-d7GAc z^zlP$0UH@26S<0)2?eIB`%UOPma5{}?ei>MMKL8J z=n;M(eTqYU@wvA}i`%w~>rXJDUpuwK2n4~yJGidj2h-DVv zqrsNMKlY79YsqJO$H()6U==LMQrSaBn=%TSH^b$k=WY}Qy8U;Svhs%IS6;xF@ZMgz zG{Pz@cNOJyr`}DvRW>|*`Pw1IkwTx_@u&2Q@n5m!ms+`1&2ST6EzzPQ?X7VID8?#Yw9J?d(*Z|kLO3hMq0CJ~dbLSy2CL3v5*=7W@ChrMD`#O9qm zptZg|ZFTdEa7Bvm=wnWIi62 zHeL3)61!KgYA@Ed?*o)ZmaGYtgl6I4MPYM`K;mWlp=nK>V6rpK84|pExxgJ;ST}Z$ zGh->dKbiGq%jH|yyc>|-r>KdK=j1ocb?@3-T&PZBjioSfW*@y;CpZ?N&~gWlEzI#2NW~`&fUvz|C?`!(rNyhCY$BaQQ#~CYhy~msJTTKc7met`21rL@%OZaK(y(Vx z_@wYzs74x`cIT0};E|z0^_HWmpq6nO&BZJSv3Q5&nBWw}!Hw7?W$=&Z;+iGFY8yKb z2#SHCi1W9H?SG}cxi3+!_#{3AQT_+Qx;)&nlFa+d#ZF4}MNW?kd*2tTc+;oUe>z>RjB|^QGH`73+|*S)J(To;+dC*M7cY zKT{YS$FF}*So&IhG%y|^oM7lAYY9gwIzGCSa+pGxqdVD7xKLK>zaSS9Z0htqSm*V5T*Ww@hKP?H zK0lXKgwYk8EOO*GQ;0GXPKo*bjt#uS9ya<=r%Gk_wdJ!vJ)L}d*nbB-dA0g{nNoAo%^ZUVrdNV`B1+P z^ew$*vvqPyT>dnWt6R7R)%ZgA9zqasHln~&JF-phLM#obU>L78M!Mh z$nTG}3X%?nSt?rrWp0J*jr_{Gnrtm$*@s&_u011_e7-~K0c=Fh(ijWt82iYXrYB}m zmMWaMwF3lc7e54KfDm#5LlTpo2J_7xV13z7;#QLR?uR|)4JQ6+HtAK!Ltw{ls?Tat zLbmnyer@@WB%4wdZP&FEK2X~UKnU%`P>?u4q|$$F1<{oDe1z?ObkLGWZ1<=UR`Nd9 zOn};|?(Cl5#_<_3+qkEdr=62@VOGtpCbe(keGIOBbh6E$E_sNZ4e(lJ^4J;St~_ND zqZ(omTm+x^k7+U<$S|X3h`CkIlv`^8Wq!G`JM-T@Jnw%*ELHJo&3@X>zYt4$`|JI9 zi?Ri&3^U$8WAV78KN^TL~4T1=p`PY1j&-?J=(A2dHa=`}qpw>q1l4XkPQB|7{@F&slo$y@P6nZ%i7zMA7 z{$yUqQfpqALTVGy!*0BTgHkz_46BonjcpcUk2a(8=xWR0Nr=YR zPwD?3dv6^U<+jERD~f?4(hVxz-Hp=S4blxF<&Yu*(%l`>-3`(;pbR~93PZ;XFvRzG z&fe$j?YrOqzwf)wb&Q#taYz-ue*NtFSps1eX``h;-*TZ+?Ba;MU-9&qt4iG zb!`&6MWJiUmHS;C88a2Tuf0%y!d%b~e<-OH&U4Sfn6I0$V}T#G+Tdm8midC)=ld$X z(pQZ~&2nI&ThA_r*M`A~@b=zN%ZAwWfXUuFE;Vt@Fv{Yn`wU1Phh+t#Y;UT!r1SeP za)TZ|egG*WK!l&6gld(yN{`hzr5JkbkycO#o8H^?|3D=mFykSBqb%Mu(z|)AX>tQ3 zG1w%4qRW3RM^5Y3%!~IWh#_DD+VKN9^iWv-3*>PtA-9RkXT2FeE34ShUXJbz$k?ej z_?a^leDX%)BDmDG!~(9HWG@*;w3V*sK7U=vm*2l)fg#XRc&WFQGOA;lXyB56gyHGB zeQ}XI-99dapvde`6v@!8h>c{6-LuPT=5~!BpSk=v;2v6TfSTT63adi~pQWVo!7w23 zPCV|2Xba1m-bIdf1bp%cHg~3!2Sie}>R3{kdAD#*x)>Ziy`9Ex$#{au#``$u9gS*A z!cX8ZM}QK1_#n63_co8GnLN#D$Vs!&a|}iXq93dDF5`9c@T0=mQxdtQKW3*X#Nb+& zU#cStEIwKB3(8~*nmMTjN+sa!O%r_t$k~v5HHsE8tKrciuKJ~T{xS9TeJ7wSpst25 zk&{%QcFextwGEJRkg=Q?1rekXN9K2HsNpCKL6{ zJE4%&!PBrEftGV%AmA^|6?gGUz_hfL&s+)Wsh6nukl5WQ)zrw!=XCq+LF*4UduJzu zI z(^$D3Rxvg?k==sHGqeR~{8IqUViz{R4}ZMUKfB~m!sG$-ob9!2vDmn*DI(R)cH0SW zY5nqGc6P-KIl0t-LOCGw%_g>opC?WN;tMn8no61FUtpEBw^D=k!7&|KYr?r_o!o59>M=12Ss)gx9?( zfoz8MC#VK2Sfc8EuOrXG1s^rO{Yft+a_VX1mH|47Z@>znt2(o}j`GI<*gg~wQA$hn zuV-#~Ehh5GE;m4{I-T$00aAmM_!7-z#6z>a!iNSO&r2VcJppw0`w!G5VIObRR3t^K z{A7T<-0>XEao6N$3M_M>A1*VrCw12+sh{7VN8=1v>d*mBkBr9r>v`8L}tTtRlQrgThcB4BBR(nJTtJM@*mnlt237 zkv_w5Y?51X7SepMPy8yUu{Eor`ATriUmE$VB1=u1Z$}jyQ*G9$&+iOYW!BNi{T4*pzosiE293&Ni zIw8QKGXx?_wZklYP}8p)+G35=ZCq$_&^VmI{e zy^v%PG*Ljj1Mw~xBa1#cz9LLyY1nf^!qley1v@Cy9wzrpLLK+*C5Ek7%oDX%=eON% zvyutUgj^!qm~k0KbfR-4=5r8qch5)lD&YHkD#Z2WKd+QYxAYF`bJ&m%HX;>b5wqcL zQ{b63XO@LeeT@uy%Iw_46DnYiP~=-4}G@4!1|}-mGgMlYz=mWpWdTeeILj2&9}}C zF=_@!@<*o$BGRmK^&=lHD3_V^PaEj0$=}m!l_xnmb*$FC>}bH{1Wb1&F}S}V>g}ax zuujwa_^tRXpXm{Ek0`LC9>9~U)nFmTpi}*sr&K1hs4|qfWAKDI#(nA@Vll4d6)ul? zdAgW$GVWt~U1~1d-Hd8mrPtL!IE${r;Gv$FIN4XeI`ueAJxd=$^1;*KcII{ON~<46 zi~gG6l*yc)kQsPjneC=By6@yjv`-ZUqpw|axflqw_-T5k$K&YOSpw7H;mEVA-I6zZ z@rZ=h7hIU)lg05Uy_J>hfS22=KK|V?fFSBoZu{e7#b-ADqgB13iP{z4aWf7pbm%u! z;;>_BKvV7~@740<=@?F@rMKno_wwjhntgUtN;k z{Ywl%K&i*@LA#R6Q+{7u57E^+9|PD`h3?n)y;I%%|1OJu zD@FN{wVndb-~ZIl>s8iEhkGJqyXZ%cJ>RMy+Vp;p9!%ZUg%PSm091JkRZA<`XhMtvNt3F70u)n*&>QDLpQDyG*7$>|XKKxgeW(;N4 zO9_m*b2BUx&YlZ;fzn~}b-mu7;b&@N?L>4c-LYun)H5GGu{flQsgO~HP~pK)5nJ;Y zhN3>x<=%+dVJ44X+pM_LOMjN^l9C)FGv_ye3?(p}gy)!qFYfr6J+jiy3+x-Dknfgu zXn`5CSb3HS*?O!(S}&foCa>V6Y3IM}Cv6z5PLT)Tw`!)acXH_Ds;n0q^oLSGw91%l8!|mo zWvF!|FV<7hcuJD{_7c@TM8Wmop3(=7w!V^4bU>w>ZxlL}LBmMQ`3(&~ zsc^@b(OUgi#n1cKqB-)>uO!ClKU3Mb)6XS^uw&J6Xh0hZ>hpJ!=pO#t;QkY%B>Ehv zIDbK(C-(Ph`9H8rW`cg{95;{nM1NZze^&1TO<@S0d=$s}`{@6S32b5mlW--x`RKQ4 z{{tKa08GPBMPEMt`wRV17yse}sP;GaA$R`#=d=C_sk(y#fS3DFvZC()rK0!0U!yL7 ztLz<-eB%H3-oJWKn%-5G{`8ME_ybP^kgC_IO6Oyb1Ts=W1QT$Q# z_4ny|4#3V?@$Ljr{_}*4&;VQT!g~Z0Sg-$Z`~QjgH-VY`Pt3oslmG4J-!{(wcJp6! z*T291zgB7c$Va;N&0lJ zX`dAPHuwu07J=RLo3i<($!G39x@vQ&(`oQA^&{CXrwUCS!j;39Hc-cs|43l^O<(#S z@MLcvQoK;A`h?Z{*{uwBS)C?9_!n@^R0;gsLjCnP94wR&x`fPbn(k-Ly+d`}fwKvf z;Jhsk;574kAb<2v|KWpzx=puPkezk2T?}1{Aa&)Vu=wg#wnS`ke(0Ecc{nb|!p@{e z@;n}|h5h|H?a492bxuqqUw*;%>{?+V=k1WQw-G1M(;GpP96>6O#Gxp{MbBinQPFY3 zef{t$XR+sK8lT0Wh#dGXI5ipIY)I(~CrXCzK~q$}%9=xk-~<(I`3=?+g~s(>vgaN@ zc{ag3OTl&m8*#A;iKFSQXAO%!(_Uwj-v{a!wG1s%xEGf6I&K0}!{=y#WKYP&(lsLK zmBc5@!N%1g+mWGohPKwrgE}pv(*(eowJ2}Cyp;CH2{_&uPVPqvV)3MQ7y$G!Y)V42 z{kzq}%uWbG0k_s|q?cyus>OWsQI@}M>Jt`T7EW7-OQHI)s{j};eBN0}tw7NpQf@%} zi|69j0?3@rH|<3}#_r%56NK=nx-~c$fMsaN4$6?whLcf&*_-z1-Fm%AX~SFb-J_-V zC+HTL-aD5yZxVcOy%Ru)HjEL(k`lXaLY>XzIH&E@M24-xh}gq=Zh=|RSc1P>D}G&X zelgAP*^C;6;wOJI^0Md)CpGQ!;h9q})cyhRUi8&k&CZcbu2}(CzN)-owS|o#%JlRw zI?5=_t{FfG7VVcFsWTM9SY%j_O_hYM(+Gt&TWHffW-ze(01%1QrQl7TP7Sq(MaW=m zK@vo9&)jX1Z~m@QtZ)12amnUr%YJyV62p>iw7hS%xBYcR`*F^%mhuhzH=#m(u}b_e z)d^BXe8q4uf*lkIQn~jE3hn9pXA4zw!D%8}%^Ed`MbCf`3I7g~m~ekL{SM>T>KD;n z;&}90#W>aGdhhL8rpgUcl?n8&rj7V)g6=o74%>J1!*#STWIJ}pL&M+5b}Zw2&NppG zqCUZsbUMt`s@g*$mTq^O5aL-@T;@X(M53w%+@u_ie67 za(P7%`K3|ToiuwXtlzdH&T~K^#NhY&+(#CmFZQlJ94Zw7=cqATWy4I z3Ohjg2gC3tnx@Xn)c4BndE$7g;xF_EWn_-F8P-F;y&m}#@Z>e|3#9~Y6-LgJDfC>) z$j3PT30EhP7e3?bjV;4S8fl!+h3y7H4u!<-NcLie1vi8cB6S<$tKiNtZs==xDKy(B zay{a_*^VR+OBL!szFs}{oPsd$)H^KjJI^1{poW-KZFpY|zHnAYn2wP9js!3C{NY0W zY32FVVn)4|1A=`wf{?W@ikK>PnclGEnz5!%PTR#-)yPW6c5cgbpZ8`$KRyMjnQXs| z!Xz^_{<-K`(a}_l*zil`%402n2YY8>)QI^DW1J}yAFha=zy%J>p&DfNuCRT33vh~k zUwG#8S?0)E=h3$B^6Bh9;uv*a#biVTENU6p3SWHqNN3pkp}csy^1GTxI!?1=GW%fB zRyHbSlX1u8S=-A{<5^;!XJiS*S513}ndW9Uz7da=J$r8ZIWx&{B2%AaVIlhAl8QD5 z5BM&b2N|8SA(EoF>tnoadCQMlYv(A^%_gbNsYA+O4>g}8qIfayrDlyM0wfJqaSh4r zGItV`cLSFt_Xf~@+dty+%YaXoG+CnM$t1Wj!2z%Wi`Yq(nGZ7fj$U-9&B1tc$Bx$8 zPlOSr8ZFPs(G`ZRjCZ*hIQ1<|!+RrO@$SQ^j`pG7;zz8ryoG<>8e2`dFM2P{yTKHq zL%*5pclfk5ZEr#siV;m(>HHZlva+2<*PW(daU8G6tg-sr3Asl|e(b$Vq+-exnafTD zHehMf=FVbMafjrY&E}|p19-B%XkNyt^*tnF9`30ORT5nJ;C#Mf@G5ov$IQxi=4j#R zz-S7oe%!v}Ir8YCHAZPK$TzD{o<7-tg|a>%)=@vav+vmT<)J}1Om_Bb_Bhe zUC)LWkT$2PVKS@UdGedy9~XT)F@PDZ>)5<8N=qvGYKCii(X71nN}OdZ=|}FO2#FC$(DJsW;-Q#1BZDj0K?{XS*S1H^%sb^-0(*Wyz%Nm*C)7BiKYvpX?A zm+^b!loR>nU^{kV1r-04rHdEO#1K{MlBfZ-g#} z?%=^q6e8zMso7q)AGwryy!&D(iy%H`Z*7&?V9e%fC^PHB21opOL>t1H$AzQ#x#sV? z_=O*E^zc}wd$%|3hLkAGzMoywNMveZ+9M}+f66d_pMB0${wK2S#v(2;V%*TXE>rlu z8Bh&vt$srhxoe?Wp=-|x%j7^Wu$!(A*En5A>D`x>h<{6CTJicwwL0kWACJ;x4?80N9$mi^X+U!cnvZp+ckXmchjX`>{WN3fHb zZsXM4agob&Y~oycUe=o=HYjsfVfy>VahF?aOihzQ<9CR8Z>b2`WJgPh-IH^*Y$S=d zi#u=}?tR}YnqxKY>S^3-JdJr$)2lKp*Fgi>2dDC+`dlxD4b7DsKnq=OW$7=wjSP3I zJG|^$su5g|*M_b86hRi3ds+ANF0^tJhT|D*PlJAQ#s}V@q{JULS!SL-D|6W%&o?i* z$8p5Uv6IBEN+7T|q4ugP$pc18e14OV>Uhe}%>DDmx*$D9nE<(&`g0|T=$U4=IO@R| z3$+#R#(+xhh;cfP7^cX0~8oZ8B&?g3DGV?&T3&M zrc!m06pKCeQu1V=S|z*pW^JIFeAHKAbgW)>@6M$ucDqowGGi40BwYt`*lpMd4&$rw>A?%tr`$EONy5({HQUj`PO*0>Ebiq+JntLvR^#$Jt$}Sgy%> z-e7a;yI(780z`0}@rY~pOUOm$D@8Lg9m68Bh|GSb| zVIGGWg82H-a4J08B_DzDC!-a2uB&`2+TfCQZd1R-lkD8@12u5AqXI}yfTKvhVo%Cd zWRApp_XxV+wEG2pR63H)b-k$Iw7P~r;_y-(rDx0NDR+sTJZnd~+s@$4WakC|-6aBN zZ1MCOq=@*oCr(s;^SM#5=nT3C_Q#kf4(`>cu?R2Bwg|;zQVb4?3k=jOR%a`AXkd3- z^r~U)Qv=%%Z(B~6r-KJ+(%&w6AKY`)0zF!A+FIbFm+aQ)E_F#BH%?VeF>F$$0xZJ4 z@9em@Lyn+5cVC@B#$WtFQzb_au%Qt%-fBO{tIRK_UE3FZmlhUTWfOe@&}o~tGPo9O zCM@IW<8!B6ee5btl6`rd)F1q=(f}wuIzTV?WJ~}epB9BXUZeo^2b)HUsL`$lSQ6}b z@udMsd;VF1w^*;|l`FIiw=HovhIHx{+}ov?Dq&gNI_f7iZNEBZP?Faa z?oEUkMP0NF&%=#;uO~UJ$7jk?3~4gW<{Z-F#Ji;r8c#9?>KEKfh;%IO{ykdJ36~Ug zN@QvJsn20@&SMBc)O>?y`W9uaDlwR@8@*I()0LDO?52nEI)w zpeUdbxf&K@xClQF7ra0KbcpXt9*dl`h|ude4qIk|UKtC-t!Tr-w|LTTE6#?=1mvfC z(0S%p$B5h$rV7JKe~VDkzPVp-H)~Sfv55w|irSC|NG)AY;GX0u^cn=XYG0b^it^2N zm1RlWJ{`JOF4A}Jv5IyKc2gz4)Hiy1Gv1N<<7vn}5ib#Xa2bi0G!4hrSd+u#C@=2> z+5;(QXihcX!F&x^j-`WWHvh#gkfq@g@*uG;2Dr>{()7R0nC>OzdiGF%b-hk0sbB}z zlR~}m$0MSzjSfV*L>6f0%#k^1GXv4NfSvG-!_#VN%p0*lqvzy%#YW>Gz_~kxek&)@ zVQDKW|H%R|alvVQ95;{Q z>Q|XNiXv-i->*Vc&Y-8{uNxlPH=Z6ZiVYg3w9Yv+@y!v-5p(*9?^2@Oh*KsAFCna1 z=rpePSw!4)j@Bkd*A#jAs<*E(9weQNiA;0ORQOObEs8r_QW%27pbi~-7AIhyZi=Ig zPsdg>$FXkkaeo04ygHp&qsGUqIovb2XGj6Z4u_$DM`=|TIzfLA-79N9ns-O+ZSFM~ zMsgNN3{P!#-~Fb@iOZnLZkgulK*{nXAW~Ek<13B>>I&lFBX=sjWOT@#iVgy2ElcN8yTL+=fVF}40gbt^nB!cH)cfsxf* z>`q`_^%8cSLibAnG7o9>`rGS+Z-r-X7%mpbfzyP0ObBU~W0BN`SR8(Oz4IjZ@dK>n zaw(7@+4okRoKm~r-~Z<$FV0F|_0$KxYy+pD({G&})5tcG_{8`wsTUNVt6oWje2xLy zGBt*WyJqve{u(%J;PT@_MGO%d@ZqD(%(0#+!y{`eizF_)nNDe!6KLfM#j6rrdUe)o zL(kbhdyny2zCu^Zg^sN3L?fT0qV4gfSdpgAgixHv@W4>}&R=G$Zawb{?pZyS6f;6U zfgnn1XnE8^tSqx8BWy-(;UJj4C)DT8<@R zNSAXtrNpQszz}l&reTI@k_XDIveE;uW)?UCzNid}?m28|m zH^asFo3{nGrC*EoNt<=S@=XKJI3^~_f!Sn_XVButI%0ifhUN#Dr8CZ6RX>;{Gc19h z(MUtm==w*T{;o`*EPhQ-5N6kPfGDMZ?j4n-j7h6j{5g6{TP%4;HC_Ai(Q_{m@7_d|h`HuDvg8>q|DAb~1RVWV>UH{?sfan7Jfy~#+&xg8q|1tBW z+zI6K&I;jjWB)c>CjT$ytLd~V(M$%V)7@;8(G zUz5@4rjWHFJlOz)7nB@*2>Ti;c=it)16Z4R@&5{$+de5I`MaU^A1!bF;$y!yQI;Cj zKMUa!NEUzXz_7ae&zFe+34guIukn9*`meO>CGsQQKT9yr54illNB_B?|20XX|C{jt zQo9+4`%n6Rw3Gw#N6(gWJ_qGQCv@#`I)PC-cBl4&n2wj^>$ zkyKhPb7TEYOZmM;5tv}O#poMh_kJkrtbBGHRbl(2b0pS@ z-R<&0Rhx~LIKeQ@KF*W>E|;jL`);(xcFTpyGH2`kns>wq1I8s>kL_rN<8fzPbeW5IqDmg|KKA~z0h5y!vo^-+`> zQ~9bA;rP>Lba!!Pq8?i=uA(y3y3SYOWC-0rUWp;)_ol7vex1+Q;@%(cQdwM7yYlT+ z7+K!fZ(i5*O_bJ}qIV2oQnCZa9ic*&`e4pxpJ^({m(V+lgycW(^2^4k?8cgTDcC2R z9@SzT?N5DE)tx~=!uz*B`NP5*a6utFV5{F)x*f^-F$k6JQPK^?lT%+KpXa zN7tEEgNv7bXYL$jJDO(+xF;JxXW*H&%i$57*11xHj=_mi)wzk;wluzW1yZ#Fo7qnG z8=bmuPtW(=PoSSREEbn+M=rxt6&8p#1o0luRHp{^@S!n|B%TMEBNr+{!CnF-v-2HV z$ro}{N#XV3@k8;PxWjR$9~u@M-PI~QKs6A>cG8nZ-tVI}{O*H0ovOt%k4a;ak2n{b z@=m0;)adO1hBsi7=~(3Nt%=}O)Oy#?a#nV45=|GIrD*S?Bc!kGB5dtQjTDn%lYC!9j#tSY6%{SxQ9WyYv zo?hbUH0qR|YxXsRd4=9&o!*6kf}AH4$#`M&EY+xMsx0+UIdaoei+c}Vk}y$qZHsgp zIhC-eZd(F3^yhVaWtdJ4ag!V-nxfG11;dMX&-zDY48I(eM}BIZroQ*~B*8a)P5@aE zpMhV!FX5|$VR2?}?#WVYQL%Lou>h{EM_hl(FhA<&lLHqT;d$)oE_-X0a%}ANpzXxk z;L0ZZ?_Er_DsJTMNB&~vWO;VM_dIx?CWyOy-(JrI*}hb11m^Tq05>3zs_>R_xP!TT zrZdz7DpGkDf$xwW{Co4|Pk|P*9)}B$ zXXiL!k-j5ej&w5YLpDx+qVC0dymODIiY+7+y&g1TmwWt_qG72$(!`W(C6hpc+%4CJ?Oo9mXKg75mo z^3SIpCMsyYXx;KZ06%hxK8ms4)t%+RlFbT~gCo0fR81oSZhdn*Qarv<++^`{H za_d%uVcr&L4X{&|nup8;m9);OdoH#LoELmI@u)4eCl6UO@`&`}o&A~isCs9TigQ!p z7lT49sB`LQWg2Lu{1SezA-{NRUtq7Z(L}x;MI4KAuJ!7SWi!^%G6`LiB(%=R`>}fm zdmqVMbN%*$sc%h%(Bcau1nsFY$5ij;?G@C@sTCsTx@u@tVvjTgXfi>=^*mMt@KTZb z$?sd~!TH&^T+G5b+1nF&oAu~2j8x7G%_fW7I$Lq&LBx@^4)h7kE8G&oYJ-%Et`%8V znF~&4d5c-7kFzQ;ZYNvq`IxIvq)WlokFmXj<4fY$3tUPaM=KFu-%LyKAJnx|_Zv(c zbqLtxgSJn|?~T2@9d-1rDs!yhEtYA2wvwn^gila0dqBUC=|AF4(a!oV#NAi$y7GEN zhZNS^CQ@F-BHmRlwP1?jy-dK+HO9>&BEqg0e>wWTYer9#6Cy+#z1z_T=J_e zVcwqk=_R3zp3N_Y)32{k$DZ-X zyEIup*t{wTB7SUQbF%Fn_y9j4L#cm3gjX3y?ZQVb^Q?8t zxyHq{v2k~dyk5{{M-XxXS8Ba^hSmECPHaKG=(%`Y1+p}&EyRuEiMKx9<=Yum0&|ed z%hFmkdiYjn1Sg(d^v&Ol-jCh217RsLdpW#`DA$`TfMu__-V5vb*=1ugvz9S75_{lH zP8)(+r?;AXY%8*ti9cN7XgL{{^?lJdpPzC!oERo42 z^-YKz8FH@mIs1KTm;N$hygB0(KF|!|ZMPs~2|oDx9!)Cwu5yx@g2{^aR14z>Wyr$u zRHJfaTjllD_OUMS_BWRB)}mt*qD#7iL{?&9rW>b7`A(|nLp;rjaW0qYt#p846EwVP z9y*ub>aN4mKc6zn6f8N2W>R%F;JGUXThM&YJ%y&BxGcmZh5_o;+Ta#hJZdEq&+B561phaRZcf0TU^C z(@MMFeZB&~+Di3K-WdaZ4n@EUhHoCh))R$~G0dJ0#|s%?V28-ynGs<*_nzdwzrM4> zqSpPby#_;=ug>rF^}Xd7DDqoFKg|Xmp5ENDN=K@bo0VNu8@FBapn{&s@c7~Ee8}f= z#sHa}ANl2aU^t(btQRIn#!OXYc&~bkO|qRib?Unr|PGK{beX?;^#qIf_k~a8u zJ^?5mj$?wlZ$M*-s6Z6R84L9#fU_~Xiv-u(xA^G zX91U#1qtMcIWy=B$8V6b#tc2o8KW_8$1twpvZ!=4xkG45c##`W71#*;m*%{tApd zA>Dw`G7srA^Lq;qlRvdZrjQ7~+5aingQpg?H;<+-X0H&JZ(PJ*fHh0z))jn-R1X)( z(=pdK4|;+Oa-o0WO~2RtkpNV+F^pYVg+5n-loIsp_V5gR46c~t{m6He5T+GbFLbm$ zA)TPk5sZ`8^Qo1)mo*vIQj30=F+kZ>*!rpZ!>=jInlU&s(Jdw+Z84dzn-lR7}!+@pmH89GT?8(?PkpwtbrGe)uXmWCMMed)u)~zd(;zor(K9dOqzHaN?5%bd`F6; z5rDWefkz7;#%H_X4ZT24j3#|p_@O{?;eSk-N*%p(+D+`?cg}SWI%9-S2omqDDRP6C ztC_OXYL2WO^dWlq^3{Z#w=u)z(TPuu3@x!OZ_LZR+}`~3PYq;fKb;)gOvym@Pb7TM zz|hbln;to_7`Ka3dYVL{8`;4b{0g6Rc3{`g2Y>8j!ri%!sjZ80|EBua}*7tENI{k>z!< zVpfvhean>j?dH6kk+w?#hWO2n;HnFN@t8jObJx?^te0`7GyvsKk?j#ci_k_WMU}Sm z)jx13F!}P$1~rGjl^tPG^K@SVfXO;J3}DF*u;)I8d-aUA{>+pYM+{-FK5ZcpS@0Nu zKV@PsLJu*=KS+bLA+t}QQWoLr*&(k#qzi3P2Rz+4u#7x;@43XROX+?vrOR1lyaPSa zt9fU`+ulXSi~O13u*g+*MpJfYdfF%PzK$g_W-ZtmDq$beR-VgU{kIn9&rt{&!nTds zGh9P42_ks2Gz(d&F){~h5|cLJ->6xrv-6fF)8*^Xpn?X!h)}lY?37pxm9=L(SO)eD zI6tUft$d)FvxXfAHc7l0fyhUBGciR$+l&8D8H$B6Pu-ov9WX|4eie6N2DLQ=qMjqoKK))`t@fT_tYLHy$2I% za5SAgicv0o3qRWZ0r2Ticun{2Pf^G%#<}a><+j@V{=r;^l7N?9|4Zy0cl)qACC=qq z$YzTJ8*^|n0{t1|jkiEc9(JGpg9H6*mLt2IqtR`@HH((jYVV=Y&2$*QWomSV#zL{h zvb|JbMnwp+Ajtt&7B@1EuP3DONWSKyu!{vT1FxMxhJ)LHyT1^Yt!Jyz ztecFu^7=Z1K0dtM^RKnc1x?0SIw~1d-dr0vOs{UH6a)ono<#4ZrlFY@wN3|eGg|n! z67o3A?Sh0KoIzso>&LjC>UQTi@_&uTe=3sC>*xxcV;)R89=6_W#-CKHS!&Z~ zLayp^t@+>BjFfm!E*E$dVjLXgWb95;>Udn=oAQuAqokvCZCvcDzDya-)q;e0@buz^ zJ{Q&5h&#e^h!Hl~ASjbJSlK%yM|;zvDxY{yQORhr$C>Re>2!O*tJyr4&*Q6$8JpaX z0@Nbm8oRRr@RfU`oakIQbU>Hr03z5AqfS-E;imo9F11>;-14jHZ*K*`cBCOlcoRb^ zjataISN3W1;97e?fT;)C+%az??ngaN##DJaiwcH0(umai(|rfBnf#G2khJq=X;P#~$ za|@)?gH37XPjb31+_|gL1`CKZ=}i>Sho~TCdnj%&cT!cyt7hdg{Tiqb?W}NqWiS0k z=%z-}Crd2tiy732KCKE41}bA6g0j-tjPafC8PjLM5bQl;4|3V;y9+k3%=Y@E8;h9z zYP8aYS3^vOEpz7cpUNuhj7NKuzQ22Wl&Xy>Xwih|>tIU$ zJHs0Py;TYDmR{?}22*E)`f0q;6D@m@EX`rgVGVX%SIGWrn-cBrWG^4k2JUc`aL#A% z-mH(^S&asJc>(0qEY2NdwB)jSwAVTDLP1Idu?e)%EXhk04PUl~?_Ac2)yzL@3fvQX z>+m`=gq-i3_86<7$cq1F|Mh08YnpVup=<9d9=ZcDiHzIe>b~GR##4cLZ>Wk_C`e{f z6_qiRvVwQp)h_5f_!!Gmz&#Y-;clD(+1_BXQIvNX$APr(YqmNkgmIk>Osr1zilQ9q zq-?+7tZt9H(VBSMCOyp7yTk$!Xv@1;5cxn_ACbJ@E3_KKh-K}PaX}$QdBMv3IO@)S zKlc@0sL}TYg5*O{I_XylNZWnW#zjlRTFuUP(N!4wx-J5T5+NPq6ltqN``sfh!e4vQ z`WC)csX{3T4%bJ>46uhxPO{C4O*vPzK}Tmscpr5AC1M?GtyvXz;~g!UsmW=4RspTU z>tc7c%9QQBrN{`Ojh-gMFt_AOl(>_&k$9Tqwi6b*v@#cEUAdQ#$YCdSUlz+!w_#}| zj-PGP)hMQm_6u^V?1u2=h+NYn7QTao59y&6mah#C){DFU{S17$p98Tm>*0_o#l|r_ zTw15v+rRh$t2~-1EV7++zD~U)do;tr-_Pa>@nIalem8|p<)y73PivJyRxj|j_jTSD zzJixNV#_zFOj3y(`7VTdNgL962{ub_0D6wsfetWjo3R+>s7GBJG~bX`s+uuYD?O;9 zrekLK6o@U4-hHaoac|qenDI+j{FbX%MbKHQ7C)p)=&oMY?C`8<4Z#zwh)m>EhP&h( zg1;e$%sc#=M0TK7DAvSo>}5@Ft`NrV!`W&66Zi@7=stO3Jof_Ly`xy+0bavtsxR-q zAMleRQjgIKNVvY5^8Q!ifb{dY{p7byN49tQr9Ud8$s1b0({apn3_%+Mo8Ge#ExfMo zo9?UY)|SdY8;qWORQ}|$=r>jwf$?ncoh)1US4X)TrG`N{?0hBWqeJI_c(nBPahFA( zhFPJxk)sZ<{kMz`qpt+xW`nB>l6x1D&5p-x_}0SwJ~mStiGKF=tnv{z>F?PdcwUCJ z9!y%M#r2|JeM$89uDH5hPuFzHxN(SR-bz0r6sHSx9`$h8SqK^s`RR!XQkDLA|G%FX zWslAHL#vFs<#WSu4;^c!^%NIO!88e{D~w)lWPTMFq2S=o|{P?NHJfOBcmzCx*6t!~Rt2Y_HS6uv9uAb0@cCrOT zwZ_)*Fa)HH=>R2O)QW;mm{wHF$q?%rfB6Xeo>$)>!t`2|p)tSIhA=PgsVUpEZhp5U z1Ke}6{UBi};+|A*q`p|gYeM0X&=t+2k`1Z)15TFbE0!LOne3_@qwG3_r~OsKUbuIc z!X}4tRbAoD zOWXHuKhX2e+mtqyYj1@oV;bcCwEYZ`tHxJCWtf&H8nPY?eVPu7j*K~GFHbn-OmkfL zE1eLSW4O{Mo5R^2_2sXIzJ?UHKNa8O(A_FsNT;p~gn03ex$Di`H9mxJEV5qX%jQjS z#gLpz>8-rFcuy4T5Ow!@5OO>8V%pB2+9tKBt`C~5c5?|`*9?) zNTR#iW;vY{rAVvlY7uSgOyF6`&g$)V%F4o&dXoa!itYwlJ}ktS>Phk#dP1PKQ{9*O zQ0i|^ETKKCs3UvRahAG47*BI?y5n{h!3+$g%(}{WqCctyK3V7RhcDR!_k`q$wB)|7 z7XIPb9wzmdV_Xy^IaoAbH7xw}@CVL>ME5fKln}vuUgG*#JQ%VBagWt3c|w#7Yk1pj zSZAL)8^C-0 zscKg@J(|78c`5qNxMqBKuNQ|;{819rkDV-}s+SniOeNF6%J{}O2KRkM=dIrJo9L_; zXU7nD#}R|zfgU5H)0UoZlO`_2eyKIP4pvqBh>_yvTdUbXZQYK@f(>Er6{mgp5y7)K zrk(T-%T-SPMoG;K&cQYgzvwjH&_`zUy)%cb z5ZY@+;tRwGA(!`}g(=6m>*l44Mp(AVg07}&cYlRj83i7>HVq_(o>-Ik>UeCUeA{LD z;tVXyMM{%v*YJV1f!Ri*w-85w3QzSUu^_#parA76C|c;`%w?@AG)04xEOcEobcaeX z*O%X;XlJtaQwbI2+<4&=2$1RrGa=egN(re_r5gw~w9bswnah!AO z)IL2+FD8xz8@3I9VEl5=Ku=4vach44%=pO!vZ2;hz$}3D`PQYoH^0tGvbN})`?prU zQl>eNl~%sV4f~NB?C|6VE_WDwf74fwP?9Ct>aE8xb0^V7UGXGhrtfqf-@CjoN&*ef8RUdxP$VpoBMtsGnNDNcjR_cSo{7D!Z-q2SfEoM9!w!X$B-6yE` zG`n14!R+bB%?-DznM;#wIxT@qc`!p#SV9j+sp`>HvCT$~%~`Wh`4QQrR|?Swcc}mk zfpm5aoP@E#VjpVI1{_X@W4cm4oo5bJUqA~ms1ek{LO7^Ncwa+Jrq?Qbc=RUmI$Rvk z8044Q#rC-%T)WUF5B6Sdqhwi2Rjb^?)Ws_`Z#Q53$iOHrjL)J+!3VmG z@==y~?`5FHJF!N1jnw{KyZH#3LZt(%O!HwZZOul(K0iv#OVC zUBv*Qmf+aQX_tZ2SkL>)$XdQwlc#702Q~?+*U)@3uggr1od_lBB2ehUHF=JRjD2s? zbk38|jf8SjrP)nLn&8H`6fUotWr&y_)~=%$M}!;vEeZW?{7Un_C; zY#@b3VnrAwMU!d2=x3+s>8hnvaXUM1MT~aDx5&x7uTa5<)3o)z$YNpHF!EQ3{Arg1$C3I#2*YDBWfKA?;7eMmJiI|d|%}fb!8~2pd~UP#etse%~n@m zdd-M?Dpz}-Z@~@^$BIGe;|aYGi=!oO%O6u_POmP0R2bl%<%qF82+*093M%$wu`MOa z)RM==yHy+J-qYgH+*22cG_q!VAiT!RDsRe6qj-_M>>P{mO)HQD&lT>d)0fuo3FRA- ze`iltDsN|^C&V>PvhfI*m(xNN<~C2Tn9`;4JWfq!XCVLh^=a_{U{#caM{LDzwX!+vTL5G_uP$)4@@grzM<%`q>IZZ)yU z#wxgk6|ogXz8f0VDQ^?ER$WE@C_}u_H!Rz!o%9_8@zX$qZ5>e=y*Z_#?gt+G0 zMQzJ1&1A8UPP_&qX322Pa>TxC*b(TjhQr7E^7q-pP3&ek5k;>qdjuxva^1b{S6=VB zCX<ws?m7y<+4~JFvu|Hj?0Wl?CSP%pW^PJQ$A}sBT z@`;+4J3nCnn{^sOvKs>`s;3P6WiAZwf*>VFo(K`X>y2?4r^gZoE+te!vLB}m>Qc!_OkC^y);ZL z>Fq#tMrn~r)&O&(BFY-$am#PBUbV}vJ2#KG~d-*VFG)T&H{}}khR)*aCf7p8q zr#8E-eYnM~xH}Yz7b)&iiaQBz0ZM`5?gff86e&fDyF<|6QrwDLaW4?u9e!z_^Snpq zedqiBfHT7|gk-YiUVH5&*SZ#j@3~T3ROS4Pd)-{M4*CF>E?<5;JPfi}X>UDMUgC9( zNHubJ;iQ)&G;USA+`NB0Kr&>-0(ZOf{T#(E>Shx&S*SV{{#EX1wU~SSF+Iui?1{xv zbS9f1^OczVO`!#rD=lKXdRomO76R4kHlHWf5776~Q`Zc*RG%4*)4k@$J_7g0BNR zS!dDl3c1iA*##yKclF5Ecmxr&_q}UM*a?-#^tu*wR>Ai?s;S$k?=6QGA^B%$too)~zxa=Jgjrvk^Nw9L(nbkSD z3Yoq2T;JLg57JW3v||!EE|r<2gtmW`pkZT#2R?MNxUmBnrq68qSpyst@uxLB$HHac z?o$@dt*O(*_ap@uRu*8xJ>WC1uE%(V%p)PD&zr9~lU~j}bUxVp{NZLZS`SFqQqoX` z2kp9Mo!y*NO}ez-|5!ENiW@Pf{l@0AF@!%bf?jS?EKb?-+Upt+EIYPOGOe+1F*o7a zX=BTsQ7?xwjS-%0apN90t3#9@nqO;@H{Y#a6u~Tu`cV0!9``Bi_~Vsl$E1Y(gRfAe z*7pf_{c5dyF0V*(u~~zpNZ$LrC;|7PtO3H|;EYzJ62qw5wpLhE zf63IX3<>FOidyM1HkHU`v2Xti3?ycpa+2;HHLaOqIszcs=~#S+>;8P#u}AKu7ELAX zo^IV|a#qC)hn_O^`C6(;y+-@hxW_8s7FpVU6C*~@CxcdFvuy&JminwSZot%~b6LQh zZ3vSLa|Gw1VW<8kE-*^06M8V3#+ydrdY-((Fb+B^Wb>@=f zeJywdq+Cz}DLkk&E^i032f`iN zc*a#DJ_WIbTf5$NHvP z{N>0=O+sUIqxZ#;^KG*gJR7^jdWi}Q6=NN~JPnVnEM17O#M}a?EB}Ydt%;!hSyu7$ zu>$QIgqfE6#AK*r1b1Tp;Tfig?C`g5sQ>n0{?^sL1S?yBfusHibdP8y1LqEsA#eV)_^GcHA!iRfC zTOD5g^?L?2_C?xj@%pB(#emwFSKt0k@BGhznqQt2Xy(%!zV&k_xO(nHo<|ZgKon6} za3i2GRN{n@N5Vw?ua4;7`!Y%EI863svhSN$?DtvKm8O2%$)DFLlQ(v#Q-Q&~95m)$ zr<>|TiX_@ZiW^!+7RUpbGAJzV`kAP#htY|^cpqq z;iY0uUN#1G`|7&_@1q7AtH*zT!m$?rXRTj>KAa6_vLN8s4>G9tkT=xla;fmtRQk_m z1zs`4@QIh~mru(@QEO#j-MvY>zWh88%NWskTKkmMfd5ddN9H&UM_i{sEe&`Mo7OaH zsyWs9sYi-gDt@vngA4k&rc=eCNRHY|-Hx=D+6C+ZrAi#NULYTf=`Fg(Dp?dzj#lRxBG56A~A8pe| zETUH}HNW-tSf?Zz-p)*8#>h{v3-GYAUOt)4$X4oFx8Rs(GhXlC<;C^%k0?m`;vY@p zcW()W*oe7I`$Chf(PjA#*NaeN)FSdWYtP@b%!7H8BM;>-Coit6D_d; zAsWs~Ma^EPh0c~rd5UCG7UYoh2x7)FbY|P-2gK>bOHel$Ny9aHgWF>%TV2RyixbHqeCHJ4~0i5JT(CIj_oyNrj&6BbBB!9=k}FWaKZ;m z^-aZ1KosSy)~H%ZV=nWzrx2v(^lq15FR}8H1OgN=y1!+(H(AUUt5a7qO0f1w3)aj0 z7VD&R$sWoOl#hOE8$12Z$z0any4|`}NMtL$0?QAj{9tD+aAowe4-4XCH~=n}^3M<) zT5pr$?VzmjlfjpZ$>g#~u~zlo{0>X%k>t0D@HX~ei;H!qyT58;eHcDmP6Nw<= z2t~B!afM}8MX3Jx#@S}vK*(A+tOKkE?}g!b)~+wP>cn~jc#9Y^8J%3)YBLBL~~%}gmI8En~s?{tE` zY;_cpAm+n=zMzFu9I#E@R75riFYC+C*ALdhY#naw5(!$&Ua4?dExPLM)b9hHa^B?=SWi3tlLv zLip`wZLS+TFb#`hy8-jlLx4Q+W@@)VTbkRurP8dLI%q<$o?*$_+G9~GJFiB$F-N?B znmyZ)hF(1=$x!C5%&1L)h(l8s5YOVj_-q)9_lPtx!n)%2wofUe%TL^D)-zZz%0bgxSKD;l-{<^+^z4i#{jH_wnm0z-` zTmD?Dd7?1uOzr!fSd7iJU!4sEE{ zF2CFBgDLtoCG_)B<9$&;rSz;t<@2I~mX$VRrsURJ9mnp z7;4#07|D{6oBXVIYQb3J25bvzg=%)6U}$-tY^lwX%%2MS-P9zvnHE7EyY=-x?QisJ zIhPY_@)ZLr59%BvhJK!QwBFBoiRGm3fUF}b){GfiSu0}~LL&{dfqKlSe)zt(yoDwr z-7e;jfx)muvz$^^Pos=k7h-vu#FfK?8(U5_r$eC;v(;=%o8jh=eqS`zWF8s2#o9$M zoK&3fMxr27Y;CUWYq!kGBbs>sKs-wsS6PWTpU@qjJlmHd`2CTYLI-9-}rFrMI%` zasp^L;jv>~zcTFG)q+*J0Oz3;exBA|I)8SzPrtzgT(14-jeZ z>rZJds11we?98OzzbQQw8WWFLya|<`FT1~Xwq8!}@ezFP=xi+V@Z!(yA08>0ul?C< zioBUplKc}7k`|M#wuG87`HcQ>=CP(05Ct}dVf7zt6NsgwY@!&9TVU2gQ*j1iu9 z|G3l9376A=@-CCHW8zDbqPN4wnK%aG&yLn*6G-Hblrmh}WzI4}qI8921Q=#D6_ z#(wvBVH*`l5=U_Ty8q(t6;2UB?_({HXz~&l<0Hu86EpRS&-$m{13X64K#TM5IFptu zB*7dRp2NZQ0;3lO;8^PqSeo}}DCdnsGI|?%q!iq+=op7D2}3DY{o;hE10OFX9>!rb z(t%Zo$msPyVxRKgd~rlT?ycr*P0EgZzf`4?b)OX_@T`7ZlMB}Dj{eFoN*HSZn<~Mv zjnPC8bB2Iz3nnY=Dr}hJg_%&eoXcVCWX-9>ogFPyXK#nTFU^_n)QSb;Y^j$kaGs~oZjb}@{Bp^1Gqzdh0&5IUwY$~bp_{2V$d2Q$^Wpxfqq3K6p?3FGE z7ybSV$GYUP;h0tF3$F0Cxz#1S^Y0xSkomBf)Mb5Z%h;6|=T~{lwbQm&$aBQR=WEr`uX8DDOhn49;$Cu2*M>`N`_T9m zjsDsPi3z;6#;|p$LA;fV-=FsgX%X+kyezXHnK5?L)9wDrb1k1a9}mGKUB^MlOElsR z^+&{be}?DY?iYS>d7D8nb?>qK-jB432B!|kl5&nqyhgi3U$5nMm83t`6fY)YBX}l& zrghLe+H1coT6)Bj3NJ$Bj*;zb(QW=bf#-GDt{tzRTg@S({Uun(Bxm>Ca?gpq*@)bs zT9UX(x1TvjcqKpGo1$38&)`a382-Lb6vU&xj3uDO5zAM6$!YieO$2M5?Ph_A%LT<) z>sHqdyZbH$ySqt=L&5hCXBfQa&t6~O{a z585qP15MMCRMUzTK{a(!UDp{h*PqS`RMUX)4!jCH@YH-()CEr_Ejj<4hf(ep@qn#e zQh2+B@#x}4IJ4{-Yd{50v0e_?fOm$gd9>}MCkzgtVm2^fm!i8mEzdEU*p+M^HUD6v zbGAahy1*yl+Y$hOxL=?3!C4_G8fV;$S0IPyH*U7Dl~LlmeMRm`f*FMw62(v{P?E5W zZWB7Y^`4zVVgZMG42mM-?FZ3h@5cB@F2T((9;Ipo8eROgR1Fd|abZC{JygoamY1S% zWS3Va=QIK~jo(G2^eviZP$P*(fxyx=Z!V#X!OXtVjK`=)8*|+j5m424ElKAql(7ct z?Pl}wM%3})^{|BgvGdGWp(F&Zt_`oy62JJOrU^*vF~okeT*t;2Ae`&ao#sI zGE-_eh;W`!-54sXUYQpusUHg6P^KDKJv6HP%xLpe6@PgjdY_Xx%v9*lF?X4oncjWl zd&ZyaUNX^WH<9wyAdU3RANvv)D^S^rJM<}Q=?hxVq>m>E;}ueNyOtE35fg8R0Xk>9 zX?tunL@Yl7NKMUf%Q(8d-4C-XY{>F8q%RKU%a?ACly+@7jcvlBuVl{m{Q_+vq*$i7 zo)hOfxfLG+*LEty4r<0*u1=b=04#3bY(qo8!+ZS|(*#hPQKhkgpKmFOsmhY(o39{u zKZGnn)NSVq18o&ge6NciTFwbDP?%@Apc+as%G|hJd~NFzGa;?x z>xOTlsF}2K@CiZY58pDqT@NWW>9@VV)yh9!+#<@%P;x%bM06YW$0{H?-p+KwJE%|) zy&9XwnV^<<6b__=eLC%QCFiJ+_r5PvD@`=gWhds)XGpRPjtVPlRgIQ#){!?u#{5cR z;x!^1+Dxlnh+SWsG4+O<$n1;qW?<~oAnW2Q`c#uRVSSWGF72!#nM~g1-HWFQ&e}Mc zz=$eRFm~V7rwI#IM*&-jg#ftAq20#7nzuqT%0Y!0nUqjGLgv=8 z8Om0+GYUsCB53gYi#n?FL@-zxzk5#Of8!?a6c#8}%F3-CnyGg4_9!NXp7L6Z1?Yi` z)ph3QH%Jw&YZlM=qlR_!x8qwhv88*=^{(v_lJa#?tzvuP^7aws@x7jyX=53&vc~n^ z;Nh8?^i>|Cbz6$O^e#)-Z-K}y?7xAmliH%>qwnAW6B$%;xRwG>f^ z6X&g-E9(1bN5?2~@Lah(E$-^+);vvUkvGrOtT=~5pmN6K2;D-7V8A_P$7G|2#NC4C z1&9`_n>^oJj# zt(Ie}!;+(BSC^IwhsVHCxhIBgsE7AzJZ$=@JpozgSP z<$CLqD3BdA8&3i@qo&!%5Wyh9W!DcO>~v}#oi9MJQ_ybSu912rOSLkgj2Xuu#<>S# zv34c(m6xSY9IUAk25~$=ZuiR$uVCwrQM<$x4n={Q?lF!{;$UEN_;uH%8nJ{r>dt?U zPEW}2C&Q~cnlQWIKykV#@B7(S;}ES*=quanD!2H8OE)B-_M*0(FMoPA&u>1V75^MPQ?|!$aR( zUyoi&Q)UUh-&2{qa^37&A<5o_AN>6idW3 zuk6+lyKdRrNS-D1{n)RUZF}^C2-t1h^^bmgNPCSKcg0dpnPr=}=m|7+i_R*{4?QXd z(zlfdNPe8zOKM{74uy}B_n9m8igWa(Pv|_;+XeECe)aLnaNKus<^sWVScvTg_VtSS zAAkw9j;+P^yEty-Z3YvsSi@@tP2%=(&jh~Nm&Ak6g2(UOcP26|4mfEl_YxWe)0e7D z&0^2p)RKmGQ}zAfA-0X#8A-SI%dANV^yA)_2eW+e3s0Xp+<|zr#fXbfm8}~W`|*Q% zoXzY@;t_X+ver3cl#Ad!+ib5o$7!a*<*`Y|?Muq7hC6=P$Y_(bfd<%!V`kZ3VwLL1 z{W>!r8N#uL@7hK@+yfH5=Xw(E&5m12_G#rWmfWY1}}%JqT`i4i6Crh zjq=^LWBLA%*Mo#uOAG8eF~q?wnBAO##DcUQ1kHV*l~`g>hN?A9jLg$~BFp=is#S#_ zo?CD*soqgl6}D?X`?`rTJYQ5X=EYknB<;bP7i0l`3QU_*n6_#3eV@XwdILJphW%U@ zy3EoI*4#c2u~%1<@HeS?S}z7M4#b(JPA<9^yvQk{VYjVud&Z8qP)ZUb!2TS%hTfKz z@BuB@loq>0pg_Ach6_#3Q-w_4+;Kk+Of(P@bn)V5aqKDS=XQlVTTZbr$~*bd<_7iZ z;~+f$cp^vfsGV=Px=-5IMqi?0Q)58Yj+|II_MOshD?-O7t}{Qrdya@95a|3v{K&N@ zaNU)|5hX+R4z&?IA#f(UZ1T)OV*dJ+fZYs_1Q@Nqy009;pUrX3KS=*fe}!GQj*&T9 zS$tmDmX57OKl(^Y4CDa-5?taq*_P1tD$@v z$E<{%>R+%$3j^lPtjSKV*KF=eoM~Z;&e$V_w`XQlz2ZcUm+2cc+?plj)0XBG7A6Gy zO>kF1YG2MEk#^Z-VOUlzMXoJ6Bxxbnw_!ZNlJ(|%x4c1=ka63eBl(X7ll4_GB<=He z79!OMz1TnD@1JN4EsL`V>u#4T?VVLsQ!h$xt~%Za2)(BD#gXEE5>Y7&YRFV+y-(#d zUV=hNQC3t-iZ{Gc(eD>(?Fve=GvjoHbK^}z#03#TCC##RhHBWpSAa+B(X~or2QCf=N%+rzu|YIbXV-1(6GsdeJ$cDvGC_~#lKgO(x5*LRP0f>GAbD$(+Z zr5oP1Fn`K90F=yx=12tEx4&h2z>vOe?mVfV%G9XhSDB;#6v>ro5 zxH}ky#A9DT7aQH=n5lsv-cNay@l!0^Eh8$^ka=8;P!lO-OXsfv4+{FF<_2qu9`?t| zX14D)C9Ta^QFmM{M(p)|*^w|U>GLzZ>F6Z#2R69(Bgj981w`O&gQ!)s3#h5O|;xu zR;Rmp*}Fit*{1dZfhMH_<|d$rj6d-9YlrZ-w^YBGrp+4Zjj3{h`_~OWxAEI>1H+HV zQ#!kMa8=cO;55+`0+Ab?e+R=qYsxuIJrd9H3pLwR&`OaM)kjtvv69(OL_8y2*YVusL&y!LC)avOE3Qa+ChB)3C^@6eIv( zAyV8*cY#)qvD5Y=Zeu1Og+EY(*{2-KYlaSTX~nadKaPqWQ`=B z;sXv2oyf>dPqW?BEujqYCJS#jew^=+>)l|f?oyj<9%Z#$9T$w2(~{V)!}Ky6$W2@k zwyCV1GW+><2>%C1v~|6zR7w)dwAR;xVa=3o)F_(Gq*pj~%N0K-5ZlR|N9KskFLvTN zk{Pd7od!EOehe3;`l?*Ug1Ubw&UDJf5|;VjhH&ubrRp?XnCpU#BNo-Z!Mjjq->>LP z&00Dr*#lqL?*wB{l$rueGpb-f?71e$>C@}&Kx8;RPUneW^lSmXP_*o+!3?r>a6QQv zocv`Ug`IK6Qh*g6YOo3O0BxDY91n216I_E^x^^|27+mJ(XhToK&6PKBfVLz|+8$s$ zQm8vmUmSQ#>c4lENgcHZedVL;Sc|)9H%J_bpmiqe2s6u(HVd&Xp7UUgt2^2clc=#{ z5Bd-iut11s_Db!uski-&3Jo|h)DQY{!oR$0Zszsr!sI!cH-(DP-bfPd5>?uk>*$3& z`7_&FC`PtPM7#$32#6_#Hoyu=RVp3ll>SeEOkRQZn}+j%?*wC4D>)+yhAzsI(u+P= zygwYcrb!w}DdA6_Y!R5RP~ll*r?(xfMzO6a5{h@i$Ojzekr@eM&ZJFbP(y`zP5jasMbD12@@l`BL()b4IPc z2h&f5)ov5l2x?t#JZxXNM4%y#%XisBf1UG0i@JnoaDDxjKb*Yw{O%jG5Pkf+Tl`XKR0*q#+PLR( zQSuQu46Sc19ZK>)SK{Xr`z*TY>O1Vap}~y1+>Y^mnnbTJi()`ne!`R&kKFcEcPdq- zFjsLOi{w|tWbJUygC=q6E`E4~M|dUaWE?ZPIIHtbNztocFH(Y{wgevD^wA?-F8e8A zn0QO2%9MJUCbO1~2T*bI^QKXFWk&~XzI+h=6wS^2#Hob`ocq?KY-G&8Jok5GCU$9l z(to6KE8~@h6Lr7Pd^telUEuPH)FLEW`i7?w!6mv-1-X;Q$M$ z-R;X996rjrTZig`&dSLI4+(i)IFDkGTvq1$b z&x7Nkc{i``s4lZOJv5e@4WTnpXY}5q)K0@hiTMG7kNJ!3{D7mxgr&h)cE=&Y_6 z-I>Nu@|9i)et`Pf>htfJhyIn|57H7j%su1TPuc!J2D!au-&&fWJA$#IJO!FEsdpeieHbGXgU zOQk$>TDzO-a&jT-0j#tiE|?{l`vGPVS8C3XC@VEAt7fg&8e!HEQs~2Ys>?YV{2`+is3kgwfS*8sMy8(72nh#wLOg?^xbqHV8`j^Ov0uG zUJy&i-tWDln=Y5mDeap@^ zBldBACxBk+ZbpC#`gMlQlp#ckK1crKcbiA%n+0U0N%3F+X&k{(h(&DZT8i3WSN|)x zDaVmtA9?ShDgWsky)@{%Q)kWo&3PCmi>16SIlCw)!w*hfafn zO(njTu%M|ZdcLp=>$ft|lJO_oAx90&_a-Okdn!`0{N};zv#?SAv>7f=JGg|*j8)%e zAtJ~Baj@@+ouQK5b%hzpg3Ef38r1jATxTU5RP}Iw^D#)Ld=_^O$A$^6ZLMaLp%jzh z8tbuojj)?`fw}IMrrnGL#)JnmkNQ?kVsR&eE4--*_r<8&XRlPqLPAK1AKTVHVeq>@PAsHN>YfgfGeYx6t}v^&#T5=j^x@DTe2Z1Xq+dS6&?Pao&Lx-k} zG?kQQj?j+H*%~KAHVosp=b01MdOC8cD7Jxa*ho znt+Qo_0&v{qw)C~CowjIxbp`&6TRWdv`wrBGhKO&j~8X}^~0V;$4$yRv03j`R5yG3 zX=%iRL3n}$GbaOcU%mc)Cg*vG}0MdqGBntF0U9{2VWZU7(6gSis%2fKHy! zfdG~wJP}ot?zO6-cJDt&N0o4C9xK=vX?6`)=r-f6)!iS`_NGe`@>4WnNc-*p^f~ zW&Z{BAVXkT#pZI@xAVTXNkX^Uy~Xy-%Gg)S@3@VI-RjmJ9By8PU)=h#-ULkeaywZ~ z9~EUvC(MTvy|`S#PW<|mQDv;Q{3WGpL(zTugx*_)ch}ydY7p?xthOkPk`42IRXvUQ zgXmX$H+)7UZS!!cSslQfv2PiN3r!)-O}vAJ@9`>=NkI{yl!W3Bnk(>!G_96$UaFv% z_qKP<*Z9mkG~nlP#OQty-&>sz-^R@uE1s|7@)xK|pmKM>I#;GJ)D^{}lx<2IZLv@Va zvKVmb_GBJ5Jo*@%?=9&OB@peyyJzM1M9@$u`-Q9^p9cEab6duydnp5yjoxn@AN!^?vnolRZK&=LsE&OGfU5lHoh0z*8 zUAqR38x5TU+0ucqhxO~kB|JtTq8t&BZoj2!(@0s8eIjo_pz7?Yr+ z`p7R3<4&C8xoc0|<4abpV273+oAcxEQoy6z?9Y314QZPN=uatRuO;^CxU<9wduwoB z`-tyAGsrL3W|kjvr%MgW$-`x7A;-V*5DV8z@MNUV;P4YLonKp4S@Yc-Du(!#jdnQ* zIvWy^s6ZVdy<@r?ul2r9UzuIl6{hSd1Nh3C};B&XzC)cW=xY&Nf$VA z#DucLtOb<&Ol`HF!_ip)qG||$w}ts!8rouLc5ZloT;$X#Ty$a9Uo46A2zM%Py*Sac z%RjA5W`n`4d$$?*vDml!9>+u;@|KAq3i@Co=UlC+7SsY>hDL;AYyQ8xoOMag$79epeB^uEgzh1EDz4--C@cw9`JZlSaNy@}$ z2fdv2l=Lqo5E9awF?T4DSB>VAt^Juq#nZRXWWbe4F2uzp-Bw>{mL+u}9%Jrrar8OK zDl~65XT~7)X0uLPc8G@2viRZc_=KLG{h>I|?|F=?-vDfj-!XV)sd~;%Ot55He1Gzt zt7xNyyfQwRiA_-I$V}(?61*l|p7}01`2Nir)A%7h({N4Exk#T6p5op#M>4bEyWUCQ{^hu_hoEoke!}ja z)bzs#Th6+;0#B5zp$_R$m1M;xH90ZE2(iH$n%q~S#{NsVs!ab-Q$+<;J{N(f7L)|1nig$ual>M8hN^{ z)$jmwXgW!{m!P&zTHx|dhH&c=C^NVF;7~5iETY^-|9c;&R@#ekJN;LYHU7Hi&wwxO zDKkHM?`4T>%2tDNXWtOgfa4y;ofCEAC!JMLF=N9H+xTECabH?>SCPkFR#bT39N!ww ziynM^I&((AR`>FPL1SOQHi3&um&x8TCApxJevzMOkiGOo7YH16#ULbj^t*ysh+im| zHoDrBK!}hZ z$1!vk=ft=<+r(k2$*L~qP~cXf!)BAL0ekr3gJ6YC2e($h2Y1X?fe6N&qL#Tqu)9P; z?N9Ce^Cgd&p&oHeMaBi3IncPVTY@5K`Y@j!maCPI-kEByP5#4IUmo1qCgS9#wYN*|TNB|ik#EB+ zeU84$HXPMY_mpS)e|*Sfgf6?pJf_uQSCo?MZ)J}lf(x1)+S>KIfnl?CQWv%tzRzcI zV0!OaCmfpi^5i_dT^b8}ud{0BT5Nl@_m^7)kN&78i#G^c;SSMp!(u$^N%}0@4(){c-iN^<5h4 z9lAKZ7G^MEX=;O3zPsgM!ccMD;<7r!=68JC@;fsyALJPdqy2PJzG!9DO1juB5#bZv z<6(v?P_xe^b>)LF#Cda|2u#GWMz)-UGcXIaEuTtIS7SD$Zvgu5?y&1T_N-ftr28dI z5gf2gKHV4Mix+BqKSAt2vUGohxqGykXiS*gdO$2u=z|xPDbQE*?RW1rm)s9-RL;MC ztO6~-TxW>rBUy;7Cat2`?^p?rd2b1m-r(UX8Hl=WhtF|L&^kSAzse+Zp;%}FVD6Rr z011mjHqEtpF`eo+OFBoHzBg7?T%mV%pr{6RqrjqWseoIXgYl}rI{V2ap5J3Q z1-4SRm^}Vmv_-I8fO)RO$mtNE_771RPKe)8hegnM5khzMbs zl@K2-_6bn&qn0NcJ6zAcqqMmA0sr8?!P0~A?f?8vN|AJylS;ZZ^m2#)&*{y|WYGldK`tFIYiaX5 z-^O}pvD)^vmW->gy~6j`D>-V9B>HzxoK|Rxg4&}~Sa3rB0}9_o14qEG9VS=yFgQ-* z>b8QQPSpJd*q_<>^+MF$j~v`4KCmHP>JYJh5c!|k{PhP}=o2DyvR|4N z^zhfJ2tv*q>QEaXG?j>iJ;uZr%*O+PZ7UBhGvJ~J76VxIB1lZ## zWWnztCNO(#{?9Sqz-Rg2jd+6I_1}&7OYr`?5&t-^|J{iHZp5D?_dgZ#e|5y)I)c{r zeScE&Ux}hu1cLr2G|=~h{%$LM%f*UCcg50`zDd`vh(dMGny^ctBG^UJUM_{nmY79` zv(-qkoUi_vEq^iG12n{gq|Z@5?lX1?{fVG{SrPs{-thpg?A8qShMu{MWNG zz*4HO@5fAr14>bMMX{r9Z9cby{L!rDYv;m52b&z>88!lpb?kq?Z6$l?eitr&)=HZ! z>#y~bn;Myz3E|jC#-0MSi_UiTn18H}_}=X1Gda0AZ=}}fm&Q|U9MS>l%}HR^ z@@V&YQEj-Hd#YQ#o(VhuS+4N3wgpngR^st`Rz^W}M1-Cw;UN~=65qzU3zH0qpu{VcDjEn0q=SN{~i#QstK(?8$4Te-|Dk(q}2VNOf8&ef)a{uz? zBbEQ#a>B(7tXAaacO4)%DfK5pMk6wBRp-V>w;)BRp5AjVQ!=g$FJkmhAAa*Gq(9J{ zN$d+%+`8=P*4bwlekxIV3{uWnua){4GwSDo$}%fQ$67Ym)Ax`qtwwcfK&?FWj5!v= zA5r>Udl`5}?>Noe180*hcRvu_i#YADoS%XO_X3U)8hpF0;6-BaI^PfIt1~TvJ-VMH zmg`@~v-r9{>jA-8qmEw$F0-?C$4<1L(uZ9cRo|Ld_ZDi2yQo2HGuSRi>=D$#NjK)Y zJ1YL?vDFl~Bk6ye=Cqx7g0{cfKc`l6(_cN`dP6Lc{evtCZsDt-r;6UIB`1*Jz-cna z3-n|G4IsQ3Ew44$FUYEu&&n&FmPwUj;LBof{-PE4a5Dx+AfBL^LEn0*uv0qw-{LG8 z`oa;Y`?=j2w7oHfXB2KyP_Bb0JQdFT?D=ooPVz0?lSeZN@o|+F0h`L?`gbM5j@$Lc zz}E1_^C11=ysO;oT!zsCFWJi6!7OpIYUQ%oW;G(hfOx9iL^$0-p;Ewm7ANkHe=p{b zx50xHSS`)XA8=3YW*T7eU<8EBmeuZwDVmUyWhYhP!_VrD6m7KfGvBkd&}kFzIGSk7 z+9Ey0)#$%M9Df_a47fB%!{DO0wtnaJvA%F2ifn}*-v85TmZY69_$Zku9G zo57r5ao7jWlfAza^FQW?dafJKtS8nMmnt#|KZ<+Zm>_3Zolfs-z-!KdK3!u04QpP2 zGUpH?O5?ghV`;c2H^fIH5y)0ctxk~mUTgk>O3UDqTBGTLMytWnvUOb6dC~oWhC17I6C#wFJ zrZT1Lt={ou2}c%HmKZYKZFIV}j8&ZvaP;3A-SK4SLcH`+`5gI&AvsMeGxC>E7bv<#X{62+aMM6t2oVOFNkCMl}A`jf$zyeAnYX>Ww^fPSA^nYLn;Z57DQFI1WGS)yig_)B$+iQ8d@K$3VV~ z=uL=5mSm8$d0&kiEdJ1{pm0K%?4xpOyy+}Z9i_uzVSIY#yAjC?bL{h7IJe!d6Wb(s zs>#){YF~Q3yucEyy^K9I^~|Wa#F>W{-W+lsyExiE{g8Y!Rf$3$T*-aPU&7RuoUqmm z95nT8&b?ck2`ToOKb;($snwqC_Q2bSV@At(w1(VRzgK+wZ9sS&oTpum4VWUhPcFO@ zj}O88xEYp{P}q;6B(-m6AGh(?co)z7MQw4uF6%eL+<>$F9(k4QxP2>4HE-nwivS5< z88ZgXp~2y~$uG7VbO9D4a&XU2Li4cb3{6fYvbMVhdRaVE30`i(nZ523gDdLMg1#Tb zAD+S~J74N_GL^X-H8|vP9JrV$Yf*KZOtg?p=}c|-E?RtL7E#$BCkvR&-559?=3w($ zGLd`jbvb3iHd&`>QfK{hm!Bd2Xxal#yQ=Ya+1(6Yz-~O7izt3^w@@dX($1Qtvg+@7 z7W<~yaf$Lq{T&4ZLHRAU>b0Es;&W}tJT_tGV@ieQGvX+p*Q@(+Zunfi#Nksg2w_{) zjA9$%!Grur>ojnau*pQxq*modNucQEz=Pwy`S9GL&B{;9EUUm*Gz%&E#&3&t_3oD8Xc!^-;$fvC^M%y&0Z-n@0Pys7tRxI z_F0G{pp%1tco=8B3aaX3P!C^l_UDN2lu_XB$TZ~M+#!l8&s7_UNKMSf-r>e=JC3h} zb<-VSh**(%9=C3GqQfeqnaJWn!@L1X4EA37U3<96 zqsUicl(IHk^b}JWhn1c3vUJB(cnto8(**f8ZantXvv#Kvb@J`r-!VfE!}S{Wl-DnO z7P2{tFSR<$b|Y)Ikjay;{0V{;IGFOc7L9$*V$Rjuj2bvCjv1dVMzWV30Oie@Lfr9qVBp!>oC(IK6 zA7|$n9a*>a`;Lu{jgHk($Lb^<+h)bK(XnmYwrzLpRLAbvy7ioMo^#H9-}~u)+GEtH zjhbuLn%Haof4{$Cv)33^VJu0|2T8M7ooLSIh4xc1ZvI(s7cu1(FZK;m(mdz1lg7X% z!-S@rg-yh0>3J%7R<6Xzk2`0Px#`J~zRz-6>SwSB)lN_gnN~wcv3|%x9`DB zA>e&Kmo6B}rpe+XNCr!p_yo5E0a~i-)Bij~j-Kq6^RUOH-A}V6z5j(kiON%eRmgg) z9!RHI4+wp!ROb?%T$8P!`LsURQ`c z9PRvf3wJL2d$DSyjBfM5&Hgd?6#(*hYqSz);FJ5Y$%iE5JOu&gB!Ap22A%O^L;e|a z*UtgdiOrlJ_Z0lbaB!rl$k4>3apS-2qq&&)S=@{->GhHgrz?l0H7b6~4a#)m^$3Y5 zVVmmWkcd=}FSz%~%!yQ$ALY1KiR0NPjq8oCjYEcnu0fJ zdE`d4hPS3ueL86wGDsS%O9q>W>);Qy>fKkRtQ^UGWP|&&?kSLg*R(U5&4>|wxtgc| zshZW&gWiRvoC5>h)e6#kjFYsuZHikWjei8~Us7u%WJ z+2MaAVy}QY&AldPzVaFAVX4O35I--&V1=nExoU| z&oZsDcV#t*8S(zRX!L7H_{)k&8RkA|!NfJ~`{Q+mN_pj0~abCyH2h4NEpFgQfc%9fCoJxlPFCiA2oQ)?+ zQrGPXgZ=Kn>MFxUzFL^5Ht!S>DMfBTDy0`sa>&JfK5bJr)tF-Wvm*ksFA7S)(1l=NG;|NGUzA;zt(lFqY*C-0+VXR8#FvlI}zT%wERUYO?IS914wfrj=s z+9x``1ZR8ATMFE(wnKj!VW=*nOR9`K+OhF}w>An9z-&=|A1fB=!}Hl+6U$Nb1~Jfv zsD2qdiL@JzGRL>M&Zta%l+S_Ad$h5C4K}cIoPug%5PMbkvmPENgin^&tru4v>=^$H z#^1h$%sNzPdw6mZjc__Dspn2#(ZF@xbeoMrg$MJITFoh6yub_EFjUZZDZu=zODuDv zKOL^Zu#3!xub&1tmM-y2%YA8qB-W&{Mt3J81W%2VrP01*iGGkU2h1_|*6wnPymz{q zsyn*(n8Ej}gUg_kiERmZC_*(`wb5I_N@Tn${UWK%4D<1w!J>6#V~zbOn#-)6BK4!$ z)=t*23t#|MH8TFgF#nwjl?G{LV@T7~9N`+c# zC&15IJ<}>Rh(SRfNZF?pU-zngt>Z}U0t?_})YYqK*PNINee&RX+aqbB`aGYPi%QDk z)K+JibG;?>X+Q{!ESqe{s}P1h10kK>PPg}))`fYQ9q)TCJ+vRD;0LD2@gs%SqLQ8r z#}@Wc3%jHjr!Y!<*yrNyPsQ@nsTX`r=nsZ7l$ceRVtJfHVldK4Y;e_Z*LMy6rK~Zz z&zh#)g6}^3(!|j&57&1L!U*z}t3Kjkh;6@rat~*vYe82e*1X) z<56~5&2*YIkwEBpKQe~w@!Gq?RArGEZ|ce3q_~CDjS(SH(;WT+J(FfOaZ?J968Cqj zZIH`P+LAfAuQ0{Ql^W-(m3AKvAr}9Q@zb+9RCxUC$MT3GFR0HUqO*0td$$x-wW#Il zd^_oJ-ki830yuD&8#!HWO7^%aKS)+YOaoAeBYy39xqup<&;+EamuwF)qxGeH2LrQcOJ88a4LPIl<2nAE=_6U; zjXB96BCjuht>SO%>T0jFl&LYQ9{Cjmr%>S3q^#mRmO-A{m9 zHRT7+To&0!e;6i;y-q%IR@+u80mMNLaR*77B4S*I3Qct||LVZXJrf@?|K=HsYGSLd zzu(sKgOe%}9cJAh4eIlbiNp(1#pslwU-ToAje<;9>i9@M2u@7d=VaHS z@{JKqFUK0`oqx8+b`9M#$Eti@=MArdnCMS=^|8E67BIK6$cy++S`Ogt#DA(o?+Z#+ z$3kcLDu8v*>M63S!U@)jybtdySc|PMLu5LO861txs%G7}3S7=|Gwl|WQr`WVffu}P zlc`iF|DMq@yC7Mr3=!!l(jggFabDMpgtO-Eb3n5qg1+;33QVJN5qweRdr%6udS#kX zlzJqWap?#N2E@^a?%FREF!>@Qu)7@HyUf)fA3!WCqY%aK1~m=@Bo}8<_33xrI+B}s zsv0vxr1x|C*gxG;_^Sc&=L6tl$NJA8G0w8p3*G8uZ~LnBD?wkjgT-!(OC`RA5SE`_ z^?C_L$9E`*ED;#u?hLo~oMJI59&A-+li>evYRe!3;kaQU)IYE-N?}K;RMhda$s3TM z4AL`wodiRcZ%xf!%7SZ*T}Ieh<~JE?g?RMc5u-j$^1k+iAJN(=K#iR>`TxeYXoYK; z2G>G&{0H08s(~M=Vm4b_$)drNzq#EiVQ2kXeOq<|TVJ#24&C+Ud;mQ|pVLvU!4lLR zoe6XPBibE~o{lVu!`7@Qwna(5?J-mgU+lFm`WU!cpQ^{8I&nKI0&V(GgJM<`%)2c0 zd>OI0@bFbF0!G(bBPie3_RP(M=M?wc*2}K7%kvy#W`ba~z7)IPPPc{ZNvxvrOp4{i zk#X%5Rib@;|83YjD&qDr_hv6ihOc1_b-$lBJMEg}-)*rCbf%uJ;j1!(_V{HB+f6Oi zX6V;@rO7O^3XAzcUK#5^b;Z69rA9@CVlB=j`}@vLRlLE4PaOe(1&E2&j6%IbJb1=C z%C^Eq_hMny)-jHF8ik1y3LTWis#NVKvlvK>c= z8vGMG#Rcl$_s}BnJAM+XN_p-s2U+Z=Z{7=!-7^Ou1yEv7)ku6yGs&hodG|~*u_$PT2a1#ZuZGlxwxySgnre^ zB-BG^o!Q{IqAXf>WcFhl?SJ=-uc#5~A6*0_K0GQ%(h zW!QCQ9JMBFYN&)BA|I^5;?8zT*`OR$ey&GoqU%b+7z`0svqM{$9; z=NjteiJLj{A%wl_Ti@p0?+*LEns*fU&nAtB*t>BmOgfP*bga^$3 zN#7E`kIxLGeHtX*E~Xp%G%w@vt<3DU1hPl1!MI(q(WhiOfrd#}Q8tlFHy;K$6yW;( zPjYd^_N$Y&dfRWhr&o$U5DhvoZ#|sx;3r@(TJY;3iW*{a&geD0{nTWbr&k+O zUXbZPDXTK8zAuVI7RyQ`oBasP9YTq50ZFIh_|iTdQ>~ zjrq!D7W>42-2($go30|Lp~&{MDAk!?xL!y2(#1f3e!fu^zm4^Mb$>QH^9kxN;0N;c z1AENznmkbm`ruRVZ!=%9KqZ<1n$0V}ye{?oG*!C|M<(}=xkug`vCrPR>N=>tsHehV z(u7rdWu?+jjCq>Mba9lD%!xW8f9M#?sJlrHscn}LxxGOn(9V0vUcmd1c?W;ZLH~Z| zWYywjupjvlZ}+~vF~<`5cbE6~8AU7&CSZWKSY%sK?eoZyEKmT}mc+;VZ*WX!_i(|a z*ADMeO`wu?o6NG)fsSUS{s9jJy07iSCCaXJJgcXZ47C(a1gR;Pk1t?i=Q zM-9o2LHY_EaMEeYBJ(lKQ_*YP47bNEVoRnj${3G=WqmjAjQ+45_Gbe)1z&L%_wJ5s zEBjv2qr=1u5!NmChn-|r>Pb7JgaO8&x8-4M+()x2$cUxQ2X4>uc6zsBeiH>?etYjG z$j+rcB@geh9GpXazgJWWKX`aYO^~Zxd6fj2Z~EB?zcl@V% zj{V69K5NeAWt_zymwV~|LCI+DIuoNC5;YE6^Rv1sq?07GX~60GD@w!S(U=LbEHgHm2z0FvtHz;$J|fJ_gQRIEE4f}QvwMg86%6R@ z@heB*+Re21i|(Q7LP1Skjxbysigb7#U;URnJ-GUsJ1vvKtLTnL?8d|G5sM|uAkmEy z>H)5#4c%aG-~_nX$W3`d#uwyw!sW5X!yuyMT5j7ztUEEiXaVY#eD6QpH_k7F8wncx z-wPG=@@L2j_#d3!6afekOz?kC$e-XKWk~J_-5_;#nVz_^bV9<2W(_(k<#e&qE|Npx zFLAlV*e@oqY<>k9%4YIf(=#xosVdz8Ko z>&xZDfnb}F3H7ah#8(cr7fC%iovfOfmBB*+3&1el*kdO%$|c?%!*& zNGQS6rVY=QLAVY;K1G;sMwf2T4HSkY?)F?r!)2et-jjp-i_BH}iE4eVd(*I;vsBD3 zBU0MML_8dp+{et8O0R~3^HoLs9Ecsa$q-Hw4j%637ryzdu@a=S0%jON%`g|`iX`=)$lMiJz3%2Ve1N_%1w6iaC|r(4!2R!#tZ+XHQFGyXecub(f8PSkFiOZMZH zVbn-#l6@4nNzfdp2s)jEHM^gx(jR1x4MusEhuX4;hP+d;gV$Y%;LF~5T-ta-VMX|O z3x#&b6`9~B^=aMBhFyeZmbd6v4B$i3S_7_W@qZL~goI$2>0+x`GK~WT=baQ$Sb8MDOKg;a7?}$I$zuo=1TVrqwQf{+z_2mlf%M-`~M^% z`RnlALXbp;d07958?7nIPIoD=En}hV6yMXs8T zK&-|Pw?1Ey3;03|fpBPVi%*WUZO=yR9bprAsEZZ`<=FP1*TTEY*9ps<&u-&z-wYQ* z+GiHEeYHT;k{iOu7Oa8oZE3p=d$6}G zlhN0{X?3!jTV>^zDn8kDOLw?L7&8|(kDIIp1OoI5Od)gttH;E}ldy`DQhIXx{U20e zZMQH{H#W-o`fhGWL_I=x6k57A_ZhvTni9Hco1;=+7PFxhjhISp@fP)j5c^3J)XD@X z+gr>D7fH8h{<0d&#n8)3{4?CY{qiSCFKi*r@Cvcu0=2LT;PIj7V`1`13#Wj5dAot5 zkQ#u9d9qL#c}fXB4YQP>8J}S>sn!U0osZnKk!GQLthlqPigKX3T)eN7W@sudWYHJC z*MPlTZDOzH2JOWou&%YLHrO}Y{!&J2im(M zIM)mrmGDECyHHrbrGTtf#iCBuwFnOT-HaE9*gm@YK(@~lT+72GWsw`mlq=BI zfW&txE#|8Wsuh7D-Y)G1$rsDs4p9_k##@g4ccmd@I3{-_W;??kb)^?R!GmV7G z)$kLxY^dy@T07KtU?EY;azY_=l#4*!joBg)7Wf&soWdk5l(;s*>vszUJ>17owy`Y4 zYPfoEn8$(QPIdOB8BVufzEQ=@nLsFiQYWv6H*qLLNAULCz^xCsL${ z#;r^=5F1|jH)gt}VmAS?{|pQNj1E`+-_s2uqe#~Ebe%NbE@gjp#1=-UoQ#5~a4H-* zh3=gv=l7L@LH^m1$W}jUyle_jWXs)*N0X7x)MYf9Hys_L>OES@f2$g42dmx25&Jl@sNCHhSa?%gR=OL< zly@I{U$~xvS*Tp>Sg1i)YcoO0uuo|%WlkX}c-6n;nuQUVQY$#HQe2$aQ(1o?RD*b7 zhpQc1?6SvI8^$4b^q6f~IIKS@_pUNbU+qk*O_M1%9uQBTjG_tl&;}MOes}iwZW*oM zNLutuEo~~h$S00|H%-uN9$#(7VS%A!2+ceHF{{6mn4_ldYPi(FJRFN0U#T|dCu|xo zP5*-(r2UbA+L>@iPH`yW1{&tC)0u)gvZBXb!}&bW(Q5kDOC&F$B&DPSQU%z~mP*Wq zjLi={911GMZACUu;3uPD^A^a=Wt*wT+9A(u5$5Ky?@_{^Zh<}8$ojqC)*X)u-;+;L z7)x`hyX^*as}%nPiHO9XNh2i^03NYP|N- z{(JP}zQmf~0D5NZQvAU=55ioi^|u$iNU{<;J}=NH#WM#d9CJv3BO^$2^iz6XjuzR( zBm@rpa+7SPkq8okEma@%Y`DEOonM$wL@6w)HyJy!n5PQN&ySwVw=j^ySGouP`jB?z zA@xFQF`8X<)7@~bKt`9wM+|<{w8P5|86q=;rpaN5oY-1gc z#l>No4D=f}99av4-Zi*=FP^Y0t+Zv5XdPXEhqh6|hF$X!pjLx%(m z!6vQPKycZ*fxI23c#S1_)B2JiB$gFL2?>ANeg?IL>mqn2_;={CkYaZKw=zlcP1Hz| zyWa<3U)42xvQ1l$gq+PA97v7T8qKQ7rITXOJbv_>?62ZyxD8%De9naKboF$cLcAqA zoGXN1Em6sZOip7gloKinLMFN=I#Hdt$v|PV9UOSLeO#PI41XpkU2d_Qva5HDLU3Np zSW|ZKCSsth#_iy3trAvIDX4F86;3IX@XuTF)*7Le=$}{oG+fhiVRfvPutmWJ*u(vr z*CQ&_#iQn^tYD*3mD(>Dq35Jgaorsg;q(Ap;X$cK|0uRgHE4C4Q##k=#3`P7FO?-lB!LKIPYvq=%j6w>A`ZZ}Z zMaTPD{v~uS-Plj!?Tw6(AxZrP7x+NAsKlfsloxdRKpqA?ypR3R1Q#}&PG0x<6Xk&G z$LU#HoNuiqgEIp&W~s)&M1(VfH2qSbdxVvkb`C=chKaHbLyq3lR+H5pr>sOqTH5I2>D!Te z!4u!P%O;~{`2KotxE%Xj-G#)tf+K3at-`KYE~OZ0izdvvz>EPT--=5iXOFz}l^A{{ zJ;$Wk^f9)+`^1<59I5Z6u+XE>_{1 z-V0i9{GMHS*AEtQi;9T>Q{&Es$*LvN{tBio^#yPb7D~~Te@^%{Sc8BJeXp6{YD1pQ zZWZM^EMh@2d_=RFs&x63@lgqTl;=&6Le>9e5w2 zeA!>{<>cQWKuYvOizd=8G4BzLP~v!3BVs(|T$Zk7#3j`#1tlJ=4RO#Gnk|hrmErqS z!8xR#*#W*i8#>+s&QZ_$D?-c92PDTW10yq>EPAzYFgKT!hMDiY-dWGaa~8{9t%|W0 zZ4HihwLK+t;{LTVQHB;c%&RaTU5eH~pU?)U&i>M;MVW~NH82P103G4k^2Ukb-j&_av^xwY(S)9fA|X zKfuqgFdt>C-j=djRd}o##|pvY^TEdbH`|Az_n-D;Y)X|Q@t}neBz>IcUD+PGMBE-} zrxnsD@p;uzDKLLmy+27%v0=3@i50XLe$Z~yf%6~}sI4E{j=Td-_KqD-5Z#OD(;o~~ z$mQ5o{K+djWuLvP#Nmxxp3izLZnnrQ=KmC&9D&m2g)~R_k_TgW^nwKC7sd2(%3?ss zYatzRTDO(V0VxgUlM<-VzWouC<4>z#8|@FJYxci!+$w7GoH#pyLJSnT0`VT$H9S zF_krV&AyB}6Y!7}8_VEx1lRo*EX=~F3TZAG33u6@9yT0!<`nDaPL{f0Qv0mmyYoZY%@gmSF5k`xqz3X zV16xA)E&j~`V3ZHRL8>odr|j)4OR~g)X|X$k!+# zvMDr3MF#MU&@iX5f}XaekAx5&GYOSt*z11;l7GdMe;yPe{k|Ch=tW`^grNLTQGXuJ zd5fVH@{v&73_T`gOEa%{Nma?B3jGw$hXprB0}~%Wq`VVj92Z7Y5c<#a{G)RM@*!W? zK-)>0_canWG{_w+zZBoJ5^J|U7X?v8!mIj*K+)SCdTzNOdxSt`X;kq5ST9NT21@$1 zV)k>+QsnsZD(obh#QD=YUi5#*yuU*qhR}B$01ad(M#lZ;pBQ6ie5~{rsO0@h=mQ&_e!5VxfH` zuOs;#-J2iv5+E6BZy}zD)LISpkyruB`wB@RfAcAd|M#T$k3hVG4rUNJbm$qUSp)_- z%(#Zr&Yk^k43$dG@|HRPJgRlhkat6I?fSMTGmI-*ecuUiv#JDEFN zlUy#lvq{#>jVDi+WFPD3v6WNPChBsIu?mtjk;?nyY`VHum9?`|C#{u#y~F$;cA3Kj z@5hl+tI@*C$b`K;R~L!Ws@1Sw@ewEFbrM>~naCzZqMU{x9d*{5It)aIPc94hyXU>Nm_3+jYiV3gUkz9sl+flHh;L4~RD@ zl~Diw@PE4#q+ja~lB-spuN72Df8FGNT;;z`Fbw#~G;A19u}Awq%=4dK9LS>%|8n$c zy;xF8MFd{Oe#J4Z7jZE0o0Df6n}`=lR!%|38oAw-DSL zS1__LXI!xWQua{PT6Fequ?Cj0TW!SCo_yg#fAXq* zz5sqKh+u=t>h6Y`@zmLlu~c3lXg{;_{iKEpNE>3Y&U>N}Q9;=nLY3J2I*QwVr?6bJ zD8KzSzFA?PDr}w+yayRyBr zlHWo8%SAWaf)N_yG-@iRd+_sn$#%QyRQiG%$9)_3v<3B|L=3DK=aFTT+p*;4tq+G@ z#mRC-l8Jt(iFbCgy@WNR+t7HE#op1Cfy&DPz9)!}|x%6Vr`LMLZg_c>==`1F0Rc&6uBmyp*@Jc&k0QpZ{3 zFf=^o!CUG_RQ!+e45`F%uyj6CKg}kOuEp>tgcK0BNlJ)=?79O1e5H<(2W z?jRUKPCIH!w;Jsx)mf^d`uUyGA6H+PX$E4X0~Q^xb^@8fjL!w^1IWK^e(WJW-03&RA@tAqhDJghMV zQl8B;0g)o@{Bjem>WsWe8&>4zR=RRQn#LxDfp`K-SFfof95!nf+je`Svrc_D>&@07 zLl^@*nnKdA$y4|mHK%{Ug64~@pI!6(p8&&%Zy`(WA7}}GZqry*SGTtYU%Dwa?!{jr zA?HBa<9lVS5L-8eG=4o0M8n51eZaT!EuRn8H|!FXM2o*FI272Gm^ta*dSqRKrrI5SK8;{x8e~311T_S*(H0L5s)@GwOa0WU!Bs z->FagnyWI7741{k+KrWREMq+y(6x7NHVvv`sa&UXeQ{Y$B4?n*tNMjI(n|%I%l|apdx#lHNhJy|Ny6hoz}bGOa!jo@F|i!AR2bp; zVarQ5hg;99?lwXB?t0G7xTdWYZ-z_55iFaCJNl@ir3pK((vb%@soGA=SbL}}?mf6mX9RHfX z-pTAfOVAaH7UPilB}f~DGvhfp3zJACPfDg5+8l|yNs@kY_DWupDdQdlwE0W%c4L+) zdHnd~G89OYOa%0LZQ+uUjNC5tJ#in#863Mfa}Ds=3&Xx!|H-{iL8vA6W|Dis?>k58nlFs6FmQ<-wlwczn3oI(sLs~bj4N9g_ z61@X4SLkr42(C97Pw3s9*4ualPmtPw>3O|%B{3rYJeYKG!MuP$R$qQ*@JW8OpeQnGM-QaHE3`?-KT>fR>gMx#7NtOy*8WUiDkMX(x8qp_x;NgfgM=~CzquE$bvkD>I|l{n#`Qy~M3_V=r_E8E z4V?n-xxZPchdi%yTjye5l#{IbmWxDFS}awT*PC{zmvD(j(E)Qt;xr7-C7HJi<(FY-$PgjwL*n0Ebz=FbXQnu{u% z+-#Zo$4oemSUe>81OVMX6Yw7$_%+w+Lf<<9&InopV{t@ zC#qIyvB8Z-_V;`AYn@Y7H>$KIQMK$-lbJY(@yXAoPX?lG~Xr zH@EGo5AS#AY)_rX#O>bhf)IHp&zwV1dsVi$kW%h^%5hRh@CE_E%fUispXYWI9=?o7-j-P;n3egX6$E~@ywDEB38d9ri8$vp3{ zcF~gkh|3yk^~<0qgGzm?#ZsXkt;UzfREr(EU;+e8I!$rk)5Qu^c#8pw4Q7TF9(#}K zQw~R){uL=lly1NHg>Lq5F0EB;XE+9vLVA6fLCm*8@js}wTYnD2TE^;QNbsC{FImf& zGV)|{v25Jsa`E3EZHerU?^lv;!yKqrvwc);*Z|Nd0$Izva0;c&8G&UQOuph??= zRb_e7QEB-F7cdE4VBV4@=diin(_yx>=yNw`TP!wP>TXL*ifKlDJ<4l z-XT0rL=@&SA#WjBtrxayok2?*FO7u)sWvMYY?XTJ&V=SOFrI2Fgj!O~f!nPpr==$# zg*1~!*C-Q0%&|_xnv7RjT}T#kTl+C3Jl@f50GOaJZaIjBl%zzbRy9AIdj*D366H}C zD1vn6lQ)pjn2iIA9(JV~hXX51q|B5noz1RA)%Ux>YKWH2(y>EbY(eD z;tcuP3Lp?v_rUYppxco?yf^cwSSB_!Z@z3cXHgeueN7;EAC87(WXKhIad}C0>&$Ef z;>q<>iZJF@Th^uZrx41T(0WSwsxlYHd=RKd4b1N%>V1uBf!4Ue%qAmd5)h5b%67;(QK3^HMNzCDmU z5<8cW$3^Sh?vyHS+R|hTgsfBOnQyiI*t+uws14>g0!i{3sm`{K;O%$CbW6u$0p{qr z_xc{e8GCK;|A-HNtGGn9**F;i_OA3$mSyv`m%CV_otdO*q~jPs++bVxAK*o^qGCIK ze#T2RLD}%k@Fy=t7g>|_%yHJ5w@-_kjN7h;nYyZTENPZVW$`f}lIe+b;v3==bwCP^ zIF3ra8OekfqnS5-FKBnNDQR~(9*wD_8-^e-GN{r=o7>4ClzO`sJn?inppRkYe02as z8e|WyfhC^rL=g7Oi;v(`TNa)l+|t-L`MSncSG=8s%uep=K8mo$xbIKQ^iFzz?ABl` zWl|n{<3|vJ8m5~-)}(OB7NvXAG3Yxe0k^g!tm~SBUsJ{gCp?y z!Ck{?L?Jj9`W>8#5*I-_I)CMjVcdVuubW-w)`R(u*HKac_C`S3#idw%TL=mqQF>r~ z+|t&klmu*BwBbX;2nk$0)1N15-VW7A7)0P|HQixCRCT`&JlJaX+b;KH&T?CaL$jn5 zoEwtWYX0*}nmI6_FO^hyF5x+e+;32tndysy*nBd+-HVGj$$aF*6~eslXaE80P;D8Q z?tu_a%=g(pF7DKv#xl)&pFz5@=^bciz5?rIn_Fh^yBe8ZL(0_)<1=RoswZW z340j48~S1jD^Wbn-ILplmfhnQVb{QD?_M0|#gtT=XA)kRow?)#6+k#iZxRkhERL2x zRf+(MK**zd?{(=(vJF>^bLdx>3dia7x~lK}JYwLZ{q~HdkOGnnTrdpJt8w%_^gw=k z=h*+I*XKB|1fh#^Bz0uTR-x8B%=`Nlu1NqbyfMbPtuc#&D0xf9_ce_tzAup;Lx5b0> z5+VR0kh@&9Og}1%(+ii?C0+}wI~bdc#wyah_!9a6jOegD&Z$9yDI(ChatvdDAJ6)C z6ZqQwNiRy1wLNQ8%a)MNo`+LaYr=3QDkiT zr|sQOH{i-8E^KdgyJYFip4lhPGA*CJwnk&@30U=B`CW#r6ghfA)fmecK%TGre((C2 zRZE|bl({4(dsp1-(Ai0)<5#x=x9uuZLzBQ^%FcG%v&s5Hp*iX28H18MK)~t*SEVsGgjDccXiLYK4*ly4( zo*`DiELb|IyV0Is(GVCAt^c~*Zj5f(7asPDh7gBOnnC<->a2s{k4u)gMcIbQLKARm z*}~Z@*-%4#a#%121uj;+{xFFDdbR|p9?3&bPofeA#R3bM`<>ULjtI8*@J(XVkLmcv zb@QTn#7SKP-ZQn(Ak%E)k>>dpZ>H9)^&@*7yqik=_UhrC!8$x|3CUTDGIY=Pe(~Wo z;rV(|b0`7g((5-*&e41HLu5Bt@sHNKz1K6f$gGRFMtmtQgU=8&V%22NRLUKs&Viov z7TIKX4ysp8hcE|tgZni-_q3-O!zSA@77()Q3R`A|1llstWp5+`Jt7M*h(uPoMBbOO z5o6I8uqRDQ)5O2M#1xg-B)oqqk7$UWB=DkaYl*zL6LCwSja+!&2AL%PG(a52Q|=Nj zGJD2Zz<=4C6Prn(MnQO4F6TEVEcUvy&c9u(;<#%*v8usml_6zt)v5Wj#DN4olJX5} zhp^x+M@i4;b-M#RUQEfCm29}>)R<7^P_{}~adYq1XWBA$W61FB50C8}?Kw4sLC!DE ze#8di(S-A#B}98WcbM@cu})wH(!qlm72<%&bSC@u=c{FbyBlC|jDoeebBan=0q(q0 zFzG9Y;Q}yhj%l#8LF$vC_F)~oBgz)*fgV+V3v&DZur7Qt`1eO-ecDgbaGg?-T&Wd( zoyOMZ|WN zh()<$JezAXm;MY7Z}0;p>ooyk5AI}0CAvq}2l$m5 z?lgXX3|s`mT-o#WZzXQl^z-QIjsThHr_XNyIEh~zwoAY2o^e_PW7E5X?su*7!8{jj zk28ke@o!*|W3xS+^Qkw4GpNc9a+GW25jHh}RSID9twx|wZBjzFCja)a=;p{Q>J zF39TeU>m+fkg>%4TSbjeP<4gS*|uv0rSKz>qJtGG6|`TYA)6>b)24V7-U6IF)S5^3 zRX^@y6wQf#cx8aZ-2U7>7=p!0EPRMZye6Ji-A}>GPt0GWi*c@xy(I80l@KDksNmI+~7~1o5Rpb<# z@~O1NVm{oeLs^Zz+if1tU4MLFa-`fwTyRkswU7zO?%Mkps7&($Nb16tF z7R@kB+{!7(>viiB=(z*<(5?0aACpJW$fn4Zzz?FeULcu_ zZGK`(J>((1U9`>`hLdXDwq>iiE-ErVB0-O-ul?@$j03GcnHp&$9<{l8;XyZ6Jf&6j zVrKjLj-_y6n%xS-6Ob2RQtm_aH$sKI!LP_{X4DtQc*saT%kN ze3DRY&7k|!?8tZ$L(vHMUf2T+G!ks2HIt@btW`q{;Mwm=uZw%JiMDL)2pVvxo6g~G z)#rxLBt|K4GUL7t&nCj8=vHUNf7AZps4J}1x8R>8M(M@ly5Z5qCK{k%hE}K~4VMbm zm}^>$vje8I@*4u3IIs@%PMhMH=wrx3dVo9p{4tL=GVL*0#^O7oZDm6$3E!~@2dZN# zXf#4DKAnChaA6YU!#yRU>{&lE^f2KG&I`p7b&k%O6whqtMTV5`B z0vd&r^7-g_8)Gm2WS`b2xm*b_MZl(z9CRUV@&WnTA&vlE9K&Bo>ytKI10RJ(fb_TX z`wFB{nB88g9F+Ul0oN0exXOQpY8&foPa;QJ(>`}|8*!9@4Jg(jRP2zgx3I8?r#ue0 zxYTrJS$7Io9ONL+xNtu5fV!I2;Cr)TKGn)L+wQy+l`2rXLi0hEL!Iufz`2`iM)>D$ zc83jeDxi-XCFnIpUhtEK|^*A~3YC!Ba@BxH){6f@=ZH zY&pC9@z2|d8AX;PhG$M5{+*<_W06#^%~IfB5CflC11L$!w?F2J(Ac*hRf5>(R+LxD zoXXHte|6t@d=UJU!Hyj45iBXAoR7-cP3MryZwQr7!%RKP)r9a@{v>NJ&FywnyC~on zlVxr`+yn$9Ovlsc3$D}{Q;s?*C=_77sYI6~&wMLj4(y+Y~jyr?lelqo6 zAY<+1imW*z>vRJO$>OEvx(s{6O_FhBSf3x=A}NN&zFVyqLs32Ka|2ZUArV+0geS&8 zT_Y~0$y8}C(20a=&5g(coQTUH=DG#~9tWm2s}J>qk8cEZgVM`VG$5QXfNO8Ezi={* zRZ}4A(hqV=_+hge1qwc%T(}g&nt0biA3JsS8; zqHf36q>=#y$_BY1LuuAim}0;A%)oZ1j?MxV*V1|d?nDyY;k!M$C1RN){oydC=`_Ue zj*h#gJONe5hHPToO{+AMwv%MXDL&ODnQJ%_IPA015h>Nb(6(MPYHF>W@U1iP7iut> zfJR?(jgD^YWHsE=3t!Tpv!c;S-}+g9?Y%{pUz0dlEynX0pFPB+&7TK zdJdUO99ZUf5y1z%ZjvEoKC==%pT6yxLGL{o$@cEd|hNr`XQp{%06Hi$lUzPCCYrXWA)!i|@@1=PFz1p|>w!X&+7Sv>YB zKrN+*N@gA(#|`NJjR2O&BNcCfW?(5F3*Z_KjLR2Wv}YlwDbmm<9VcRk2sWiMuYJ!>*FQ^x%b(6elU>soH-nal63h=B zYb;mntHGakDY7 zj8bB>>faa_e4b2WF(2zB4E{DXd?U+wNif%F6L{aw+_pIvh}UgDk45)?+B?gzxSDL= z@uk^l0S6G{rV9n^kg0ElP%RD1?Hwwpo#3Yw^r0fDVZe?$z-?w11 zc85{mq*#PYvvSAA!2Q%@7k|cNUU=JNUsSGM@pH(AbJ}ZFa(R%~J{pfFY38-sI;)1j z;|j@x6bq+3iOL-8Cc;EV+R6^ywCU5Gy1yFyBYI}F!|-uq(DYs4f$0+4TZPy+weg5O zeT1r@ID0ZQHG2E8I`U~&3RYtSXKL1)3XL+0vXEX0{TMyz&G|}YK9L_iIfg|j+e~E! ztP5YCZx+X;=3M;760WY^K3@Hv&5Kk!sPq;2w(+D1IR#I$H`@ks)}JI1O6Llv;8~_D z3g$B^KG)fG=buw6wwJzjwJ5^OUcsoQDk~6-q4+(EH_g*$KyJYDuYF6}+X*>W|D3?kSCjuZi2l&&gY5K*dxGwlujqr#sG`Xo4fMfq zjh3l6FywY2p5%ySc53-zRPUDcU|R+3!6hXA-h0!pjJ6j zqS3<8O!1g-yNGB;XnGhbKCMjIOqFh5qmDu%1!;Pb<_(YGBbYPst%H7$zOk&yegmH= zy;bKnEv**u{khcspj%r!ff`*0(s9CYe@dUjm_veaZhKj5?`^@vDBa%g1j41v4Yx?4 zo?y$4>V>cQm@m7uFUB&W*;G5eoO&*^8+fKq$VJN}pMaZ00w2t#0`93Lafzz(V^LXe zh~AL#dJM_*Hn~?qrIK7wv@IQb~)7&v>{j3g{L1fVroy6tGfvUc5 z`z=Jdx#YkReuP_H7WZlCf}Ofj`?VQtIil=Czgp(y`mT3=^FNxZN8>&$esd5emMm{) zy;D$C{PqLCdwH-f1&`w^wISa_rx9G^hwL9h_@E5!E)#Hj=-FWitU}W#GZSktV?3lS zrdWSEn3InEc5z&3&ZRDcx<$}Hv+MUZ-s$l{OVQE5PcPpKW8cul%#|G+Nnc z@s=%VV)#`fQ=wyi^*FbU<40JM+?zg9#7~aAQ1un&61wK+{2$GtBN|pG=OA*J{3X0; zamiHLn$FsQ^IdA!u{!+G%C91!^78PO?-Bf#*v+)A8mcD{jz1v)0UkpwgR`1sRy0$! z#V^d5Mc8v+?x-vSUbA;hIE|XWU3bm@t2EQ{w+m;3x?ScjP+Wj;+t$K7Me|3HC0U#QTD6RXBE`q6VBT=L}>0N7e?LvKP-5pT~5wXliSoZ)3C6E z*-s0BY;-m`!}71AlO*0whzkk!!@$N;X>m>FFcI=U6Enc+wl zpL6XGle4Tk_WN>O(UXv2)4A-v#Cxl`o*Mf}HFHO+xJ4Gl+_xY$Fl%w2(X*kyVo}#? zXoJ+e3>bRqo8Z>+q^(j_ZKSBCxW)4-rD_`}P^HQ3_ff2-Y7X*TN>X z>$NON7f5j~)Q(;5fTXp3!etVmomaJU4dxy`R}`)^%5@$762isu;|G7h^s5I^Jbdo0 z!vnO1TKIFfn~#nu+K7b%I`B9TH^FZ?2(b;O%%yM{T6__ErZRn@91ASnYrp(>Y{~t{ z+Dm=!?G$B>x!af{xU$D{$nm6%bUHqk9M2@!o&d{ZA1`uZ_UtpE9j0Ab0i5Df}$i23RAEULswJOTli@e&P<1r>DLxrcZa+ zpNUi>=_Z5~l|hSU(;#`TWv$YTmMRCz=5=l4k7YeyZie{U7`yW5ZTL{XKxvA18VNpH z*dP-4x=8R-!4eBB2UoAyy{09D1%j7wV(sNO9+8IV%OpkQ8bYWP!aCIK6K-nN z_As(PogDRn=rB0(6Jn&yruxwNY$8J5`T^_Ws5_i=wXo#l2Lwwq{a1-L#T}IaXyeb_ z?PI3n)P8fh=a|=4bM!@XD0CS25z55XTa^AKb;Q%7clpN|IQnkw%_N&5zx2j8yEpnG zRE(}JZftjqV`=N`O0-k8CKAlF6|Vh2ztkjZYm*HWJ!dh?mrsgg-d1b38SoEjiyRnf zfWloyP;1&N417-`XclKGl|uy(H7B`GSTSn!IyA5hKm{A+zzVSX!1GixNZ_9vDUM>JchJzI}!MbavoAI2wH{IOha-)ioYk%^V+7IcD9 zED*Ok-^x_KDld&y+ChiIsmcH>i%s?E=NBYZPSxsOP#dX$5YVxSe3irsKQ~#8hggOl^j0&)72R` zB)!ovrt|ajjnW_flSSP(U}SZdtG@pYJCN^1Ts3)ILLJb+k4KQlzX`=(ANR(Yd7<%4be|_AOZn9 zs?ob1RjwM*tXl4_MgdP8NF$MZuyNaWv|aDgD~|L>+PR}G_9Rg3)|ZuP;FZ#d)%1|5 zDQVvaPt?+d;nF{BK>5t>U+RFMGC|RUZ9|=vnOAUks_)LkB7|8<9SDck)EsNg*})#! z0Y1i%5lbI)b%u)TbSu6|EdnkPGspR+|Dv3n&aA%00MI!ezzT+zF)3$ z26WE~)t;f>k}A58Xmaxz{SV0ENfZW4CgJ*L()DOS)JIq5UNKp~eKY-eyGh`<+lRa$ zi|bf(x-jf<%l^`HV$RW<{FQ>3oA1=LHkAxHJXBx3#t>vv+l$9em@CwSqJ(1ATtuV} znq%O$GN;l$WAqGN%uRhY)|V_ET&&+^O3>fBrg%J$`X1i%9Zr;LECxD!U^dMWx60&T zrrK(9%n+j z9bLo~hyyujP~Dl$3@kf-ujLa}09J6Yg}LKZ<$AloiVRtS;FeftCYbU5azt6V&tR-v z=duR;4?T9Dk3d<%V1}yzG_HN~TrZ^xstrCh&f<6?Se9NNb8@$I#~xNvHmi0#wWO?A z)?0P|ah61Dfl+JL;BL;ti8`OmEtC>;hvUkGE(Ph=keO)bL=~J-(Bvo58RvtM_X^%BqhF(UywN4>GIA46CU3z-Z>hn zGCGNn1jpu)1FSV$%21Fvf6ep_U$s`j`Gb8}xc4!5fgMve&DeaxTQFU(hZp(o+VEH~y=Csp zE=S@HJ)Kl&nAk8^O^6CX1g;Q_We{7KJfn~}mRe=dVr94F1h}Nm!bCBpXWd$ooIef0 zS4fL5f)50JF^-s9VITffE;v)0@2^1cYqp`L&a=L`U#YeoxJj)lFsfbe8GWOznH?R< zUiXR#?IYLpwN8-Qb4(RuwXs_TA?}JsvDj;3r%a%;@yR7KOOt(2KjAB>;WU*F6`pw#0^Ae-W zhCv>G9%~)Q9WprF^K20FBs-m<`ACOnSMs%2%s?{|3E{N)LsFrBn&ha^+Jk^9A$Cvc zdy2c$RN;#@c62spiQ)$Rbaq-^-k)4cLSZnJe79rqxm926j=ZbP^dL^E_KY?^75f^O z67bd?I1b|`QKGT=V~BW$iDkQghVmHv<&}KzEWhW8j;LLEhbqxLEVboECgh{$dwZmK z$3NI?N8EF5Ew}mQP}BR-kBobS{C!;DG3MO}ioL=n6USAR$R>hSUwbuM&@4S?&gmVN z+vm~XOs(dmgN=DoqA@R4{50o#v5w*kuaUAGh~IV8UBJ%Mq)Y9`+eL*m+(mMhoOD~l zlPTQ`^vBYeqfCP}K1`|4CpXca9Jw!7bxM^)V~bhM>z8O@l<8>3R%pOj^h<1X8E%^= zwp%}uz2rgcGs-nHB0KyiK2%^RD9&|haZ$AkkUqt*z^_o%%H2A?Lf6wfU5cdXkBaT1HB6$*eOu#tQ4H;<|c?fW2V1j9O=bRN}J$7rf^A z>^xg7593@72pX!b=PcQh`5AW?Z5^4(z^Se3atgGEpxxAm*C#|XX=7c znp-Mm3`}2h!j!9ETLnF0dvy}%^uB+(rkrjoro<%lixtJOFn3KXTt&^G;n96Mr zid!vMODKFxyS#oCHD5|#juy0>6O@_7ki7CZT^7ITi#m){x2`HT@RH8^c#A%N`TZE# zwl`u87?5hIE*4dhIMrGq6LdAX?Z_g6Bniod*+H|@^T2pTJ-3dHHWMU79c}T=H`p{5{7}QVv)@9)huo{P&&l@&rk-p+ zvUU+^Pt(A8-Dt$rsp;kHCAKLXX$T*BlX}Ip00uIi59s9c7|0m7|A56vs~aceq7yYC z^u0KxH=JA}sw)moX2B3)MLhQtBQSX&_{*VcKDof=BOXMvZo~?M1(E%4zPa@4sW{spO`SP6`oZK{h-qcMxK&sh5s ztqs_|C3`<(NnJ*>d6*j)Lm45@rpOe8xiWzl4*l`VgiPx{x;}NRAH8#f#USM^Ojo~2 z^CJ-gT!2D(ZO4ZPH%wU~HYwfReImiQv^gR97~LeXO6TvgyDMRX;X|au-kXdG>?@^L zN<^FxbGyoNR8w3J$5`^Zj>m6M%@*8$n3Mhiq!_jA)=Q&7wb7`HhrZqWgmWvZvTe-X z^u$!wEZU15)u1?}5SPu0-J_bFIY#B}BkT`b0HlGJQ*1D*<;`|6+5BLqLiM@}&o(RU z>7wmZgKHFAaRMIJ4tknx^W{<10xkF2OYsu|hKgjIO8t2H4#&bl;RC~HB z?hbKGa{7`U8p_c24a{?Q?}=Bc0jO$0xU4=mPs_6vu);IiEQWP->*C*-j@eBj>%>XJ zH)n`RR7z#Us9uy5z4Idk$5k=@=z%35ta3vs%DLj)v5%fKvM|!?>qRSZy{T6PlHVR*cIXoCg?Xx%~I576^OyojeyaqpG8#KkMVMT&P%!dj%Ih z7RQPDre?S2VP4V|bkgbjX(OswXBG^yv6|(N?5YVbIIq;Z=cWX?T9X_auum^AQD@v` z-imwYPMs5jn-G7ssLRHFX0`*p2lUKp&-{s3Y{Fnp7PLA~kDDhy&J^Fmf-!nmw}(aK za0*NnftpKgjwMb|aF~6MrNFx!4+@2*QIk~#yEqDG(}v&I-n(w0&N1Pa$sL{XVS(ja z$>(m78~K?2ZoM9EgSPXCaPFVq(T*-^B^fh;TNm~ColPxD^*jeYJg6G7DQv8VZ-RX7 ztBGoJ+8$F58q^#_#5N^4|8m|^;QuPBBZgSgY&97GL8F2-x(GpjSAc%i(VJupQkON@ zUD^fQ%{Tm#k1vOD#`Ra2IoP+8USm#6-lSKkCj@KF$8pU$ECdI5?lcLzF$|(vOS<)L zVB^A>i`_6beo1o;KpA7@Z3_I3)X6^s?@!l5$ELn92gYHB?>q^oN#Z@m~|e&>;tL!XS)pv&gH)2q>Ie2*?hDZ(O;vM+>jzD_&qP% z&)^c%$?zMq_XV)7?}fj>&zi&$*zMq5SFWheGA!NCCC#l_c*U0P^_?`V!ZHdb(Iwx+ zY6pAV*9kGczii;v6Glimv|qBZjDG|ZB@DhFMzZu37a??w43{5 zOLSVnNbccy(i_I+u*G<`(-zegmTh4b|6|%v?k(i;PJGb-!|&aXUB@#kOT~$38--wd zoXY`zcPiGd@&nbHXXV@h4@>%J!=Y!sFUauA3<@u-1hc1{w8Gt(FC=F7 z;4pyvoA(;~+)FR|sc&zYuUd;qq{UsG)Ud6^n$8ivd)Hng8U?AU3pkTtez&i)Y>lBI zTD_|vXiB)MtC6buxTj;4nGGG?n#SO%F0Vq+cOKm#`+GJtb4TL(k7?A8T9|jCEfM~_*Qd2PU*-uAH#;ob zoigHB=}kzzwnGP#NF_E&yZ~KFgAVnNMdZgOyhWnEr6EhwaK~p7>B!~ICOB1+#OGqK zdO`b#ShwHK;M#+JgSwtVr0e*#GUlFMs`SD3vy1w_u5*qX5ldoID_P-&_e=hy;z`3( zywQZKB6{*Uw5HhO^mA-fw!=9ZkB|bs`_WPbC3D1pSP#stv>=`L*TsG&q5WpZO^q*x zt~z+1hw(^oyY3iH{4e}j_F6%rSA=EwchVKo+Lj)P+-?O4C&c^ekan*IR|mdK7cC37#~8#Go5 zAs_LIlXbz6*4lpFcveg@H9jZPf>67<+S$YGC9v`2727!d0v>&v`^tGpECStGhx=;f z@#NrK4Wv@hkL}o44ta13*#OGW?Igu`x6@|H z6g;SZ;cGM6-U%($-IK#O^q|0&B2p#byfZqZd6uMKa)R*+w5l*OAICQpcWLxBt>BlR zia=$_=#`OBP%!oA>i4N`sE zPiQS~mZJOM&|60r?grcWju-z6?%hioLgOZQi2Uh~_oSPP)4+WN4<@oinnbS-EU$Pk zmh%Wd@H&+^(K7kKg|OiZnI(z=bd?HH;7e2LICXNnma^2QsCY5;1e57kFefu`1r3x^ zZ@De?SYnzuJ2SSygi$p*GPXb9p?Sbs?K6t|FaHIDmZ1g~e(5T!cv8^?C{ihUr0z6! zjsXV-^*9@KWMmjWs@hc2H=}7>^CLJVC4a)m@lDuBoG+4_mKmx5o;Xk~tUH~dr5}@>>NZn4;}hmk0H*El!Cx#%0P45s z=$a8IO@#}_Ar+&;|ADQ?h%;IT<*&vB+o-Yjbb_mVYtX-?;FWy(jS&AGWd*q3H-L#! zTGvu%G_a&Jk-7ue^=SKQZGP$T4Hs+aYu3c>p%7YD9wT+v<7W|cud}z?wRrBIcNh&T zQA3qr9$Gf4Nt?%2zJAP4C;j?gWNhF?1OUrU?Ruwbp#Fy;{$-ZzdrEW0DHe_>7t42 zH&yumD-KM#N=+GE=K_I;gZjTToBlWKoO`UV!~7q1{g;(RS|tHae6JG~3F_Zl@%P^V zBLUqY1OaC5|CK2Ybcy@@{JsQOQ2%9;KT&3>+~3%-Z>PIu;~#zf-)-6>|NZ>;Zz!Su z`%iyw%wKWktD4OMC0j&rwuXkSnsbnc*aQ^+om~Dw*6I9Au5XJ=<`|+;quAsd~kH+1a*i zMWyzaklet+e?^n|@5c$i!U7Wp2B6=3rwH4_aMa$t$~a9}^B!DsC7IPd_AMUI5hQ_n zUzMRgO$)MFzRKQjNnM-M+8RsJ?YIL__^%;npdVXA`}etF2c`eT`6ipP#hpX^pUd2Z zAH=c+oi%1FmDOEsAX5y^zl2d|x60}-j+gD*47P`I#=P*qWqJ%Sq}ZmkzmtTLs|zRh zH^_eLs-y3*5xO6vnRC`?PyW_N-1qeIdLQEaT&}^KQHpSB9csWA@t5H@^fME$@80;%KJ-i2%SlKJ|_izFTwAQk;SF8`QML>@B=q=ksf@Ulyx+DJ`J%8_m^zEAoUYB2tSIKwU_Y zf6)`qE)ymd+;y*ROSMBt_$zqLpY{;vTkbPLPu_USkqNxC#>@X<#8Qa@qc9;a1w36z zooONmc>3n$*d7!n+2gCnH%&-BlSWA zrpWklX-Pq&Fu-xmdfrya`AhCo7Voi5{~r6%80 zZV~kAFVda= zVn*vggO_HFro%tf!s)6>=9L`lP>@e0=0AW*8-q^--hsEG@R$onQW*=%HLFY;`$kc? zd(1{NDuVAWI%UY{3YI;1&I#Tq*K)WFY^N$^9vt|=A)?bOW~=-Kg_haoy^l(*@ZVvX za&(1yW!&|u4#^7rQw7tG>xNBByjcc%a?Je%Ja?yCRY zzoXsdH#?#tDp*wibuTOj1r2Pp6*?Ot%`p=elUd5#GW$u>g|5qte%~-d6J?rrxzt$SNlSU) zwUtMEY^XVGX&l9V-3p0__%)7_vk!^PahY@up?>4 z4o<9NC-&vNHN7_BHCv&%Tt1m3)i>-h3MQ-A<9Jrqf7`68-{ykm@#_)t~wnfP5(!-bmxL!hM~EA05>0 ze+A>$e05Z{KVL-@56j&a?f7jcW;XNKk2nCD@B<~Q7QJF-)o~mdzcCv)rA7}_G>^F_ z%qoA181&N5EIQrxjedC{HE6Udw+BiH>1ym(>n7cHVPuQxp3LRtdhQGmjKf9m3G*2C zHkJ}>{ZAHiQxe-Ska3Nz)F3;fnmdOZi`sI;b0rf0^$Q$TG*2&McrbOAX~3{%otXA2 zVnP_Q)}vf>+q0lo9D_mSN3YGJxQK+5p~LqA7YE2&v==!5aWE>O5LKb=c3J*u)^L-) zqNRU<9r>#6yL~sO_xjqh-fZ;7>WSx>nUz0%>GNk=#u=7AiLycR{hAS8DYLL8p1!@{ zl`yG68Qe_b+454zGilP+{XB@u*n5xGHll7NB1&!dqbOfCn2`VUI{c)mu$7DfWV3VV z*-Ekgy*q*pAN|UVY;Rf@ z&?JZ01G3IS!6BNZrpM++JAri|cBh{2o=)8dOV~l_$K%=FGvT>DxZMGP@?S?j zjg;x}Icq6*QGtnQ5Y=(gUCs_87DME_8t zTB1-f54l_LqLSp+(-Q(W=_`Z{lo#CXY0!i0P-1}495*+}2<7t0a|77G8bh|ebXTqi+2_H@K z$L2ba2Dr6}V3XSQdB^Zp4xm8jYXWT5#r{>Co!iQiy)sp0zfL^SKP4=ROk|B1uEIv<|evQd*wEM|3`<=-|K_f$g$=@E&=4 zb8hE=8AhfKR$-+KPG64#wZvnU830rV_t#Yqf3{8~8*6 zbX3WN^|TglsD;Mjk=7C1>HSNy8-2etUZ+ox@M=QvymrKnwWwas4 zLiZqN4rDt=EURQ+bvB3m{z#7nl!06=$F{`eIt+dulb*a&|8Xzn4 zq7Z|P0pKK79h&24+>b|V6m?LF4duak#-k#)k+N7T*ShBSH*h4t_Yl7V^*caYSoh0u zI_XZ}uvE15Fwol$)hKeqb60jrD-opR8kJ(CTB#fiXNE(FjXLa?l5ON2nT`@~79FZN zZCA)J3ZTK5;3-=kLlPyt(wvN@*^`_adY^Q+_=aqPRT0PZ*I^z;Co`o6+Ls(7L|kq` zW=c*%O4hShLoWi{uDdqD0e+!Ekn{a&oe>nv?m`k-Gy|Tq3i~@gjiA?SZ2>L?x$8 zMF)_<-xn!cRg>yJe6xbLr;JEYvgDQ)3am}&_-y^TgOo#%JNP$o!WQM0U~H-S7_%9a z5D;V8U@EiIz5X?+0b{u_vWcnWf>mJx4jD!26`ykRH~vK58c&o&m-ux)m!BU;J~1j+ z#Z8L`M`@=vsSzYW_ug)L9msbysuBE>O?9ibAH1p(KwqY|t+bf)+~a2@F$!3TFdBWB zL_Agl&RMiOHu#JvY5opx0)<(EB;}==)_sj&X_kB9k0?CM&bI7|JdI&z)F7KN8S=Pj=gpxmezJ5?Uf^(G$l`O*cYo$k@)AP z97Iet35=Ex77l?t3dP`<{nOvtq=g{1Sh+BnL^KmZs7v9}yufwOsVGmY@=-5!9kx)^$RTIXd@SjRW^#??cr6x?>Ys-2 z(p-xXCunj+JWM_9#&_3{e*4tE8=ZQIZKz>Ej9o@jtJ-X4R{wOiCV*pz6-#`NJQb-C z;0PPnVzd*ajZC^qm{NC`nGpNQ0K%*WPLEBhkK8hizK!u!_wxVQX9q_|NeH4O+zQuJ zQ<90BZpAsv{p0uy_x`VJ4*V8pGwD_vKeqATF}{K!$HpLkYaweA98o(T5mh3 zFe@@EgR>2UYLdgjw-f=c_op$SU@lnzQ|vQ58cf$-ch5VGl!2hq zP%-xKqDVP`Dzu+;#CoHxFBp<7pdh2dGxu5?FIM**hklwXOv&5r9d6XV<91ylJxk$* zoJCs3ZC)R1^v6F2&%}tH?=UynTr<-T6J5wEshVhkKU3VL(63;$Dg{2Q`Id{HiZ}iY z?q4x^>%pRohspzYz>-EGz^H~06X&boPh#6Pbx);k&D8^g94gMwZR zh7yF)DdUx!rfcK>Qp}Qyf2s?=za_b{&GEl8zC5?59=f@eVYFtY&g%^kUZ8C~uGVFm zh}I~4Wj}h=BE98+ebO}i;&o2e+Md-Mtty>+8AeINDUR8X9|JZgq|S8#4P>E|>_Vl{PDX?ymKWV`M>%eKlY-;r#M_ES1Ve zFZ_()<2{`RM7PbLqMn%&(_1MQR+M2YRC7(ert?PpMG=iWimtjGE32){m;jgozce9hVRhz0e7 z4dOl!u7LUyJfI;UY`6X*?}apcZyzP$1S?f8t+QFkUHz%gpo){lK1)h)wMn7A`gQTVU>g*W$6?s;a1 z=Lnv<+jW6hdMV^VTW~W6ZV6Sd7o^UkUGEkroHC1RhO~*!>8Mg8<@-ix2auqLY^^7z%!A2FmL~5W~e>dHj}oG z8|&~nz(~vc+g!hVEfMB5tB{cG9jUFz@)Oe(gkCQTPDIRtO?nFr*nMX6B^eD4GZm-| zd@27>AExnrpDQ97mytCxbE&ahyG%0rR+mJl874Y2oBz6WKo)mKAb)&+r(;SA=c-CO z)!0AQRsI>1XwdJjh35j|(^pY!9rTJkDUEzVZ!%fT)rYaRz6 z#>f^n`<2>iYSqKTMduU-Hk>EP=ZDXCTc6^Nd__a2)Cz3GntrKpi0(Vx$BIW#)-%V) zW^#k7@l1&qYg8**@t$@)No;^gm&18ZfcZ2c4>%d!?(p_iVS=qHIWu0&F%y>ZHk>2@ zZx7Db6W)DsrQjTKH|BIJHAR;1ERaCar9~h2Fp`Po{ zmFyqpYCGSWj_3b7n-kh%w5QV`)HQpXM3hrJM>{^CW>9RozO9C)v-IdlIgYN*|n$zyB8 zrdTOLVRz_C#AfA_sCW9*^>9Vc%!jzwVuS&kXbOa~EfZ>UTMffffs?SDFK)p*0mOu4 zF}uOot$@dP6@$>#oE>s(phDk>n}!)IbU#OaxNI-gJ5#Rwem7j`n*s654*bX%W(WgL zL3HQM9b%N{2mI4v^I&E~SRftGq~0BTxje~SA8pr;%3uc7RvKyRY~^p7)ymV-Rw3$xd3M87!U@UYoIs!> z6~G%d?b$1#)pys37yYH#Hajn64v@KVrPAby;L=Yhl?)s4ETPU$;Hgn8B$%|lHIn%;qjIf^R&uC9QLS{{Y8e~D3T1_8 zz#kHNLx;vSEkmD(qKUam3+8a9tFl=5=&_uGA(P;cW#M#ReIZ$#f8;Lzf$>m~<1*I&pR z%(ZlAkQqwi4At+xsRe7#>=g@XBC0WGIC|m7X!l9dNOcDhJuS&9mJ75bEwP}!Qu6!b zGD$=nIPZhCTD}y$h~5Mhz_;lD5}0X!I;!~EKrd`Ut$eSdtqWf-!!kgvWxD+g`1KxM zt)tM*th!&JBSmc?a;q6d2U5md0V<~+K;>k~#mB0f$G{hw=9;%x7#k|gFM4(uyIkFj zcz;Hch>Zz>D0aL(q0WXpgv#PQ9fpjp1izRV-uWbGSJ?Hk?6h03xtvF83=1xTHbsa1 zJmlrgllzJV!mSr(Sq*jpF3p^g54nGl27Z0o%N~@rCQbK7=WB9XYK%vZ1qu*p(#21| z^m36YEjc1~y+SPTRB-+9IKk5)+`HRaKE$(cjI^oH$KLUz!FPm6<)0d@>B?7}+GpHs zC8AH$>vyeJ!X8*hf$$a-S%-(SRjTjw0r3Bb4}Wt!t&46?*qhGPS`%}zE?ltc(Y;oP z@StwWfT9`lljNi{>Iuf|4C#ykE$D6eV)N=0BI-5nJ%+7rX@shoRoEq_Fs3`x-H2Uw z(hNV)HM`Y<(A$T!C8WKCr~%@C^hoDRhkSd!A|r`~kUoCu!Z!!?B^?L%F z-HEclJ*FW;3_~ycl~W`XKj=`pZhkvdVV#Pxzm%hCfq=bhOKiT&+RS*jxaxkPZX3b2 zKNHsj)!zI(Rt~xsw`3bfHI7#j(>;OpOFOz`l zXXiRB!o9Xq>QVQ3KmBFM#j+MHNuN_|g#;0`E)UJ#lqFccBWzHPKGvTNIZ z%jnm@({o4Z5V5UAWc3KhAgT>WPb_fSf)99XFu0=fP9J)O{GZzQZ_UY_k~!aZ{e&DN zxJnPs#a>3ZWQA49X`~`>Ok@mG(#dS(o`r@stRZvB^MiEWWtU?YPgsdnFcLR+fs?7N1@Fglxd=%dRD*q0S9u*#SC+aoF zXcZMA2KiJ|_{kfpJ^y&ZV8I5E={(YuTagStYD--H;|YEwOB*j=oYWg>+y&hr7d)is zuk1y#Uwd%pH{!WN#zd4R$n}}Igr$~ksXz~actIl+Wn1(1YzMi8>_#EqBwFCapqr3E zi_%{%J-p}6sB&;bsYlchA{zVl3zjN@+$&b?cP~pxK$U|F_mgcbEp(^2-qFq{q^QL# zYZPg-`ARgKoKxM^>A0#~$?UYT|FOJKB87wnw3Uim>}|fm=T@(64DQmxN_$Z^3NbKe zRhLO|Fh9)is4WZbN~U2w$|7`stz5Fw;>7S!L?g=1OuWZwbCq2P5wld4X`mS8HH++F zduR0_(sKiKkCJ|vt%maf4e~neRX)24uRiCzkuU2U=tHTiq@laY`$vE8>zXA~!cgc3 zU8>SmmkUK~h9A}MCf~OcOCr{E_q1rPbA3#rbenun+Me*kivAn{hdhGcbG7Keg}G-w+1M=EJ5Q~dK0ZHJmtq^|CHY3J2lwWkQC9uU)#|X- zw?mJZ-9bf`&7zg0|E?b@5C^(PYdEh_+GrUD;{Tv$^FnXUo%HZ&=d!}GX@~WGd%}yR zHlllRv8GF`uFiDJqmysueZb$oFAom~${g_|4|EC)m&C-EeQ7ptHoei&HvNrYLL>9S zEqw><<9)R8$RCAo2f4e3y>f5a)%QF`MC2jJWE$j)i=!ZuSKL`P)BlG-{!#MRkKJpw zMeRsoyEuT6{#D!WhrdAL_31L7drSMD|G+{59>wSt@^8ET+gH5c;6Q@)bFcqqx_?wA zf`kNyc_;nvp9cEnJ2fTYL3*RYpGAp@u;LTS{Im1^RY@Q@Q1wKmtIQuoVZj8|bu0Z( zPk{vXGOJSkvnVXAX!x+if0g~uR_95Iu+DKl9lK-;u|L=;wzf}LP cjfy8Icmy6n4W*oMDBwr>y@GhPm{HLG0n+p}Bme*a literal 0 HcmV?d00001 From a950c77aba9a5c96da2ab325900b7fcb3a40305e Mon Sep 17 00:00:00 2001 From: Hendrik Muhs Date: Mon, 5 Dec 2022 17:32:58 +0100 Subject: [PATCH 150/919] enable debug logging to find the cause of #92069 (#92090) enable debug logging to find the cause of #92069 relates: #92069 --- .../transform/transforms/TransformIndexerStateTests.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/TransformIndexerStateTests.java b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/TransformIndexerStateTests.java index bedde8047a28..139f038b2aee 100644 --- a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/TransformIndexerStateTests.java +++ b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/TransformIndexerStateTests.java @@ -31,6 +31,7 @@ import org.elasticsearch.search.suggest.Suggest; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.client.NoOpClient; +import org.elasticsearch.test.junit.annotations.TestIssueLogging; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.indexing.IndexerState; @@ -501,6 +502,10 @@ public void testStopAtCheckpoint() throws Exception { } } + @TestIssueLogging( + value = "org.elasticsearch.xpack.transform.transforms:DEBUG", + issueUrl = "https://github.com/elastic/elasticsearch/issues/92069" + ) public void testStopAtCheckpointForThrottledTransform() throws Exception { TransformConfig config = new TransformConfig( randomAlphaOfLength(10), From ab4d737d8c4862c7503a4b1999b3046d94094473 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 5 Dec 2022 16:49:10 +0000 Subject: [PATCH 151/919] Ensure balance threshold is at least 1 (#92100) The balancer can get into a loop if the balance threshold is less than one. With this commit we force the value to be at least one, and emit a deprecation warning about smaller values so they can be properly rejected in a future version. --- docs/changelog/92100.yaml | 12 +++++++++ .../allocator/BalancedShardsAllocator.java | 27 ++++++++++++++++++- .../allocation/BalancedSingleShardTests.java | 4 +++ .../BalancedShardsAllocatorTests.java | 21 +++++++++++++++ 4 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/92100.yaml diff --git a/docs/changelog/92100.yaml b/docs/changelog/92100.yaml new file mode 100644 index 000000000000..7d6e312466e9 --- /dev/null +++ b/docs/changelog/92100.yaml @@ -0,0 +1,12 @@ +pr: 92100 +summary: Ensure balance threshold is at least 1 +area: Allocation +type: deprecation +issues: [] +deprecation: + title: Ensure balance threshold is at least 1 + area: Cluster and node setting + details: Values for `cluster.routing.allocation.balance.threshold` smaller than + `1` are now ignored. Support for values less than `1` for this setting is deprecated + and will be forbidden in a future version. + impact: Set `cluster.routing.allocation.balance.threshold` to be at least `1`. diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocator.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocator.java index 1586b0ed6068..156a34410967 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocator.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocator.java @@ -34,6 +34,8 @@ import org.elasticsearch.cluster.routing.allocation.decider.Decision.Type; import org.elasticsearch.cluster.routing.allocation.decider.DiskThresholdDecider; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.logging.DeprecationCategory; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; @@ -132,7 +134,7 @@ public BalancedShardsAllocator(Settings settings, ClusterSettings clusterSetting watchSetting(settings, clusterSettings, SHARD_BALANCE_FACTOR_SETTING, value -> this.shardBalanceFactor = value); watchSetting(settings, clusterSettings, WRITE_LOAD_BALANCE_FACTOR_SETTING, value -> this.writeLoadBalanceFactor = value); watchSetting(settings, clusterSettings, DISK_USAGE_BALANCE_FACTOR_SETTING, value -> this.diskUsageBalanceFactor = value); - watchSetting(settings, clusterSettings, THRESHOLD_SETTING, value -> this.threshold = value); + watchSetting(settings, clusterSettings, THRESHOLD_SETTING, value -> this.threshold = ensureValidThreshold(value)); this.writeLoadForecaster = writeLoadForecaster; } @@ -141,6 +143,29 @@ private void watchSetting(Settings settings, ClusterSettings clusterSettings clusterSettings.addSettingsUpdateConsumer(setting, consumer); } + /** + * Clamp threshold to be at least 1, and log a critical deprecation warning if smaller values are given. + * + * Once {@link org.elasticsearch.Version#V_7_17_0} goes out of scope, start to properly reject such bad values. + */ + private static float ensureValidThreshold(float threshold) { + if (1.0f <= threshold) { + return threshold; + } else { + DeprecationLogger.getLogger(BalancedShardsAllocator.class) + .critical( + DeprecationCategory.SETTINGS, + "balance_threshold_too_small", + "ignoring value [{}] for [{}] since it is smaller than 1.0; " + + "setting [{}] to a value smaller than 1.0 will be forbidden in a future release", + threshold, + THRESHOLD_SETTING.getKey(), + THRESHOLD_SETTING.getKey() + ); + return 1.0f; + } + } + @Override public void allocate(RoutingAllocation allocation) { assert allocation.ignoreDisable() == false; diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/BalancedSingleShardTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/BalancedSingleShardTests.java index 7efac54bf3b8..7378ca56d2a4 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/BalancedSingleShardTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/BalancedSingleShardTests.java @@ -298,6 +298,10 @@ public void testNodeDecisionsRanking() { assertTrue(nodesWithTwoShards.contains(result.getNode().getId())); } } + + assertCriticalWarnings(""" + ignoring value [0.01] for [cluster.routing.allocation.balance.threshold] since it is smaller than 1.0; setting \ + [cluster.routing.allocation.balance.threshold] to a value smaller than 1.0 will be forbidden in a future release"""); } private MoveDecision executeRebalanceFor( diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocatorTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocatorTests.java index 6b23e5126655..113b7b369a72 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocatorTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/BalancedShardsAllocatorTests.java @@ -315,6 +315,27 @@ public void testGetIndexDiskUsageInBytes() { } } + public void testThresholdLimit() { + final var badValue = (float) randomDoubleBetween(0.0, Math.nextDown(1.0f), true); + assertEquals( + 1.0f, + new BalancedShardsAllocator(Settings.builder().put(BalancedShardsAllocator.THRESHOLD_SETTING.getKey(), badValue).build()) + .getThreshold(), + 0.0f + ); + assertCriticalWarnings("ignoring value [" + badValue + """ + ] for [cluster.routing.allocation.balance.threshold] since it is smaller than 1.0; setting \ + [cluster.routing.allocation.balance.threshold] to a value smaller than 1.0 will be forbidden in a future release"""); + + final var goodValue = (float) randomDoubleBetween(1.0, 10.0, true); + assertEquals( + goodValue, + new BalancedShardsAllocator(Settings.builder().put(BalancedShardsAllocator.THRESHOLD_SETTING.getKey(), goodValue).build()) + .getThreshold(), + 0.0f + ); + } + private Map getTargetShardPerNodeCount(IndexRoutingTable indexRoutingTable) { var counts = new HashMap(); for (int shardId = 0; shardId < indexRoutingTable.size(); shardId++) { From 801807727ac173f7a8b85c49b3e1b136f3bd19ce Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Mon, 5 Dec 2022 16:50:32 +0000 Subject: [PATCH 152/919] [Fleet] Add files and files data index templates and ILM policies (#91413) * add files system index templates and ilm policies * add tests * fix templates * fix index templates * fix index pattern * fix import order * mark .fleet index templates as xpack * implement suggestions made by @andreidan * spotlessApply * update SearchUsageStatsIT * add collectors import * add missing docs * spotless * Supress correct warnings * spotless * fix warnings * line length * do not resuse req options * add logging * move to new var * fix assertions * fix fileData assertion * fix version templating * Update docs/changelog/91413.yaml * remove docs not needed * delete files after 7d --- docs/changelog/91413.yaml | 5 ++ .../test/rest/ESRestTestCase.java | 5 ++ .../resources/fleet-file-data-ilm-policy.json | 25 ++++++ .../src/main/resources/fleet-file-data.json | 38 +++++++++ .../resources/fleet-files-ilm-policy.json | 25 ++++++ .../core/src/main/resources/fleet-files.json | 53 +++++++++++++ x-pack/plugin/fleet/build.gradle | 1 + .../action/GetGlobalCheckpointsActionIT.java | 4 +- .../fleet/action/SearchUsageStatsIT.java | 7 +- .../xpack/fleet/FleetSystemIndicesIT.java | 78 ++++++++++++++++++- .../xpack/fleet/FleetTemplateRegistry.java | 23 ++++++ .../plugin/ilm/src/main/java/module-info.java | 2 +- 12 files changed, 261 insertions(+), 5 deletions(-) create mode 100644 docs/changelog/91413.yaml create mode 100644 x-pack/plugin/core/src/main/resources/fleet-file-data-ilm-policy.json create mode 100644 x-pack/plugin/core/src/main/resources/fleet-file-data.json create mode 100644 x-pack/plugin/core/src/main/resources/fleet-files-ilm-policy.json create mode 100644 x-pack/plugin/core/src/main/resources/fleet-files.json diff --git a/docs/changelog/91413.yaml b/docs/changelog/91413.yaml new file mode 100644 index 000000000000..4a6d68004920 --- /dev/null +++ b/docs/changelog/91413.yaml @@ -0,0 +1,5 @@ +pr: 91413 +summary: "[Fleet] Add files and files data index templates and ILM policies" +area: Infra/Plugins +type: feature +issues: [] diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java index 8ef88d81f4e3..1b2bf23dd63f 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java @@ -602,6 +602,8 @@ protected Set preserveILMPolicyIds() { "180-days-default", "365-days-default", ".fleet-actions-results-ilm-policy", + ".fleet-file-data-ilm-policy", + ".fleet-files-ilm-policy", ".deprecation-indexing-ilm-policy", ".monitoring-8-ilm-policy" ); @@ -1774,6 +1776,9 @@ protected static boolean isXPackTemplate(String name) { if (name.startsWith(".deprecation-")) { return true; } + if (name.startsWith(".fleet-")) { + return true; + } switch (name) { case ".watches": case "security_audit_log": diff --git a/x-pack/plugin/core/src/main/resources/fleet-file-data-ilm-policy.json b/x-pack/plugin/core/src/main/resources/fleet-file-data-ilm-policy.json new file mode 100644 index 000000000000..7ea1aff57d7e --- /dev/null +++ b/x-pack/plugin/core/src/main/resources/fleet-file-data-ilm-policy.json @@ -0,0 +1,25 @@ +{ + "phases": { + "hot": { + "min_age": "0ms", + "actions": { + "rollover": { + "max_size": "10gb", + "max_age": "7d" + } + } + }, + "delete": { + "min_age": "7d", + "actions": { + "delete": { + "delete_searchable_snapshot": true + } + } + } + }, + "_meta": { + "description": "policy for fleet uploaded files", + "managed": true + } +} \ No newline at end of file diff --git a/x-pack/plugin/core/src/main/resources/fleet-file-data.json b/x-pack/plugin/core/src/main/resources/fleet-file-data.json new file mode 100644 index 000000000000..973edfa92ea6 --- /dev/null +++ b/x-pack/plugin/core/src/main/resources/fleet-file-data.json @@ -0,0 +1,38 @@ +{ + "index_patterns": [ + ".fleet-file-data-*-*" + ], + "priority": 200, + "composed_of": [], + "_meta": { + "description": "fleet file data index template", + "managed": true + }, + "template" : { + "settings": { + "index.lifecycle.name": ".fleet-file-data-ilm-policy", + "auto_expand_replicas": "0-1" + }, + "mappings": { + "_doc": { + "_meta": { + "version": "${xpack.fleet.template.version}" + }, + "properties": { + "data": { + "type": "binary" + }, + "bid": { + "type": "keyword", + "index": false + }, + "last": { + "type": "boolean", + "index": false + } + } + } + } + }, + "version": ${xpack.fleet.template.version} +} \ No newline at end of file diff --git a/x-pack/plugin/core/src/main/resources/fleet-files-ilm-policy.json b/x-pack/plugin/core/src/main/resources/fleet-files-ilm-policy.json new file mode 100644 index 000000000000..98722614ebc2 --- /dev/null +++ b/x-pack/plugin/core/src/main/resources/fleet-files-ilm-policy.json @@ -0,0 +1,25 @@ +{ + "phases": { + "hot": { + "min_age": "0ms", + "actions": { + "rollover": { + "max_size": "10gb", + "max_age": "30d" + } + } + }, + "delete": { + "min_age": "90d", + "actions": { + "delete": { + "delete_searchable_snapshot": true + } + } + } + }, + "_meta": { + "description": "policy for fleet uploaded file metadata", + "managed": true + } +} \ No newline at end of file diff --git a/x-pack/plugin/core/src/main/resources/fleet-files.json b/x-pack/plugin/core/src/main/resources/fleet-files.json new file mode 100644 index 000000000000..b2a8834b8113 --- /dev/null +++ b/x-pack/plugin/core/src/main/resources/fleet-files.json @@ -0,0 +1,53 @@ +{ + "index_patterns": [ + ".fleet-files-*-*" + ], + "priority": 200, + "composed_of": [], + "_meta": { + "description": "fleet files index template", + "managed": true + }, + "template": { + "settings": { + "index.lifecycle.name": ".fleet-files-ilm-policy", + "index.auto_expand_replicas": "0-1" + }, + "mappings": { + "_doc": { + "_meta": { + "version": "${xpack.fleet.template.version}" + }, + "dynamic": false, + "properties": { + "agent_id": { + "type": "keyword" + }, + "action_id": { + "type": "keyword" + }, + "source": { + "type": "keyword" + }, + "file": { + "properties": { + "Status": { + "type": "keyword" + }, + "ChunkSize": { + "type": "integer" + }, + "Compression": { + "type": "keyword" + }, + "name": { + "type": "keyword" + } + } + } + } + } + } + }, + "version": ${xpack.fleet.template.version} +} \ No newline at end of file diff --git a/x-pack/plugin/fleet/build.gradle b/x-pack/plugin/fleet/build.gradle index 71b023106fe6..f54ee022a744 100644 --- a/x-pack/plugin/fleet/build.gradle +++ b/x-pack/plugin/fleet/build.gradle @@ -21,6 +21,7 @@ dependencies { testImplementation(testArtifact(project(xpackModule('core')))) javaRestTestImplementation(project(path: xpackModule('core'))) javaRestTestImplementation(testArtifact(project(xpackModule('core')))) + compileOnly project(path: xpackModule('ilm')) } testClusters.configureEach { diff --git a/x-pack/plugin/fleet/src/internalClusterTest/java/org/elasticsearch/xpack/fleet/action/GetGlobalCheckpointsActionIT.java b/x-pack/plugin/fleet/src/internalClusterTest/java/org/elasticsearch/xpack/fleet/action/GetGlobalCheckpointsActionIT.java index 989d4d10dc38..7b90e41584cf 100644 --- a/x-pack/plugin/fleet/src/internalClusterTest/java/org/elasticsearch/xpack/fleet/action/GetGlobalCheckpointsActionIT.java +++ b/x-pack/plugin/fleet/src/internalClusterTest/java/org/elasticsearch/xpack/fleet/action/GetGlobalCheckpointsActionIT.java @@ -23,7 +23,9 @@ import org.elasticsearch.rest.RestStatus; import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.xcontent.XContentType; +import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; import org.elasticsearch.xpack.fleet.Fleet; +import org.elasticsearch.xpack.ilm.IndexLifecycle; import java.util.Arrays; import java.util.Collection; @@ -43,7 +45,7 @@ public class GetGlobalCheckpointsActionIT extends ESIntegTestCase { @Override protected Collection> nodePlugins() { - return Stream.of(Fleet.class).collect(Collectors.toList()); + return Stream.of(Fleet.class, LocalStateCompositeXPackPlugin.class, IndexLifecycle.class).collect(Collectors.toList()); } public void testGetGlobalCheckpoints() throws Exception { diff --git a/x-pack/plugin/fleet/src/internalClusterTest/java/org/elasticsearch/xpack/fleet/action/SearchUsageStatsIT.java b/x-pack/plugin/fleet/src/internalClusterTest/java/org/elasticsearch/xpack/fleet/action/SearchUsageStatsIT.java index 0dc6a7a51929..0dbd745a5b0b 100644 --- a/x-pack/plugin/fleet/src/internalClusterTest/java/org/elasticsearch/xpack/fleet/action/SearchUsageStatsIT.java +++ b/x-pack/plugin/fleet/src/internalClusterTest/java/org/elasticsearch/xpack/fleet/action/SearchUsageStatsIT.java @@ -16,11 +16,14 @@ import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; import org.elasticsearch.xpack.fleet.Fleet; +import org.elasticsearch.xpack.ilm.IndexLifecycle; import java.io.IOException; import java.util.Collection; -import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; @ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 1) public class SearchUsageStatsIT extends ESIntegTestCase { @@ -32,7 +35,7 @@ protected boolean addMockHttpTransport() { @Override protected Collection> nodePlugins() { - return List.of(Fleet.class); + return Stream.of(Fleet.class, LocalStateCompositeXPackPlugin.class, IndexLifecycle.class).collect(Collectors.toList()); } public void testSearchUsageStats() throws IOException { diff --git a/x-pack/plugin/fleet/src/javaRestTest/java/org/elasticsearch/xpack/fleet/FleetSystemIndicesIT.java b/x-pack/plugin/fleet/src/javaRestTest/java/org/elasticsearch/xpack/fleet/FleetSystemIndicesIT.java index c40179f3a843..3d37ca7b7359 100644 --- a/x-pack/plugin/fleet/src/javaRestTest/java/org/elasticsearch/xpack/fleet/FleetSystemIndicesIT.java +++ b/x-pack/plugin/fleet/src/javaRestTest/java/org/elasticsearch/xpack/fleet/FleetSystemIndicesIT.java @@ -9,6 +9,7 @@ import org.apache.http.util.EntityUtils; import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.Response; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; @@ -17,10 +18,12 @@ import org.elasticsearch.test.rest.ESRestTestCase; import org.elasticsearch.xcontent.XContentType; +import java.util.List; import java.util.Map; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; public class FleetSystemIndicesIT extends ESRestTestCase { @@ -37,6 +40,22 @@ protected Settings restClientSettings() { .build(); } + private void expectSystemIndexWarning(Request request, String indexName) { + String warningMsg = "index name [" + + indexName + + "] starts with a dot '.', in the next major version, " + + "index names starting with a dot are reserved for hidden indices and system indices"; + + List expectedWarnings = List.of(warningMsg); + + logger.info("expecting warnings: " + expectedWarnings.toString()); + RequestOptions consumeSystemIndicesWarningsOptions = RequestOptions.DEFAULT.toBuilder() + .setWarningsHandler(warnings -> expectedWarnings.equals(warnings) == false) + .build(); + + request.setOptions(consumeSystemIndicesWarningsOptions); + } + public void testSearchWithoutIndexCreatedIsAllowed() throws Exception { Request request = new Request("GET", ".fleet-agents/_search"); request.setJsonEntity("{ \"query\": { \"match_all\": {} } }"); @@ -77,6 +96,33 @@ public void testCreationOfFleetActions() throws Exception { assertThat(responseBody, containsString("action_id")); } + public void testCreationOfFleetFiles() throws Exception { + Request request = new Request("PUT", ".fleet-files-agent-00001"); + expectSystemIndexWarning(request, ".fleet-files-agent-00001"); + Response response = client().performRequest(request); + assertEquals(200, response.getStatusLine().getStatusCode()); + + request = new Request("GET", ".fleet-files-agent-00001/_mapping"); + response = client().performRequest(request); + String responseBody = EntityUtils.toString(response.getEntity()); + assertThat(responseBody, not(containsString("xpack.fleet.template.version"))); // assert templating worked + assertThat(responseBody, containsString("action_id")); + } + + public void testCreationOfFleetFileData() throws Exception { + Request request = new Request("PUT", ".fleet-file-data-agent-00001"); + expectSystemIndexWarning(request, ".fleet-file-data-agent-00001"); + Response response = client().performRequest(request); + assertEquals(200, response.getStatusLine().getStatusCode()); + + request = new Request("GET", ".fleet-file-data-agent-00001/_mapping"); + response = client().performRequest(request); + String responseBody = EntityUtils.toString(response.getEntity()); + assertThat(responseBody, not(containsString("xpack.fleet.template.version"))); // assert templating worked + assertThat(responseBody, containsString("data")); + assertThat(responseBody, containsString("bid")); + } + public void testCreationOfFleetArtifacts() throws Exception { Request request = new Request("PUT", ".fleet-artifacts"); Response response = client().performRequest(request); @@ -165,7 +211,7 @@ public void testCreationOfFleetActionsResults() throws Exception { } @SuppressWarnings("unchecked") - public void verifyILMPolicyExists() throws Exception { + public void verifyActionsILMPolicyExists() throws Exception { assertBusy(() -> { Request request = new Request("GET", "_ilm/policy/.fleet-actions-results-ilm-policy"); Response response = client().performRequest(request); @@ -178,4 +224,34 @@ public void verifyILMPolicyExists() throws Exception { assertThat(policyMap.size(), equalTo(2)); }); } + + @SuppressWarnings("unchecked") + public void verifyFilesILMPolicyExists() throws Exception { + assertBusy(() -> { + Request request = new Request("GET", "_ilm/policy/.fleet-files-ilm-policy"); + Response response = client().performRequest(request); + assertEquals(200, response.getStatusLine().getStatusCode()); + final String responseJson = EntityUtils.toString(response.getEntity()); + Map responseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), responseJson, false); + assertNotNull(responseMap.get(".fleet-files-ilm-policy")); + Map policyMap = (Map) responseMap.get(".fleet-files-ilm-policy"); + assertNotNull(policyMap); + assertThat(policyMap.size(), equalTo(2)); + }); + } + + @SuppressWarnings("unchecked") + public void verifyFileDataILMPolicyExists() throws Exception { + assertBusy(() -> { + Request request = new Request("GET", "_ilm/policy/.fleet-file-data-ilm-policy"); + Response response = client().performRequest(request); + assertEquals(200, response.getStatusLine().getStatusCode()); + final String responseJson = EntityUtils.toString(response.getEntity()); + Map responseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), responseJson, false); + assertNotNull(responseMap.get(".fleet-file-data-ilm-policy")); + Map policyMap = (Map) responseMap.get(".fleet-file-data-ilm-policy"); + assertNotNull(policyMap); + assertThat(policyMap.size(), equalTo(2)); + }); + } } diff --git a/x-pack/plugin/fleet/src/main/java/org/elasticsearch/xpack/fleet/FleetTemplateRegistry.java b/x-pack/plugin/fleet/src/main/java/org/elasticsearch/xpack/fleet/FleetTemplateRegistry.java index 5b5b1e45aedc..398528ba05a4 100644 --- a/x-pack/plugin/fleet/src/main/java/org/elasticsearch/xpack/fleet/FleetTemplateRegistry.java +++ b/x-pack/plugin/fleet/src/main/java/org/elasticsearch/xpack/fleet/FleetTemplateRegistry.java @@ -8,25 +8,43 @@ package org.elasticsearch.xpack.fleet; import org.elasticsearch.client.internal.Client; +import org.elasticsearch.cluster.metadata.ComposableIndexTemplate; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xpack.core.ClientHelper; import org.elasticsearch.xpack.core.ilm.LifecyclePolicy; +import org.elasticsearch.xpack.core.template.IndexTemplateConfig; import org.elasticsearch.xpack.core.template.IndexTemplateRegistry; import org.elasticsearch.xpack.core.template.LifecyclePolicyConfig; import java.util.List; +import java.util.Map; public class FleetTemplateRegistry extends IndexTemplateRegistry { + public static final int INDEX_TEMPLATE_VERSION = 1; + + public static final String TEMPLATE_VERSION_VARIABLE = "xpack.fleet.template.version"; + private static final List LIFECYCLE_POLICIES = List.of( new LifecyclePolicyConfig(".fleet-actions-results-ilm-policy", "/fleet-actions-results-ilm-policy.json").load( LifecyclePolicyConfig.DEFAULT_X_CONTENT_REGISTRY + ), + new LifecyclePolicyConfig(".fleet-file-data-ilm-policy", "/fleet-file-data-ilm-policy.json").load( + LifecyclePolicyConfig.DEFAULT_X_CONTENT_REGISTRY + ), + new LifecyclePolicyConfig(".fleet-files-ilm-policy", "/fleet-files-ilm-policy.json").load( + LifecyclePolicyConfig.DEFAULT_X_CONTENT_REGISTRY ) ); + public static final Map COMPOSABLE_INDEX_TEMPLATE_CONFIGS = parseComposableTemplates( + new IndexTemplateConfig(".fleet-files", "/fleet-files.json", INDEX_TEMPLATE_VERSION, TEMPLATE_VERSION_VARIABLE), + new IndexTemplateConfig(".fleet-file-data", "/fleet-file-data.json", INDEX_TEMPLATE_VERSION, TEMPLATE_VERSION_VARIABLE) + ); + public FleetTemplateRegistry( Settings nodeSettings, ClusterService clusterService, @@ -46,4 +64,9 @@ protected String getOrigin() { protected List getPolicyConfigs() { return LIFECYCLE_POLICIES; } + + @Override + protected Map getComposableTemplateConfigs() { + return COMPOSABLE_INDEX_TEMPLATE_CONFIGS; + } } diff --git a/x-pack/plugin/ilm/src/main/java/module-info.java b/x-pack/plugin/ilm/src/main/java/module-info.java index 7909ffc280e0..80ab9c5386f4 100644 --- a/x-pack/plugin/ilm/src/main/java/module-info.java +++ b/x-pack/plugin/ilm/src/main/java/module-info.java @@ -13,7 +13,7 @@ requires org.apache.logging.log4j; exports org.elasticsearch.xpack.ilm.action to org.elasticsearch.server; - exports org.elasticsearch.xpack.ilm to org.elasticsearch.server; + exports org.elasticsearch.xpack.ilm; exports org.elasticsearch.xpack.slm.action to org.elasticsearch.server; exports org.elasticsearch.xpack.slm to org.elasticsearch.server; From b4726ec7e547d3d29c682f98bbc6e13438fb804e Mon Sep 17 00:00:00 2001 From: Keith Massey Date: Mon, 5 Dec 2022 14:58:24 -0600 Subject: [PATCH 153/919] Upgrading tika to 2.6.0 (#92104) Upgrading tika and its dependencies. --- docs/changelog/92104.yaml | 5 ++ gradle/verification-metadata.xml | 84 +++++++++++++------------- modules/ingest-attachment/build.gradle | 6 +- 3 files changed, 50 insertions(+), 45 deletions(-) create mode 100644 docs/changelog/92104.yaml diff --git a/docs/changelog/92104.yaml b/docs/changelog/92104.yaml new file mode 100644 index 000000000000..19c66c14773d --- /dev/null +++ b/docs/changelog/92104.yaml @@ -0,0 +1,5 @@ +pr: 92104 +summary: Upgrading tika to 2.6.0 +area: Ingest Node +type: upgrade +issues: [] diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 78110446539c..49b2e15bd85f 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2766,19 +2766,19 @@ - - - + + + - - - + + + - - - + + + @@ -2826,59 +2826,59 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + diff --git a/modules/ingest-attachment/build.gradle b/modules/ingest-attachment/build.gradle index 64d74ef8b356..4db193b6202a 100644 --- a/modules/ingest-attachment/build.gradle +++ b/modules/ingest-attachment/build.gradle @@ -16,8 +16,8 @@ esplugin { } versions << [ - 'tika' : '2.4.0', - 'pdfbox': '2.0.26', + 'tika' : '2.6.0', + 'pdfbox': '2.0.27', 'poi' : '5.2.2', 'mime4j': '0.8.5' ] @@ -49,7 +49,7 @@ dependencies { // Adobe PDF api "org.apache.pdfbox:pdfbox:${versions.pdfbox}" api "org.apache.pdfbox:fontbox:${versions.pdfbox}" - api "org.apache.pdfbox:jempbox:1.8.16" + api "org.apache.pdfbox:jempbox:1.8.17" api "commons-logging:commons-logging:${versions.commonslogging}" // OpenOffice api "org.apache.poi:poi-ooxml:${versions.poi}" From 8c77fe5ba1025545147143ffcaa9c3697c4cfe59 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 6 Dec 2022 17:22:03 +1100 Subject: [PATCH 154/919] Improve performance for role mapping with DNs (#92074) DNs in a role mapping are parsed each time a matching is performed against a user group. If there are large number of role mappings and large number of user groups, this process can be quite slow since DN parsing is non-trivial. This PR improves the performance by introducing a cache for DN parsing so that an unique DN from all role mappings is only parsed once per user (regardless of how many groups the user has). The cache stores the normalized string format of the DN instead of the DN object itself since DN is rather memory expensive. The cache's lifecycle is tied to the UserData model which goes out of scope once role mapping process is completed. Hence it is short-lived and does not need to externally managed. Thanks to @tvernum for the original idea. Co-authored-by: Tim Vernum --- docs/changelog/92074.yaml | 5 + .../authc/support/UserRoleMapper.java | 118 ++++++++---- .../DistinguishedNameNormalizerTests.java | 170 ++++++++++++++++++ .../DistinguishedNamePredicateTests.java | 15 +- 4 files changed, 273 insertions(+), 35 deletions(-) create mode 100644 docs/changelog/92074.yaml create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/support/DistinguishedNameNormalizerTests.java diff --git a/docs/changelog/92074.yaml b/docs/changelog/92074.yaml new file mode 100644 index 000000000000..a8ce59b7c3a0 --- /dev/null +++ b/docs/changelog/92074.yaml @@ -0,0 +1,5 @@ +pr: 92074 +summary: Improve performance for role mapping with DNs +area: Authentication +type: bug +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UserRoleMapper.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UserRoleMapper.java index 638495ac2146..1325eeef5dfb 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UserRoleMapper.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UserRoleMapper.java @@ -20,7 +20,9 @@ import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression; import org.elasticsearch.xpack.core.security.authz.permission.Role; +import java.lang.ref.SoftReference; import java.util.Collection; +import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Set; @@ -74,15 +76,16 @@ public UserData(String username, @Nullable String dn, Collection groups, */ public ExpressionModel asModel() { final ExpressionModel model = new ExpressionModel(); + final DistinguishedNameNormalizer dnNormalizer = getDnNormalizer(); model.defineField("username", username); if (dn != null) { // null dn fields get the default NULL_PREDICATE - model.defineField("dn", dn, new DistinguishedNamePredicate(dn)); + model.defineField("dn", dn, new DistinguishedNamePredicate(dn, dnNormalizer)); } model.defineField( "groups", groups, - groups.stream().>map(DistinguishedNamePredicate::new) + groups.stream().>map(g -> new DistinguishedNamePredicate(g, dnNormalizer)) .reduce(Predicate::or) .orElse(fieldValue -> false) ); @@ -145,6 +148,67 @@ public Map getMetadata() { public RealmConfig getRealm() { return realm; } + + // Package private for testing + DistinguishedNameNormalizer getDnNormalizer() { + return new DistinguishedNameNormalizer(); + } + } + + /** + * This class parse the given string into a DN and return its normalized format. + * If the input string is not a valid DN, {@code null} is returned. + * The DN parsing and normalization are cached internally so that the same + * input string will only be processed once (as long as the cache entry is not GC'd). + * The cache works regardless of whether the input string is a valid DN. + * + * The cache uses {@link SoftReference} for its values so that they free for GC. + * This is to prevent potential memory pressure when there are many concurrent role + * mapping processes coupled with large number of groups and role mappings, which + * in theory is unbounded. + */ + class DistinguishedNameNormalizer { + private static final Logger LOGGER = LogManager.getLogger(DistinguishedNameNormalizer.class); + private static final SoftReference NULL_REF = new SoftReference<>(null); + private final Map> cache = new HashMap<>(); + + /** + * Parse the input string to a DN and returns its normalized form. + * @param str String that may represent a DN + * @return The normalized DN form of the input string or {@code null} if input string is not a DN + */ + public String normalize(String str) { + final SoftReference normalizedDnRef = cache.get(str); + if (normalizedDnRef == NULL_REF) { + return null; + } + if (normalizedDnRef != null) { + final String normalizedDn = normalizedDnRef.get(); + if (normalizedDn != null) { + return normalizedDn; + } + } + final String normalizedDn = doNormalize(str); + if (normalizedDn == null) { + cache.put(str, NULL_REF); + } else { + cache.put(str, new SoftReference<>(normalizedDn)); + } + return normalizedDn; + } + + String doNormalize(String str) { + final DN dn; + try { + dn = new DN(str); + } catch (LDAPException | LDAPSDKUsageException e) { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace(() -> "failed to parse [" + str + "] as a DN", e); + } + return null; + } + return dn.toNormalizedString(); + } } /** @@ -165,26 +229,16 @@ public RealmConfig getRealm() { * */ class DistinguishedNamePredicate implements Predicate { - private static final Logger LOGGER = LogManager.getLogger(DistinguishedNamePredicate.class); private final String string; - private final DN dn; + private final DistinguishedNameNormalizer dnNormalizer; + private final String normalizedDn; - public DistinguishedNamePredicate(String string) { + public DistinguishedNamePredicate(String string, DistinguishedNameNormalizer dnNormalizer) { assert string != null : "DN string should not be null. Use the dedicated NULL_PREDICATE for every user null field."; this.string = string; - this.dn = parseDn(string); - } - - private static DN parseDn(String string) { - try { - return new DN(string); - } catch (LDAPException | LDAPSDKUsageException e) { - if (LOGGER.isTraceEnabled()) { - LOGGER.trace(() -> "failed to parse [" + string + "] as a DN", e); - } - return null; - } + this.dnNormalizer = dnNormalizer; + this.normalizedDn = dnNormalizer.normalize(string); } @Override @@ -199,13 +253,13 @@ public boolean test(FieldExpression.FieldValue fieldValue) { if (automaton.run(string)) { return true; } - if (dn != null && automaton.run(dn.toNormalizedString())) { + if (normalizedDn != null && automaton.run(normalizedDn)) { return true; } if (automaton.run(string.toLowerCase(Locale.ROOT)) || automaton.run(string.toUpperCase(Locale.ROOT))) { return true; } - if (dn == null) { + if (normalizedDn == null) { return false; } @@ -217,36 +271,38 @@ public boolean test(FieldExpression.FieldValue fieldValue) { String pattern = (String) fieldValue.getValue(); // If the pattern is "*,dc=example,dc=com" then the rule is actually trying to express a DN sub-tree match. - // We can use dn.isDescendantOf for that if (pattern.startsWith("*,")) { final String suffix = pattern.substring(2); // if the suffix has a wildcard, then it's not a pure sub-tree match if (suffix.indexOf('*') == -1) { - final DN dnSuffix = parseDn(suffix); - if (dnSuffix != null && dn.isDescendantOf(dnSuffix, false)) { - return true; - } + return isDescendantOf(dnNormalizer.normalize(suffix)); } } return false; } - if (fieldValue.getValue() instanceof String) { - final String testString = (String) fieldValue.getValue(); + if (fieldValue.getValue()instanceof final String testString) { if (testString.equalsIgnoreCase(string)) { return true; } - if (dn == null) { + if (normalizedDn == null) { return false; } - final DN testDn = parseDn(testString); - if (testDn != null) { - return dn.equals(testDn); + final String testNormalizedDn = dnNormalizer.normalize(testString); + if (testNormalizedDn != null) { + return normalizedDn.equals(testNormalizedDn); } - return testString.equalsIgnoreCase(dn.toNormalizedString()); + return testString.equalsIgnoreCase(normalizedDn); } return false; } + + private boolean isDescendantOf(String normalizedDnSuffix) { + if (normalizedDnSuffix == null) { + return false; + } + return normalizedDn.endsWith("," + normalizedDnSuffix) || (normalizedDnSuffix.isEmpty() && false == normalizedDn.isEmpty()); + } } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/support/DistinguishedNameNormalizerTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/support/DistinguishedNameNormalizerTests.java new file mode 100644 index 000000000000..f5fb9c96a6e8 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/support/DistinguishedNameNormalizerTests.java @@ -0,0 +1,170 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.security.authc.support; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.ExpressionModel; +import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression.FieldValue; +import org.junit.Before; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.IntStream; + +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class DistinguishedNameNormalizerTests extends ESTestCase { + + private UserRoleMapper.DistinguishedNameNormalizer dnNormalizer; + + @Before + public void init() { + dnNormalizer = getDnNormalizer(); + } + + public void testDnNormalizingIsCached() { + // Parse same DN multiple times, only 1st time DN parsing is performed, 2nd time reads from the cache + Mockito.clearInvocations(dnNormalizer); + final String dn = randomDn(); + parseDnMultipleTimes(dn); + verify(dnNormalizer, times(1)).doNormalize(dn); + + // The cache is keyed by the literal string form. + // Therefore if the literal string changes, it needs to be parsed again even though it is still the same DN + Mockito.clearInvocations(dnNormalizer); + final String mutatedDn = mutateDn(dn); + parseDnMultipleTimes(mutatedDn); + verify(dnNormalizer, times(1)).doNormalize(mutatedDn); + + // Invalid DNs should also be cached + Mockito.clearInvocations(dnNormalizer); + final String invalidDn = randomFrom( + "", + randomAlphaOfLengthBetween(1, 8), + randomAlphaOfLengthBetween(1, 8) + "*", + randomAlphaOfLengthBetween(1, 8) + "=", + "=" + randomAlphaOfLengthBetween(1, 8) + ); + parseDnMultipleTimes(invalidDn); + verify(dnNormalizer, times(1)).doNormalize(invalidDn); + } + + public void testDnNormalizingIsCachedForDnPredicate() { + final String dn = randomDn(); + final Predicate predicate = new UserRoleMapper.DistinguishedNamePredicate(dn, dnNormalizer); + verify(dnNormalizer, times(1)).doNormalize(dn); + + // Same DN, it's cached + runPredicateMultipleTimes(predicate, dn); + verify(dnNormalizer, times(1)).doNormalize(dn); + + // Predicate short-circuits for case differences + Mockito.clearInvocations(dnNormalizer); + final String casedDn = randomFrom(dn.toLowerCase(Locale.ENGLISH), dn.toUpperCase(Locale.ENGLISH)); + runPredicateMultipleTimes(predicate, casedDn); + verify(dnNormalizer, never()).doNormalize(anyString()); + + // Literal string form changes, it will be parsed again + Mockito.clearInvocations(dnNormalizer); + final String mutatedDn = randomFrom(dn.replace(" ", ""), dn.replace(",", " ,")); + runPredicateMultipleTimes(predicate, mutatedDn); + verify(dnNormalizer, times(1)).doNormalize(mutatedDn); + + // Subtree DN is also cached + Mockito.clearInvocations(dnNormalizer); + final String subtreeDn = "*," + randomDn(); + runPredicateMultipleTimes(predicate, subtreeDn); + verify(dnNormalizer, times(1)).doNormalize(subtreeDn.substring(2)); + + // Subtree DN is also keyed by the literal form, so they are space sensitive + Mockito.clearInvocations(dnNormalizer); + final String mutatedSubtreeDn = "*, " + subtreeDn.substring(2); + runPredicateMultipleTimes(predicate, mutatedSubtreeDn); + verify(dnNormalizer, times(1)).doNormalize(mutatedSubtreeDn.substring(2)); + } + + public void testUserDataUsesCachedDnNormalizer() { + final String userDn = "uid=foo," + randomDn(); + final List groups = IntStream.range(0, randomIntBetween(50, 100)) + .mapToObj(i -> "gid=g" + i + "," + randomDn()) + .distinct() + .toList(); + final RealmConfig realmConfig = mock(RealmConfig.class); + when(realmConfig.name()).thenReturn(randomAlphaOfLengthBetween(3, 8)); + final UserRoleMapper.UserData userData = new UserRoleMapper.UserData( + randomAlphaOfLengthBetween(5, 8), + userDn, + groups, + Map.of(), + realmConfig + ); + UserRoleMapper.UserData spyUserdata = spy(userData); + final UserRoleMapper.DistinguishedNameNormalizer spyDnNormalizer = spy(userData.getDnNormalizer()); + when(spyUserdata.getDnNormalizer()).thenReturn(spyDnNormalizer); + + final ExpressionModel expressionModel = spyUserdata.asModel(); + + // All DNs to be tested should only be parsed once no matter how many groups the userData may have + Mockito.clearInvocations(spyDnNormalizer); + final List dnList = randomList(100, 200, DistinguishedNameNormalizerTests::randomDn).stream().distinct().toList(); + final List fieldValues = dnList.stream() + .map(dn -> randomBoolean() ? new FieldValue(dn) : new FieldValue("*," + dn)) + .toList(); + expressionModel.test("groups", fieldValues); + // Also does not matter how many times the model is tested + expressionModel.test("groups", randomNonEmptySubsetOf(fieldValues)); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(String.class); + verify(spyDnNormalizer, times(dnList.size())).doNormalize(argumentCaptor.capture()); + assertThat(argumentCaptor.getAllValues(), equalTo(dnList)); + } + + private void parseDnMultipleTimes(String dn) { + IntStream.range(0, randomIntBetween(3, 5)).forEach(i -> dnNormalizer.normalize(dn)); + } + + private void runPredicateMultipleTimes(Predicate predicate, Object value) { + IntStream.range(0, randomIntBetween(3, 5)).forEach(i -> predicate.test(new FieldValue(value))); + } + + private UserRoleMapper.DistinguishedNameNormalizer getDnNormalizer() { + return spy(new UserRoleMapper.DistinguishedNameNormalizer()); + } + + private static String randomDn() { + return "CN=" + + randomAlphaOfLengthBetween(3, 12) + + ",OU=" + + randomAlphaOfLength(4) + + ", O=" + + randomAlphaOfLengthBetween(2, 6) + + ",dc=" + + randomAlphaOfLength(3); + } + + private static String mutateDn(String dn) { + return switch (randomIntBetween(1, 4)) { + case 1 -> dn.toLowerCase(Locale.ENGLISH); + case 2 -> dn.toUpperCase(Locale.ENGLISH); + case 3 -> dn.replace(" ", ""); + default -> dn.replace(",", " ,"); + }; + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DistinguishedNamePredicateTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DistinguishedNamePredicateTests.java index 444cc850af02..9484615045ce 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DistinguishedNamePredicateTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DistinguishedNamePredicateTests.java @@ -34,7 +34,7 @@ public void testMatching() throws Exception { } else { inputDn = randomDn; } - final Predicate predicate = new UserRoleMapper.DistinguishedNamePredicate(inputDn); + final Predicate predicate = new UserRoleMapper.DistinguishedNamePredicate(inputDn, getDnNormalizer()); assertPredicate(predicate, randomDn, true); assertPredicate(predicate, randomDn.toLowerCase(Locale.ROOT), true); @@ -45,6 +45,9 @@ public void testMatching() throws Exception { assertPredicate(predicate, "*," + new DN(inputDn).getParent().getParent().toNormalizedString(), true); assertPredicate(predicate, randomDn.replaceFirst(".*,", "*,"), true); assertPredicate(predicate, randomDn.replaceFirst("[^,]*,", "*, "), true); + assertPredicate(predicate, "*,", true); + assertPredicate(predicate, "*, ", true); + assertPredicate(new UserRoleMapper.DistinguishedNamePredicate("", getDnNormalizer()), "*,", false); assertPredicate(predicate, randomDn + ",CN=AU", false); assertPredicate(predicate, "X" + randomDn, false); @@ -55,20 +58,20 @@ public void testMatching() throws Exception { } public void testParsingMalformedInput() { - Predicate predicate = new UserRoleMapper.DistinguishedNamePredicate(""); + Predicate predicate = new UserRoleMapper.DistinguishedNamePredicate("", getDnNormalizer()); assertPredicate(predicate, null, false); assertPredicate(predicate, "", true); assertPredicate(predicate, randomAlphaOfLengthBetween(1, 8), false); assertPredicate(predicate, randomAlphaOfLengthBetween(1, 8) + "*", false); - predicate = new UserRoleMapper.DistinguishedNamePredicate("foo="); + predicate = new UserRoleMapper.DistinguishedNamePredicate("foo=", getDnNormalizer()); assertPredicate(predicate, null, false); assertPredicate(predicate, "foo", false); assertPredicate(predicate, "foo=", true); assertPredicate(predicate, randomAlphaOfLengthBetween(5, 12), false); assertPredicate(predicate, randomAlphaOfLengthBetween(5, 12) + "*", false); - predicate = new UserRoleMapper.DistinguishedNamePredicate("=bar"); + predicate = new UserRoleMapper.DistinguishedNamePredicate("=bar", getDnNormalizer()); assertPredicate(predicate, null, false); assertPredicate(predicate, "bar", false); assertPredicate(predicate, "=bar", true); @@ -79,4 +82,8 @@ public void testParsingMalformedInput() { private void assertPredicate(Predicate predicate, Object value, boolean expected) { assertThat("Predicate [" + predicate + "] match [" + value + "]", predicate.test(new FieldValue(value)), equalTo(expected)); } + + private UserRoleMapper.DistinguishedNameNormalizer getDnNormalizer() { + return new UserRoleMapper.DistinguishedNameNormalizer(); + } } From 0b7d34e75c6673006fff78ba6a6ee053a4f6c577 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 6 Dec 2022 18:35:18 +1100 Subject: [PATCH 155/919] Improve robustness of JwkSet reloading (#92081) Currently the reload future object is reset after the listener gets invoked. Since the reset is done is a separate (http) thread from the waiting listener, it is possible that the reset can be delayed while the listener thread is ready to proceed. If the listener thread tries to reload the JWKs again, it will see the old future object and incorrectly skip the reload operation. This is mostly a test issue because the listener thread never tries to reload JWKs again in production. There is however another actual production bug in that the new JwkSet is *not* returned as part of the listener, only the `isUpdated` flag is returned. When the listener reads the JwkSet from the JwkSetLoader, it is possible that the JwkSet is changed again thus the `isUpdate` flag and actual `JwkSet` value can become inconsistent. This PR fixes both of the issues by: (1) resetting the future object before invoking the listener and (2) returning both `isUpdated` flag and the new JwkSet value to the listener. Resolves: #90467 Resolves: #89509 --- docs/changelog/92081.yaml | 5 +++ .../security/authc/jwt/JwkSetLoader.java | 31 ++++++++++--------- .../authc/jwt/JwtSignatureValidator.java | 19 +++++++----- 3 files changed, 33 insertions(+), 22 deletions(-) create mode 100644 docs/changelog/92081.yaml diff --git a/docs/changelog/92081.yaml b/docs/changelog/92081.yaml new file mode 100644 index 000000000000..c5e041920784 --- /dev/null +++ b/docs/changelog/92081.yaml @@ -0,0 +1,5 @@ +pr: 92081 +summary: Improve robustness of `JwkSet` reloading +area: Authentication +type: bug +issues: [] diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwkSetLoader.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwkSetLoader.java index a6f25ef69097..e8b3c5b95543 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwkSetLoader.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwkSetLoader.java @@ -19,6 +19,7 @@ import org.elasticsearch.common.util.concurrent.ListenableFuture; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.Releasable; +import org.elasticsearch.core.Tuple; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings; @@ -42,7 +43,7 @@ public class JwkSetLoader implements Releasable { private static final Logger logger = LogManager.getLogger(JwkSetLoader.class); - private final AtomicReference> reloadFutureRef = new AtomicReference<>(); + private final AtomicReference>> reloadFutureRef = new AtomicReference<>(); private final RealmConfig realmConfig; private final List allowedJwksAlgsPkc; private final String jwkSetPath; @@ -67,10 +68,10 @@ public JwkSetLoader(final RealmConfig realmConfig, List allowedJwksAlgsP // Any exception during loading requires closing JwkSetLoader's HTTP client to avoid a thread pool leak try { - final PlainActionFuture future = new PlainActionFuture<>(); + final PlainActionFuture> future = new PlainActionFuture<>(); reload(future); // ASSUME: Blocking read operations are OK during startup - final Boolean isUpdated = future.actionGet(); + final Boolean isUpdated = future.actionGet().v1(); assert isUpdated : "initial reload should have updated the JWK set"; } catch (Throwable t) { close(); @@ -83,8 +84,8 @@ public JwkSetLoader(final RealmConfig realmConfig, List allowedJwksAlgsP * they are different. The listener is called with false if the reloaded content is the same * as the existing one or true if they are different. */ - void reload(final ActionListener listener) { - final ListenableFuture future = this.getFuture(); + void reload(final ActionListener> listener) { + final ListenableFuture> future = this.getFuture(); future.addListener(listener); } @@ -92,17 +93,17 @@ ContentAndJwksAlgs getContentAndJwksAlgs() { return contentAndJwksAlgs; } - private ListenableFuture getFuture() { + private ListenableFuture> getFuture() { for (;;) { - final ListenableFuture existingFuture = this.reloadFutureRef.get(); + final ListenableFuture> existingFuture = this.reloadFutureRef.get(); if (existingFuture != null) { return existingFuture; } - final ListenableFuture newFuture = new ListenableFuture<>(); + final ListenableFuture> newFuture = new ListenableFuture<>(); if (this.reloadFutureRef.compareAndSet(null, newFuture)) { - loadInternal(ActionListener.runAfter(newFuture, () -> { - final ListenableFuture oldValue = this.reloadFutureRef.getAndSet(null); + loadInternal(ActionListener.runBefore(newFuture, () -> { + final ListenableFuture> oldValue = this.reloadFutureRef.getAndSet(null); assert oldValue == newFuture : "future reference changed unexpectedly"; })); return newFuture; @@ -111,7 +112,7 @@ private ListenableFuture getFuture() { } } - private void loadInternal(final ActionListener listener) { + private void loadInternal(final ActionListener> listener) { // PKC JWKSet get contents from local file or remote HTTPS URL if (httpClient == null) { logger.trace("Loading PKC JWKs from path [{}]", jwkSetPath); @@ -135,16 +136,18 @@ private void loadInternal(final ActionListener listener) { } } - private Boolean handleReloadedContentAndJwksAlgs(byte[] bytes) { + private Tuple handleReloadedContentAndJwksAlgs(byte[] bytes) { final ContentAndJwksAlgs newContentAndJwksAlgs = parseContent(bytes); + final boolean isUpdated; if (contentAndJwksAlgs != null && Arrays.equals(contentAndJwksAlgs.sha256, newContentAndJwksAlgs.sha256)) { logger.debug("No change in reloaded JWK set"); - return false; + isUpdated = false; } else { logger.debug("Reloaded JWK set is different from the existing set"); contentAndJwksAlgs = newContentAndJwksAlgs; - return true; + isUpdated = true; } + return new Tuple<>(isUpdated, contentAndJwksAlgs.jwksAlgs); } private ContentAndJwksAlgs parseContent(final byte[] jwkSetContentBytesPkc) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtSignatureValidator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtSignatureValidator.java index 93b87ea48aa9..685cea2c54ee 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtSignatureValidator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/jwt/JwtSignatureValidator.java @@ -254,23 +254,25 @@ class PkcJwtSignatureValidator implements JwtSignatureValidator { public void validate(String tokenPrincipal, SignedJWT signedJWT, ActionListener listener) { // TODO: assert algorithm? + final JwkSetLoader.ContentAndJwksAlgs contentAndJwksAlgs = jwkSetLoader.getContentAndJwksAlgs(); + final JwkSetLoader.JwksAlgs jwksAlgs = contentAndJwksAlgs.jwksAlgs(); try { - JwtValidateUtil.validateSignature(signedJWT, jwkSetLoader.getContentAndJwksAlgs().jwksAlgs().jwks()); + JwtValidateUtil.validateSignature(signedJWT, jwksAlgs.jwks()); listener.onResponse(null); } catch (Exception primaryException) { logger.debug( () -> org.elasticsearch.core.Strings.format( "Signature verification failed for JWT [%s] reloading JWKSet (was: #[%s] JWKs, #[%s] algs, sha256=[%s])", tokenPrincipal, - jwkSetLoader.getContentAndJwksAlgs().jwksAlgs().jwks().size(), - jwkSetLoader.getContentAndJwksAlgs().jwksAlgs().algs().size(), - MessageDigests.toHexString(jwkSetLoader.getContentAndJwksAlgs().sha256()) + jwksAlgs.jwks().size(), + jwksAlgs.algs().size(), + MessageDigests.toHexString(contentAndJwksAlgs.sha256()) ), primaryException ); - jwkSetLoader.reload(ActionListener.wrap(isUpdated -> { - if (false == isUpdated) { + jwkSetLoader.reload(ActionListener.wrap(reloadResult -> { + if (false == reloadResult.v1()) { // No change in JWKSet logger.debug("Reloaded same PKC JWKs, can't retry verify JWT token [{}]", tokenPrincipal); listener.onFailure(primaryException); @@ -281,13 +283,14 @@ public void validate(String tokenPrincipal, SignedJWT signedJWT, ActionListener< // Enhancement idea: When some JWKs are retained (ex: rotation), only invalidate for removed JWKs. reloadNotifier.reloaded(); - if (jwkSetLoader.getContentAndJwksAlgs().jwksAlgs().isEmpty()) { + final JwkSetLoader.JwksAlgs reloadedJwksAlgs = reloadResult.v2(); + if (reloadedJwksAlgs.isEmpty()) { logger.debug("Reloaded empty PKC JWKs, signature verification will fail for JWT [{}]", tokenPrincipal); // fall through and let try/catch below handle empty JWKs failure log and response } try { - JwtValidateUtil.validateSignature(signedJWT, jwkSetLoader.getContentAndJwksAlgs().jwksAlgs().jwks()); + JwtValidateUtil.validateSignature(signedJWT, reloadedJwksAlgs.jwks()); listener.onResponse(null); } catch (Exception secondaryException) { logger.debug( From 5e7c417b4afab186e888655f37c01ca11ba30266 Mon Sep 17 00:00:00 2001 From: weizijun Date: Tue, 6 Dec 2022 16:28:44 +0800 Subject: [PATCH 156/919] Add cancel support to downsampling operation (#88496) This change allows downsampling operation to be cancelled using the task cancel api. This change also adds downsample task stats that can be viewed via the get task api. Example usage: POST _tasks/oTUltX4IQMOUUVeiohTt8A:12345/_cancel POST _tasks/_cancel?actions=*rollup_indexer* POST _tasks/_cancel? parent_task_id=xxx --- .../xpack/core/XPackClientPlugin.java | 2 + .../core/downsample/RollupIndexerAction.java | 15 ++ .../core/rollup/action/RollupShardStatus.java | 141 ++++++++++++++++++ .../core/rollup/action/RollupShardTask.java | 90 +++++++++++ .../RollupShardStatusSerializingTests.java | 40 +++++ .../xpack/downsample/RollupShardIndexer.java | 73 ++++++--- .../TransportRollupIndexerAction.java | 2 + .../DownsampleActionSingleNodeTests.java | 80 ++++++++++ 8 files changed, 422 insertions(+), 21 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/RollupShardStatus.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/RollupShardTask.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/action/RollupShardStatusSerializingTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java index de2475994139..a793c2006551 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java @@ -151,6 +151,7 @@ import org.elasticsearch.xpack.core.rollup.action.GetRollupJobsAction; import org.elasticsearch.xpack.core.rollup.action.PutRollupJobAction; import org.elasticsearch.xpack.core.rollup.action.RollupSearchAction; +import org.elasticsearch.xpack.core.rollup.action.RollupShardStatus; import org.elasticsearch.xpack.core.rollup.action.StartRollupJobAction; import org.elasticsearch.xpack.core.rollup.action.StopRollupJobAction; import org.elasticsearch.xpack.core.rollup.job.RollupJob; @@ -460,6 +461,7 @@ public List getNamedWriteables() { new NamedWriteableRegistry.Entry(PersistentTaskParams.class, RollupJob.NAME, RollupJob::new), new NamedWriteableRegistry.Entry(Task.Status.class, RollupJobStatus.NAME, RollupJobStatus::new), new NamedWriteableRegistry.Entry(PersistentTaskState.class, RollupJobStatus.NAME, RollupJobStatus::new), + new NamedWriteableRegistry.Entry(Task.Status.class, RollupShardStatus.NAME, RollupShardStatus::new), // ccr new NamedWriteableRegistry.Entry(AutoFollowMetadata.class, AutoFollowMetadata.TYPE, AutoFollowMetadata::new), new NamedWriteableRegistry.Entry(Metadata.Custom.class, AutoFollowMetadata.TYPE, AutoFollowMetadata::new), diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/downsample/RollupIndexerAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/downsample/RollupIndexerAction.java index 135578c52f1d..166d681c5230 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/downsample/RollupIndexerAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/downsample/RollupIndexerAction.java @@ -24,6 +24,7 @@ import org.elasticsearch.tasks.TaskId; import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xpack.core.rollup.action.RollupShardTask; import java.io.IOException; import java.util.Arrays; @@ -258,6 +259,20 @@ public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); request.writeTo(out); } + + @Override + public Task createTask(long id, String type, String action, TaskId parentTaskId, Map headers) { + return new RollupShardTask( + id, + type, + action, + parentTaskId, + request.rollupRequest.getSourceIndex(), + request.rollupRequest.getDownsampleConfig(), + headers, + shardId() + ); + } } public static class ShardRollupResponse extends BroadcastShardResponse { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/RollupShardStatus.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/RollupShardStatus.java new file mode 100644 index 000000000000..2bbd2d96b9ce --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/RollupShardStatus.java @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.rollup.action; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.xcontent.ConstructingObjectParser; +import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; +import java.time.Instant; +import java.util.Objects; + +import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; + +public class RollupShardStatus implements Task.Status { + public static final String NAME = "rollup-index-shard"; + private static final ParseField SHARD_FIELD = new ParseField("shard"); + private static final ParseField START_TIME_FIELD = new ParseField("start_time"); + private static final ParseField IN_NUM_DOCS_RECEIVED_FIELD = new ParseField("in_num_docs_received"); + private static final ParseField OUT_NUM_DOCS_SENT_FIELD = new ParseField("out_num_docs_sent"); + private static final ParseField OUT_NUM_DOCS_INDEXED_FIELD = new ParseField("out_num_docs_indexed"); + private static final ParseField OUT_NUM_DOCS_FAILED_FIELD = new ParseField("out_num_docs_failed"); + + private final ShardId shardId; + private final long rollupStart; + private final long numReceived; + private final long numSent; + private final long numIndexed; + private final long numFailed; + + private static final ConstructingObjectParser PARSER; + static { + PARSER = new ConstructingObjectParser<>( + NAME, + args -> new RollupShardStatus( + ShardId.fromString((String) args[0]), + Instant.parse((String) args[1]).toEpochMilli(), + (Long) args[2], + (Long) args[3], + (Long) args[4], + (Long) args[5] + ) + ); + + PARSER.declareString(constructorArg(), SHARD_FIELD); + PARSER.declareString(constructorArg(), START_TIME_FIELD); + PARSER.declareLong(constructorArg(), IN_NUM_DOCS_RECEIVED_FIELD); + PARSER.declareLong(constructorArg(), OUT_NUM_DOCS_SENT_FIELD); + PARSER.declareLong(constructorArg(), OUT_NUM_DOCS_INDEXED_FIELD); + PARSER.declareLong(constructorArg(), OUT_NUM_DOCS_FAILED_FIELD); + } + + public RollupShardStatus(StreamInput in) throws IOException { + shardId = new ShardId(in); + rollupStart = in.readLong(); + numReceived = in.readLong(); + numSent = in.readLong(); + numIndexed = in.readLong(); + numFailed = in.readLong(); + } + + public RollupShardStatus(ShardId shardId, long rollupStart, long numReceived, long numSent, long numIndexed, long numFailed) { + this.shardId = shardId; + this.rollupStart = rollupStart; + this.numReceived = numReceived; + this.numSent = numSent; + this.numIndexed = numIndexed; + this.numFailed = numFailed; + } + + public static RollupShardStatus fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(SHARD_FIELD.getPreferredName(), shardId); + builder.field(START_TIME_FIELD.getPreferredName(), Instant.ofEpochMilli(rollupStart).toString()); + builder.field(IN_NUM_DOCS_RECEIVED_FIELD.getPreferredName(), numReceived); + builder.field(OUT_NUM_DOCS_SENT_FIELD.getPreferredName(), numSent); + builder.field(OUT_NUM_DOCS_INDEXED_FIELD.getPreferredName(), numIndexed); + builder.field(OUT_NUM_DOCS_FAILED_FIELD.getPreferredName(), numFailed); + builder.endObject(); + return builder; + } + + @Override + public String getWriteableName() { + return NAME; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + shardId.writeTo(out); + out.writeLong(rollupStart); + out.writeLong(numReceived); + out.writeLong(numSent); + out.writeLong(numIndexed); + out.writeLong(numFailed); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RollupShardStatus that = (RollupShardStatus) o; + return rollupStart == that.rollupStart + && Objects.equals(shardId.getIndexName(), that.shardId.getIndexName()) + && Objects.equals(shardId.id(), that.shardId.id()) + && Objects.equals(numReceived, that.numReceived) + && Objects.equals(numSent, that.numSent) + && Objects.equals(numIndexed, that.numIndexed) + && Objects.equals(numFailed, that.numFailed); + } + + @Override + public int hashCode() { + return Objects.hash(shardId.getIndexName(), shardId.id(), rollupStart, numReceived, numSent, numIndexed, numFailed); + } + + @Override + public String toString() { + return Strings.toString(this); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/RollupShardTask.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/RollupShardTask.java new file mode 100644 index 000000000000..ccf80afd0ae4 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/RollupShardTask.java @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.rollup.action; + +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.tasks.CancellableTask; +import org.elasticsearch.tasks.TaskId; +import org.elasticsearch.xpack.core.downsample.DownsampleConfig; +import org.elasticsearch.xpack.core.rollup.RollupField; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +public class RollupShardTask extends CancellableTask { + private final String rollupIndex; + private final DownsampleConfig config; + private final ShardId shardId; + private final long rollupStartTime; + private final AtomicLong numReceived = new AtomicLong(0); + private final AtomicLong numSent = new AtomicLong(0); + private final AtomicLong numIndexed = new AtomicLong(0); + private final AtomicLong numFailed = new AtomicLong(0); + + public RollupShardTask( + long id, + String type, + String action, + TaskId parentTask, + String rollupIndex, + DownsampleConfig config, + Map headers, + ShardId shardId + ) { + super(id, type, action, RollupField.NAME + "_" + rollupIndex + "[" + shardId.id() + "]", parentTask, headers); + this.rollupIndex = rollupIndex; + this.config = config; + this.shardId = shardId; + this.rollupStartTime = System.currentTimeMillis(); + } + + public String getRollupIndex() { + return rollupIndex; + } + + public DownsampleConfig config() { + return config; + } + + @Override + public Status getStatus() { + return new RollupShardStatus(shardId, rollupStartTime, numReceived.get(), numSent.get(), numIndexed.get(), numFailed.get()); + } + + public long getNumReceived() { + return numReceived.get(); + } + + public long getNumSent() { + return numSent.get(); + } + + public long getNumIndexed() { + return numIndexed.get(); + } + + public long getNumFailed() { + return numFailed.get(); + } + + public void addNumReceived(long count) { + numReceived.addAndGet(count); + } + + public void addNumSent(long count) { + numSent.addAndGet(count); + } + + public void addNumIndexed(long count) { + numIndexed.addAndGet(count); + } + + public void addNumFailed(long count) { + numFailed.addAndGet(count); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/action/RollupShardStatusSerializingTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/action/RollupShardStatusSerializingTests.java new file mode 100644 index 000000000000..b1a8357a57c5 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/action/RollupShardStatusSerializingTests.java @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.rollup.action; + +import org.elasticsearch.common.io.stream.Writeable.Reader; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.test.AbstractXContentSerializingTestCase; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; + +public class RollupShardStatusSerializingTests extends AbstractXContentSerializingTestCase { + @Override + protected RollupShardStatus doParseInstance(XContentParser parser) throws IOException { + return RollupShardStatus.fromXContent(parser); + } + + @Override + protected Reader instanceReader() { + return RollupShardStatus::new; + } + + @Override + protected RollupShardStatus createTestInstance() { + RollupShardStatus rollupShardStatus = new RollupShardStatus( + new ShardId(randomAlphaOfLength(5), randomAlphaOfLength(5), randomInt(5)), + randomMillisUpToYear9999(), + randomNonNegativeLong(), + randomNonNegativeLong(), + randomNonNegativeLong(), + randomNonNegativeLong() + ); + return rollupShardStatus; + } +} diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/RollupShardIndexer.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/RollupShardIndexer.java index 479e0e9cddc1..c42320132511 100644 --- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/RollupShardIndexer.java +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/RollupShardIndexer.java @@ -43,11 +43,13 @@ import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.bucket.DocCountProvider; import org.elasticsearch.search.aggregations.support.TimeSeriesIndexSearcher; +import org.elasticsearch.tasks.TaskCancelledException; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.core.downsample.DownsampleConfig; import org.elasticsearch.xpack.core.downsample.RollupIndexerAction; +import org.elasticsearch.xpack.core.rollup.action.RollupShardTask; import java.io.Closeable; import java.io.IOException; @@ -57,7 +59,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import java.util.stream.Collectors; @@ -90,11 +91,12 @@ class RollupShardIndexer { private final String[] metricFields; private final String[] labelFields; private final Map fieldValueFetchers; - private final AtomicLong numSent = new AtomicLong(); - private final AtomicLong numIndexed = new AtomicLong(); - private final AtomicLong numFailed = new AtomicLong(); + + private final RollupShardTask task; + private volatile boolean abort = false; RollupShardIndexer( + RollupShardTask task, Client client, IndexService indexService, ShardId shardId, @@ -104,6 +106,7 @@ class RollupShardIndexer { String[] metricFields, String[] labelFields ) { + this.task = task; this.client = client; this.indexShard = indexService.getShard(shardId.id()); this.config = config; @@ -111,7 +114,6 @@ class RollupShardIndexer { this.dimensionFields = dimensionFields; this.metricFields = metricFields; this.labelFields = labelFields; - this.searcher = indexShard.acquireSearcher("downsampling"); Closeable toClose = searcher; try { @@ -137,8 +139,7 @@ public RollupIndexerAction.ShardRollupResponse execute() throws IOException { long startTime = System.currentTimeMillis(); BulkProcessor bulkProcessor = createBulkProcessor(); try (searcher; bulkProcessor) { - // TODO: add cancellations - final TimeSeriesIndexSearcher timeSeriesSearcher = new TimeSeriesIndexSearcher(searcher, List.of()); + final TimeSeriesIndexSearcher timeSeriesSearcher = new TimeSeriesIndexSearcher(searcher, List.of(() -> { checkCancelled(); })); TimeSeriesBucketCollector bucketCollector = new TimeSeriesBucketCollector(bulkProcessor); bucketCollector.preCollection(); timeSeriesSearcher.search(new MatchAllDocsQuery(), bucketCollector); @@ -146,41 +147,60 @@ public RollupIndexerAction.ShardRollupResponse execute() throws IOException { } logger.info( - "Shard [{}] successfully sent [{}], indexed [{}], failed [{}], took [{}]", + "Shard [{}] successfully sent [{}], received source doc [{}], indexed rollup doc [{}], failed [{}], took [{}]", indexShard.shardId(), - numSent.get(), - numIndexed.get(), - numFailed.get(), + task.getNumReceived(), + task.getNumSent(), + task.getNumIndexed(), + task.getNumFailed(), TimeValue.timeValueMillis(System.currentTimeMillis() - startTime) ); - if (numIndexed.get() != numSent.get()) { + if (task.getNumIndexed() != task.getNumSent()) { throw new ElasticsearchException( "Shard [" + indexShard.shardId() + "] failed to index all rollup documents. Sent [" - + numSent.get() + + task.getNumSent() + "], indexed [" - + numIndexed.get() + + task.getNumIndexed() + "]." ); } - return new RollupIndexerAction.ShardRollupResponse(indexShard.shardId(), numIndexed.get()); + + return new RollupIndexerAction.ShardRollupResponse(indexShard.shardId(), task.getNumIndexed()); + } + + private void checkCancelled() { + if (task.isCancelled() || abort) { + logger.warn( + "Shard [{}] rollup abort, sent [{}], indexed [{}], failed[{}]", + indexShard.shardId(), + task.getNumSent(), + task.getNumIndexed(), + task.getNumFailed() + ); + throw new TaskCancelledException(format("Shard %s rollup cancelled", indexShard.shardId())); + } } private BulkProcessor createBulkProcessor() { final BulkProcessor.Listener listener = new BulkProcessor.Listener() { @Override public void beforeBulk(long executionId, BulkRequest request) { - numSent.addAndGet(request.numberOfActions()); + task.addNumSent(request.numberOfActions()); } @Override public void afterBulk(long executionId, BulkRequest request, BulkResponse response) { - numIndexed.addAndGet(request.numberOfActions()); + task.addNumIndexed(request.numberOfActions()); if (response.hasFailures()) { - Map failures = Arrays.stream(response.getItems()) + List failedItems = Arrays.stream(response.getItems()) .filter(BulkItemResponse::isFailed) + .collect(Collectors.toList()); + task.addNumFailed(failedItems.size()); + + Map failures = failedItems.stream() .collect( Collectors.toMap( BulkItemResponse::getId, @@ -188,8 +208,10 @@ public void afterBulk(long executionId, BulkRequest request, BulkResponse respon (msg1, msg2) -> Objects.equals(msg1, msg2) ? msg1 : msg1 + "," + msg2 ) ); - numFailed.addAndGet(failures.size()); logger.error("Shard [{}] failed to populate rollup index. Failures: [{}]", indexShard.shardId(), failures); + + // cancel rollup task + abort = true; } } @@ -197,8 +219,11 @@ public void afterBulk(long executionId, BulkRequest request, BulkResponse respon public void afterBulk(long executionId, BulkRequest request, Throwable failure) { if (failure != null) { long items = request.numberOfActions(); - numFailed.addAndGet(items); + task.addNumFailed(items); logger.error(() -> format("Shard [%s] failed to populate rollup index.", indexShard.shardId()), failure); + + // cancel rollup task + abort = true; } } }; @@ -234,6 +259,7 @@ public LeafBucketCollector getLeafCollector(final AggregationExecutionContext ag return new LeafBucketCollector() { @Override public void collect(int docId, long owningBucketOrd) throws IOException { + task.addNumReceived(1); final BytesRef tsid = aggCtx.getTsid(); assert tsid != null : "Document without [" + TimeSeriesIdFieldMapper.NAME + "] field was found."; final int tsidOrd = aggCtx.getTsidOrd(); @@ -330,7 +356,8 @@ private void indexBucket(XContentBuilder doc) { @Override public void preCollection() throws IOException { - // no-op + // check cancel when start running + checkCancelled(); } @Override @@ -341,6 +368,10 @@ public void postCollection() throws IOException { indexBucket(doc); } bulkProcessor.flush(); + + // check cancel after the flush all data + checkCancelled(); + logger.info("Shard {} processed [{}] docs, created [{}] rollup buckets", indexShard.shardId(), docsProcessed, bucketsCreated); } diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/TransportRollupIndexerAction.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/TransportRollupIndexerAction.java index e7ccf37edde2..be61f9991682 100644 --- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/TransportRollupIndexerAction.java +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/downsample/TransportRollupIndexerAction.java @@ -29,6 +29,7 @@ import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.ClientHelper; import org.elasticsearch.xpack.core.downsample.RollupIndexerAction; +import org.elasticsearch.xpack.core.rollup.action.RollupShardTask; import java.io.IOException; import java.util.Arrays; @@ -125,6 +126,7 @@ protected RollupIndexerAction.ShardRollupResponse shardOperation(RollupIndexerAc throws IOException { IndexService indexService = indicesService.indexService(request.shardId().getIndex()); RollupShardIndexer indexer = new RollupShardIndexer( + (RollupShardTask) task, client, indexService, request.shardId(), diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/downsample/DownsampleActionSingleNodeTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/downsample/DownsampleActionSingleNodeTests.java index f10679117a2e..f4d06397da0f 100644 --- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/downsample/DownsampleActionSingleNodeTests.java +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/downsample/DownsampleActionSingleNodeTests.java @@ -39,12 +39,14 @@ import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexMode; import org.elasticsearch.index.IndexNotFoundException; +import org.elasticsearch.index.IndexService; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.engine.VersionConflictEngineException; import org.elasticsearch.index.mapper.DateFieldMapper; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.TimeSeriesIdFieldMapper; import org.elasticsearch.index.mapper.TimeSeriesParams; +import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.indices.IndicesService; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.search.SearchHit; @@ -65,6 +67,9 @@ import org.elasticsearch.search.aggregations.metrics.ValueCountAggregationBuilder; import org.elasticsearch.search.sort.SortBuilders; import org.elasticsearch.search.sort.SortOrder; +import org.elasticsearch.tasks.TaskCancelHelper; +import org.elasticsearch.tasks.TaskCancelledException; +import org.elasticsearch.tasks.TaskId; import org.elasticsearch.test.ESSingleNodeTestCase; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; @@ -75,6 +80,7 @@ import org.elasticsearch.xpack.core.ilm.LifecycleSettings; import org.elasticsearch.xpack.core.ilm.RolloverAction; import org.elasticsearch.xpack.core.rollup.ConfigTestHelpers; +import org.elasticsearch.xpack.core.rollup.action.RollupShardTask; import org.elasticsearch.xpack.ilm.IndexLifecycle; import org.elasticsearch.xpack.rollup.Rollup; import org.junit.Before; @@ -96,11 +102,13 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static java.util.Collections.emptyMap; import static org.elasticsearch.index.mapper.TimeSeriesParams.TIME_SERIES_METRIC_PARAM; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; public class DownsampleActionSingleNodeTests extends ESSingleNodeTestCase { @@ -571,6 +579,78 @@ public void testRollupDatastream() throws Exception { assertFalse(indices.stream().filter(i -> i.getName().equals(sourceIndex)).toList().isEmpty()); } + public void testCancelRollupIndexer() throws IOException { + // create rollup config and index documents into source index + DownsampleConfig config = new DownsampleConfig(randomInterval()); + SourceSupplier sourceSupplier = () -> XContentFactory.jsonBuilder() + .startObject() + .field(FIELD_TIMESTAMP, randomDateForInterval(config.getInterval())) + .field(FIELD_DIMENSION_1, randomAlphaOfLength(1)) + .field(FIELD_NUMERIC_1, randomDouble()) + .endObject(); + bulkIndex(sourceSupplier); + prepareSourceIndex(sourceIndex); + + IndicesService indexServices = getInstanceFromNode(IndicesService.class); + Index srcIndex = resolveIndex(sourceIndex); + IndexService indexService = indexServices.indexServiceSafe(srcIndex); + int shardNum = randomIntBetween(0, numOfShards - 1); + IndexShard shard = indexService.getShard(shardNum); + RollupShardTask task = new RollupShardTask( + randomLong(), + "rollup", + "action", + TaskId.EMPTY_TASK_ID, + rollupIndex, + config, + emptyMap(), + shard.shardId() + ); + TaskCancelHelper.cancel(task, "test cancel"); + + // re-use source index as temp index for test + RollupShardIndexer indexer = new RollupShardIndexer( + task, + client(), + indexService, + shard.shardId(), + rollupIndex, + config, + new String[] { FIELD_DIMENSION_1, FIELD_DIMENSION_2 }, + new String[] { FIELD_NUMERIC_1, FIELD_NUMERIC_2 }, + new String[] {} + ); + + TaskCancelledException exception = expectThrows(TaskCancelledException.class, () -> indexer.execute()); + assertThat(exception.getMessage(), equalTo("Shard [" + sourceIndex + "][" + shardNum + "] rollup cancelled")); + } + + public void testRollupBulkFailed() throws IOException { + // create rollup config and index documents into source index + DownsampleConfig config = new DownsampleConfig(randomInterval()); + SourceSupplier sourceSupplier = () -> XContentFactory.jsonBuilder() + .startObject() + .field(FIELD_TIMESTAMP, randomDateForInterval(config.getInterval())) + .field(FIELD_DIMENSION_1, randomAlphaOfLength(1)) + .field(FIELD_NUMERIC_1, randomDouble()) + .endObject(); + bulkIndex(sourceSupplier); + prepareSourceIndex(sourceIndex); + + // block rollup index + assertAcked( + client().admin() + .indices() + .preparePutTemplate(rollupIndex) + .setPatterns(List.of(rollupIndex)) + .setSettings(Settings.builder().put("index.blocks.write", "true").build()) + .get() + ); + + ElasticsearchException exception = expectThrows(ElasticsearchException.class, () -> rollup(sourceIndex, rollupIndex, config)); + assertThat(exception.getMessage(), equalTo("Unable to rollup index [" + sourceIndex + "]")); + } + private DateHistogramInterval randomInterval() { return ConfigTestHelpers.randomInterval(); } From d94e6959cb6306603fd92f48d447528996c07583 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Tue, 6 Dec 2022 10:14:16 +0100 Subject: [PATCH 157/919] Remote access authentication header serialization (#91816) This PR implements a class (and associated serialization utility methods) representing the new `_remote_access_authentication` transport header we will send as part of RCS 2.0. The header will be included with cross cluster requests made by querying clusters and contains authentication information, and privileges of the user initiating a request. The privileges are an optimized representation of a `RoleDescriptorsIntersection` and will be used by the fulfilling cluster to authenticate and authorize a request from a given querying cluster. This functionality is not hooked up to any active flows yet. It will be used in the security transport interceptor to represent the remote access header to be sent from the querying cluster to the fulfilling cluster for RCS 2.0, and by the fulfilling cluster for authentication and authorization. The [POC PR highlights](https://github.com/elastic/elasticsearch/pull/91251) the tentative usage of functionality added in this PR. --- .../authc/RemoteAccessAuthentication.java | 160 ++++++++++++++++++ .../RemoteAccessAuthenticationTests.java | 64 +++++++ 2 files changed, 224 insertions(+) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RemoteAccessAuthentication.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/RemoteAccessAuthenticationTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RemoteAccessAuthentication.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RemoteAccessAuthentication.java new file mode 100644 index 000000000000..4d65eec4ab4e --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/RemoteAccessAuthentication.java @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.security.authc; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.Version; +import org.elasticsearch.common.bytes.AbstractBytesReference; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentFactory; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentParserConfiguration; +import org.elasticsearch.xcontent.XContentType; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +public final class RemoteAccessAuthentication { + public static final String REMOTE_ACCESS_AUTHENTICATION_HEADER_KEY = "_remote_access_authentication"; + private final Authentication authentication; + private final List roleDescriptorsBytesList; + + public RemoteAccessAuthentication(Authentication authentication, RoleDescriptorsIntersection roleDescriptorsIntersection) + throws IOException { + this(authentication, toRoleDescriptorsBytesList(roleDescriptorsIntersection)); + } + + private RemoteAccessAuthentication(Authentication authentication, List roleDescriptorsBytesList) { + this.authentication = authentication; + this.roleDescriptorsBytesList = roleDescriptorsBytesList; + } + + public void writeToContext(final ThreadContext ctx) throws IOException { + ctx.putHeader(REMOTE_ACCESS_AUTHENTICATION_HEADER_KEY, encode()); + } + + public static RemoteAccessAuthentication readFromContext(final ThreadContext ctx) throws IOException { + return decode(ctx.getHeader(REMOTE_ACCESS_AUTHENTICATION_HEADER_KEY)); + } + + public Authentication getAuthentication() { + return authentication; + } + + public List getRoleDescriptorsBytesList() { + return roleDescriptorsBytesList; + } + + private static List toRoleDescriptorsBytesList(final RoleDescriptorsIntersection roleDescriptorsIntersection) + throws IOException { + // If we ever lift this restriction, we need to ensure that the serialization of each set of role descriptors to raw bytes is + // deterministic. We can do so by sorting the role descriptors before serializing. + assert roleDescriptorsIntersection.roleDescriptorsList().stream().noneMatch(rds -> rds.size() > 1) + : "sets with more than one role descriptor are not supported for remote access authentication"; + final List roleDescriptorsBytesList = new ArrayList<>(); + for (Set roleDescriptors : roleDescriptorsIntersection.roleDescriptorsList()) { + roleDescriptorsBytesList.add(RoleDescriptorsBytes.fromRoleDescriptors(roleDescriptors)); + } + return roleDescriptorsBytesList; + } + + private String encode() throws IOException { + final BytesStreamOutput out = new BytesStreamOutput(); + out.setVersion(authentication.getEffectiveSubject().getVersion()); + Version.writeVersion(authentication.getEffectiveSubject().getVersion(), out); + authentication.writeTo(out); + out.writeCollection(roleDescriptorsBytesList, StreamOutput::writeBytesReference); + return Base64.getEncoder().encodeToString(BytesReference.toBytes(out.bytes())); + } + + private static RemoteAccessAuthentication decode(final String header) throws IOException { + Objects.requireNonNull(header); + final byte[] bytes = Base64.getDecoder().decode(header); + final StreamInput in = StreamInput.wrap(bytes); + final Version version = Version.readVersion(in); + in.setVersion(version); + final Authentication authentication = new Authentication(in); + final List roleDescriptorsBytesList = in.readImmutableList(RoleDescriptorsBytes::new); + return new RemoteAccessAuthentication(authentication, roleDescriptorsBytesList); + } + + public static final class RoleDescriptorsBytes extends AbstractBytesReference { + private final BytesReference rawBytes; + + public RoleDescriptorsBytes(BytesReference rawBytes) { + this.rawBytes = rawBytes; + } + + public RoleDescriptorsBytes(StreamInput streamInput) throws IOException { + this(streamInput.readBytesReference()); + } + + public static RoleDescriptorsBytes fromRoleDescriptors(final Set roleDescriptors) throws IOException { + final XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.startObject(); + for (RoleDescriptor roleDescriptor : roleDescriptors) { + builder.field(roleDescriptor.getName(), roleDescriptor); + } + builder.endObject(); + return new RoleDescriptorsBytes(BytesReference.bytes(builder)); + } + + public Set toRoleDescriptors() { + try (XContentParser parser = XContentHelper.createParser(XContentParserConfiguration.EMPTY, rawBytes, XContentType.JSON)) { + final List roleDescriptors = new ArrayList<>(); + parser.nextToken(); + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + parser.nextToken(); + final String roleName = parser.currentName(); + roleDescriptors.add(RoleDescriptor.parse(roleName, parser, false)); + } + return Set.copyOf(roleDescriptors); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public byte get(int index) { + return rawBytes.get(index); + } + + @Override + public int length() { + return rawBytes.length(); + } + + @Override + public BytesReference slice(int from, int length) { + return rawBytes.slice(from, length); + } + + @Override + public long ramBytesUsed() { + return rawBytes.ramBytesUsed(); + } + + @Override + public BytesRef toBytesRef() { + return rawBytes.toBytesRef(); + } + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/RemoteAccessAuthenticationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/RemoteAccessAuthenticationTests.java new file mode 100644 index 000000000000..90615a9109ff --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/RemoteAccessAuthenticationTests.java @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.security.authc; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentFactory; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.elasticsearch.xpack.core.security.authz.RoleDescriptorTests.randomUniquelyNamedRoleDescriptors; +import static org.hamcrest.Matchers.equalTo; + +public class RemoteAccessAuthenticationTests extends ESTestCase { + + public void testWriteReadContextRoundtrip() throws IOException { + final ThreadContext ctx = new ThreadContext(Settings.EMPTY); + final RoleDescriptorsIntersection expectedRoleDescriptorsIntersection = randomRoleDescriptorsIntersection(); + final var expectedRemoteAccessAuthentication = new RemoteAccessAuthentication( + AuthenticationTestHelper.builder().build(), + expectedRoleDescriptorsIntersection + ); + + expectedRemoteAccessAuthentication.writeToContext(ctx); + final RemoteAccessAuthentication actual = RemoteAccessAuthentication.readFromContext(ctx); + + assertThat(actual.getAuthentication(), equalTo(expectedRemoteAccessAuthentication.getAuthentication())); + final List> roleDescriptorsList = new ArrayList<>(); + for (BytesReference bytesReference : actual.getRoleDescriptorsBytesList()) { + Set roleDescriptors = new RemoteAccessAuthentication.RoleDescriptorsBytes(bytesReference).toRoleDescriptors(); + roleDescriptorsList.add(roleDescriptors); + } + final var actualRoleDescriptorsIntersection = new RoleDescriptorsIntersection(roleDescriptorsList); + assertThat(actualRoleDescriptorsIntersection, equalTo(expectedRoleDescriptorsIntersection)); + } + + public void testRoleDescriptorsBytesToRoleDescriptors() throws IOException { + final Set expectedRoleDescriptors = Set.copyOf(randomUniquelyNamedRoleDescriptors(0, 3)); + final XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.map(expectedRoleDescriptors.stream().collect(Collectors.toMap(RoleDescriptor::getName, Function.identity()))); + final Set actualRoleDescriptors = new RemoteAccessAuthentication.RoleDescriptorsBytes(BytesReference.bytes(builder)) + .toRoleDescriptors(); + assertThat(actualRoleDescriptors, equalTo(expectedRoleDescriptors)); + } + + private RoleDescriptorsIntersection randomRoleDescriptorsIntersection() { + return new RoleDescriptorsIntersection(randomList(0, 3, () -> Set.copyOf(randomUniquelyNamedRoleDescriptors(0, 1)))); + } +} From b3a272e94a8d543bf6ecac04d5a4a4cb1c6f7e9c Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 6 Dec 2022 10:32:06 +0000 Subject: [PATCH 158/919] Chunked encoding for _cat APIs (#92022) Some of these APIs scale as O(#indices) or O(#shards) so it seems worthwhile to use chunked encoding throughout. Relates #89838 --- .../common/io/UTF8StreamWriter.java | 292 ------------------ .../rest/action/cat/AbstractCatAction.java | 28 +- .../rest/action/cat/RestTable.java | 188 ++++++++--- .../RestCatComponentTemplateActionTests.java | 3 +- .../rest/action/cat/RestTableTests.java | 137 +++++++- .../elasticsearch/rest/RestResponseUtils.java | 45 +++ 6 files changed, 342 insertions(+), 351 deletions(-) delete mode 100644 server/src/main/java/org/elasticsearch/common/io/UTF8StreamWriter.java create mode 100644 test/framework/src/main/java/org/elasticsearch/rest/RestResponseUtils.java diff --git a/server/src/main/java/org/elasticsearch/common/io/UTF8StreamWriter.java b/server/src/main/java/org/elasticsearch/common/io/UTF8StreamWriter.java deleted file mode 100644 index d912db541f05..000000000000 --- a/server/src/main/java/org/elasticsearch/common/io/UTF8StreamWriter.java +++ /dev/null @@ -1,292 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.common.io; - -import java.io.CharConversionException; -import java.io.IOException; -import java.io.OutputStream; -import java.io.Writer; - -public final class UTF8StreamWriter extends Writer { - - /** - * Holds the current output stream or null if closed. - */ - private OutputStream _outputStream; - - /** - * Holds the bytes' buffer. - */ - private final byte[] _bytes; - - /** - * Holds the bytes buffer index. - */ - private int _index; - - /** - * Creates a UTF-8 writer having a byte buffer of moderate capacity (2048). - */ - public UTF8StreamWriter() { - _bytes = new byte[2048]; - } - - /** - * Creates a UTF-8 writer having a byte buffer of specified capacity. - * - * @param capacity the capacity of the byte buffer. - */ - public UTF8StreamWriter(int capacity) { - _bytes = new byte[capacity]; - } - - /** - * Sets the output stream to use for writing until this writer is closed. - * For example:[code] - * Writer writer = new UTF8StreamWriter().setOutputStream(out); - * [/code] is equivalent but writes faster than [code] - * Writer writer = new java.io.OutputStreamWriter(out, "UTF-8"); - * [/code] - * - * @param out the output stream. - * @return this UTF-8 writer. - * @throws IllegalStateException if this writer is being reused and - * it has not been {@link #close closed} or {@link #reset reset}. - */ - public UTF8StreamWriter setOutput(OutputStream out) { - if (_outputStream != null) throw new IllegalStateException("Writer not closed or reset"); - _outputStream = out; - return this; - } - - /** - * Writes a single character. This method supports 16-bits - * character surrogates. - * - * @param c char the character to be written (possibly - * a surrogate). - * @throws IOException if an I/O error occurs. - */ - public void write(char c) throws IOException { - if ((c < 0xd800) || (c > 0xdfff)) { - write((int) c); - } else if (c < 0xdc00) { // High surrogate. - _highSurrogate = c; - } else { // Low surrogate. - int code = ((_highSurrogate - 0xd800) << 10) + (c - 0xdc00) + 0x10000; - write(code); - } - } - - private char _highSurrogate; - - /** - * Writes a character given its 31-bits Unicode. - * - * @param code the 31 bits Unicode of the character to be written. - * @throws IOException if an I/O error occurs. - */ - @Override - public void write(int code) throws IOException { - if ((code & 0xffffff80) == 0) { - _bytes[_index] = (byte) code; - if (++_index >= _bytes.length) { - flushBuffer(); - } - } else { // Writes more than one byte. - write2(code); - } - } - - private void write2(int c) throws IOException { - if ((c & 0xfffff800) == 0) { // 2 bytes. - _bytes[_index] = (byte) (0xc0 | (c >> 6)); - if (++_index >= _bytes.length) { - flushBuffer(); - } - _bytes[_index] = (byte) (0x80 | (c & 0x3f)); - if (++_index >= _bytes.length) { - flushBuffer(); - } - } else if ((c & 0xffff0000) == 0) { // 3 bytes. - _bytes[_index] = (byte) (0xe0 | (c >> 12)); - if (++_index >= _bytes.length) { - flushBuffer(); - } - _bytes[_index] = (byte) (0x80 | ((c >> 6) & 0x3f)); - if (++_index >= _bytes.length) { - flushBuffer(); - } - _bytes[_index] = (byte) (0x80 | (c & 0x3f)); - if (++_index >= _bytes.length) { - flushBuffer(); - } - } else if ((c & 0xff200000) == 0) { // 4 bytes. - _bytes[_index] = (byte) (0xf0 | (c >> 18)); - if (++_index >= _bytes.length) { - flushBuffer(); - } - _bytes[_index] = (byte) (0x80 | ((c >> 12) & 0x3f)); - if (++_index >= _bytes.length) { - flushBuffer(); - } - _bytes[_index] = (byte) (0x80 | ((c >> 6) & 0x3f)); - if (++_index >= _bytes.length) { - flushBuffer(); - } - _bytes[_index] = (byte) (0x80 | (c & 0x3f)); - if (++_index >= _bytes.length) { - flushBuffer(); - } - } else if ((c & 0xf4000000) == 0) { // 5 bytes. - _bytes[_index] = (byte) (0xf8 | (c >> 24)); - if (++_index >= _bytes.length) { - flushBuffer(); - } - _bytes[_index] = (byte) (0x80 | ((c >> 18) & 0x3f)); - if (++_index >= _bytes.length) { - flushBuffer(); - } - _bytes[_index] = (byte) (0x80 | ((c >> 12) & 0x3f)); - if (++_index >= _bytes.length) { - flushBuffer(); - } - _bytes[_index] = (byte) (0x80 | ((c >> 6) & 0x3f)); - if (++_index >= _bytes.length) { - flushBuffer(); - } - _bytes[_index] = (byte) (0x80 | (c & 0x3f)); - if (++_index >= _bytes.length) { - flushBuffer(); - } - } else if ((c & 0x80000000) == 0) { // 6 bytes. - _bytes[_index] = (byte) (0xfc | (c >> 30)); - if (++_index >= _bytes.length) { - flushBuffer(); - } - _bytes[_index] = (byte) (0x80 | ((c >> 24) & 0x3f)); - if (++_index >= _bytes.length) { - flushBuffer(); - } - _bytes[_index] = (byte) (0x80 | ((c >> 18) & 0x3f)); - if (++_index >= _bytes.length) { - flushBuffer(); - } - _bytes[_index] = (byte) (0x80 | ((c >> 12) & 0x3F)); - if (++_index >= _bytes.length) { - flushBuffer(); - } - _bytes[_index] = (byte) (0x80 | ((c >> 6) & 0x3F)); - if (++_index >= _bytes.length) { - flushBuffer(); - } - _bytes[_index] = (byte) (0x80 | (c & 0x3F)); - if (++_index >= _bytes.length) { - flushBuffer(); - } - } else { - throw new CharConversionException("Illegal character U+" + Integer.toHexString(c)); - } - } - - /** - * Writes a portion of an array of characters. - * - * @param cbuf the array of characters. - * @param off the offset from which to start writing characters. - * @param len the number of characters to write. - * @throws IOException if an I/O error occurs. - */ - @Override - public void write(char cbuf[], int off, int len) throws IOException { - final int off_plus_len = off + len; - for (int i = off; i < off_plus_len;) { - char c = cbuf[i++]; - if (c < 0x80) { - _bytes[_index] = (byte) c; - if (++_index >= _bytes.length) { - flushBuffer(); - } - } else { - write(c); - } - } - } - - /** - * Writes a portion of a string. - * - * @param str a String. - * @param off the offset from which to start writing characters. - * @param len the number of characters to write. - * @throws IOException if an I/O error occurs - */ - @Override - public void write(String str, int off, int len) throws IOException { - final int off_plus_len = off + len; - for (int i = off; i < off_plus_len;) { - char c = str.charAt(i++); - if (c < 0x80) { - _bytes[_index] = (byte) c; - if (++_index >= _bytes.length) { - flushBuffer(); - } - } else { - write(c); - } - } - } - - /** - * Flushes the stream. If the stream has saved any characters from the - * various write() methods in a buffer, write them immediately to their - * intended destination. Then, if that destination is another character or - * byte stream, flush it. Thus one flush() invocation will flush all the - * buffers in a chain of Writers and OutputStreams. - * - * @throws IOException if an I/O error occurs. - */ - @Override - public void flush() throws IOException { - flushBuffer(); - _outputStream.flush(); - } - - /** - * Closes and {@link #reset resets} this writer for reuse. - * - * @throws IOException if an I/O error occurs - */ - @Override - public void close() throws IOException { - if (_outputStream != null) { - flushBuffer(); - _outputStream.close(); - reset(); - } - } - - /** - * Flushes the internal bytes buffer. - * - * @throws IOException if an I/O error occurs - */ - private void flushBuffer() throws IOException { - if (_outputStream == null) throw new IOException("Stream closed"); - _outputStream.write(_bytes, 0, _index); - _index = 0; - } - - // Implements Reusable. - public void reset() { - _highSurrogate = 0; - _index = 0; - _outputStream = null; - } -} diff --git a/server/src/main/java/org/elasticsearch/rest/action/cat/AbstractCatAction.java b/server/src/main/java/org/elasticsearch/rest/action/cat/AbstractCatAction.java index 535753828712..ade43e624509 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/cat/AbstractCatAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/cat/AbstractCatAction.java @@ -10,7 +10,6 @@ import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.common.Table; import org.elasticsearch.common.io.Streams; -import org.elasticsearch.common.io.UTF8StreamWriter; import org.elasticsearch.common.io.stream.BytesStream; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; @@ -18,6 +17,8 @@ import org.elasticsearch.rest.RestStatus; import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; import java.util.Set; import static org.elasticsearch.rest.action.cat.RestTable.buildHelpWidths; @@ -39,17 +40,22 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC Table table = getTableWithHeader(request); int[] width = buildHelpWidths(table, request); BytesStream bytesOutput = Streams.flushOnCloseStream(channel.bytesOutput()); - UTF8StreamWriter out = new UTF8StreamWriter().setOutput(bytesOutput); - for (Table.Cell cell : table.getHeaders()) { - // need to do left-align always, so create new cells - pad(new Table.Cell(cell.value), width[0], request, out); - out.append(" | "); - pad(new Table.Cell(cell.attr.containsKey("alias") ? cell.attr.get("alias") : ""), width[1], request, out); - out.append(" | "); - pad(new Table.Cell(cell.attr.containsKey("desc") ? cell.attr.get("desc") : "not available"), width[2], request, out); - out.append("\n"); + try (var out = new OutputStreamWriter(bytesOutput, StandardCharsets.UTF_8)) { + for (Table.Cell cell : table.getHeaders()) { + // need to do left-align always, so create new cells + pad(new Table.Cell(cell.value), width[0], request, out); + out.append(" | "); + pad(new Table.Cell(cell.attr.containsKey("alias") ? cell.attr.get("alias") : ""), width[1], request, out); + out.append(" | "); + pad( + new Table.Cell(cell.attr.containsKey("desc") ? cell.attr.get("desc") : "not available"), + width[2], + request, + out + ); + out.append("\n"); + } } - out.close(); channel.sendResponse(new RestResponse(RestStatus.OK, RestResponse.TEXT_CONTENT_TYPE, bytesOutput.bytes())); }; } else { diff --git a/server/src/main/java/org/elasticsearch/rest/action/cat/RestTable.java b/server/src/main/java/org/elasticsearch/rest/action/cat/RestTable.java index 8b42f915e19f..015b1422d021 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/cat/RestTable.java +++ b/server/src/main/java/org/elasticsearch/rest/action/cat/RestTable.java @@ -8,28 +8,38 @@ package org.elasticsearch.rest.action.cat; +import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.Strings; import org.elasticsearch.common.Table; -import org.elasticsearch.common.io.Streams; -import org.elasticsearch.common.io.UTF8StreamWriter; -import org.elasticsearch.common.io.stream.BytesStream; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.ReleasableBytesReference; +import org.elasticsearch.common.collect.Iterators; +import org.elasticsearch.common.io.stream.RecyclerBytesStreamOutput; +import org.elasticsearch.common.recycler.Recycler; import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.SizeValue; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.core.Booleans; +import org.elasticsearch.core.Releasables; import org.elasticsearch.core.TimeValue; +import org.elasticsearch.rest.ChunkedRestResponseBody; import org.elasticsearch.rest.RestChannel; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestResponse; import org.elasticsearch.rest.RestStatus; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentType; import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -57,60 +67,146 @@ private static XContentType getResponseContentType(RestRequest request) { } public static RestResponse buildXContentBuilder(Table table, RestChannel channel) throws Exception { - RestRequest request = channel.request(); - XContentBuilder builder = channel.newBuilder(); - List displayHeaders = buildDisplayHeaders(table, request); - - builder.startArray(); - List rowOrder = getRowOrder(table, request); - for (Integer row : rowOrder) { - builder.startObject(); - for (DisplayHeader header : displayHeaders) { - builder.field(header.display, renderValue(request, table.getAsMap().get(header.name).get(row).value)); - } - builder.endObject(); - } - builder.endArray(); - return new RestResponse(RestStatus.OK, builder); + final RestRequest request = channel.request(); + final List rowOrder = getRowOrder(table, channel.request()); + final List displayHeaders = buildDisplayHeaders(table, request); + + return new RestResponse( + RestStatus.OK, + ChunkedRestResponseBody.fromXContent( + ignored -> Iterators.concat( + Iterators.single((builder, params) -> builder.startArray()), + rowOrder.stream().map(row -> (builder, params) -> { + builder.startObject(); + for (DisplayHeader header : displayHeaders) { + builder.field(header.display, renderValue(request, table.getAsMap().get(header.name).get(row).value)); + } + builder.endObject(); + return builder; + }).iterator(), + Iterators.single((builder, params) -> builder.endArray()) + ), + ToXContent.EMPTY_PARAMS, + channel + ) + ); } - public static RestResponse buildTextPlainResponse(Table table, RestChannel channel) throws IOException { + public static RestResponse buildTextPlainResponse(Table table, RestChannel channel) { RestRequest request = channel.request(); boolean verbose = request.paramAsBoolean("v", false); List headers = buildDisplayHeaders(table, request); int[] width = buildWidths(table, request, verbose, headers); - - BytesStream bytesOut = Streams.flushOnCloseStream(channel.bytesOutput()); - UTF8StreamWriter out = new UTF8StreamWriter().setOutput(bytesOut); int lastHeader = headers.size() - 1; - if (verbose) { - for (int col = 0; col < headers.size(); col++) { - DisplayHeader header = headers.get(col); - boolean isLastColumn = col == lastHeader; - pad(new Table.Cell(header.display, table.findHeaderByName(header.name)), width[col], request, out, isLastColumn); - if (isLastColumn == false) { - out.append(" "); + List rowOrder = getRowOrder(table, request); + + if (verbose == false && rowOrder.isEmpty()) { + return new RestResponse(RestStatus.OK, RestResponse.TEXT_CONTENT_TYPE, BytesArray.EMPTY); + } + + return new RestResponse(RestStatus.OK, new ChunkedRestResponseBody() { + + private boolean needsHeader = verbose; + private final Iterator rowIterator = rowOrder.iterator(); + + private RecyclerBytesStreamOutput currentOutput; + private final Writer writer = new OutputStreamWriter(new OutputStream() { + @Override + public void write(int b) throws IOException { + assert currentOutput != null; + currentOutput.write(b); } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + assert currentOutput != null; + currentOutput.write(b, off, len); + } + + @Override + public void flush() { + assert currentOutput != null; + currentOutput.flush(); + } + + @Override + public void close() { + assert currentOutput != null; + currentOutput.flush(); + } + }, StandardCharsets.UTF_8); + + @Override + public boolean isDone() { + return needsHeader == false && rowIterator.hasNext() == false; } - out.append("\n"); - } - List rowOrder = getRowOrder(table, request); + @Override + public ReleasableBytesReference encodeChunk(int sizeHint, Recycler recycler) throws IOException { + try { + assert currentOutput == null; + currentOutput = new RecyclerBytesStreamOutput(recycler); + + if (needsHeader) { + needsHeader = false; + for (int col = 0; col < headers.size(); col++) { + DisplayHeader header = headers.get(col); + boolean isLastColumn = col == lastHeader; + pad( + new Table.Cell(header.display, table.findHeaderByName(header.name)), + width[col], + request, + writer, + isLastColumn + ); + if (isLastColumn == false) { + writer.append(" "); + } + } + writer.append("\n"); + } - for (Integer row : rowOrder) { - for (int col = 0; col < headers.size(); col++) { - DisplayHeader header = headers.get(col); - boolean isLastColumn = col == lastHeader; - pad(table.getAsMap().get(header.name).get(row), width[col], request, out, isLastColumn); - if (isLastColumn == false) { - out.append(" "); + while (rowIterator.hasNext() && currentOutput.size() < sizeHint) { + final var row = rowIterator.next(); + for (int col = 0; col < headers.size(); col++) { + DisplayHeader header = headers.get(col); + boolean isLastColumn = col == lastHeader; + pad(table.getAsMap().get(header.name).get(row), width[col], request, writer, isLastColumn); + if (isLastColumn == false) { + writer.append(" "); + } + } + writer.append("\n"); + } + + if (rowIterator.hasNext()) { + writer.flush(); + } else { + writer.close(); + } + + final var chunkOutput = currentOutput; + final var result = new ReleasableBytesReference( + chunkOutput.bytes(), + () -> Releasables.closeExpectNoException(chunkOutput) + ); + currentOutput = null; + return result; + } finally { + if (currentOutput != null) { + assert false : "failure encoding table chunk"; + Releasables.closeExpectNoException(currentOutput); + currentOutput = null; + } } } - out.append("\n"); - } - out.close(); - return new RestResponse(RestStatus.OK, RestResponse.TEXT_CONTENT_TYPE, bytesOut.bytes()); + + @Override + public String getResponseContentTypeString() { + return RestResponse.TEXT_CONTENT_TYPE; + } + }); } static List getRowOrder(Table table, RestRequest request) { @@ -297,11 +393,11 @@ private static int[] buildWidths(Table table, RestRequest request, boolean verbo return width; } - public static void pad(Table.Cell cell, int width, RestRequest request, UTF8StreamWriter out) throws IOException { + public static void pad(Table.Cell cell, int width, RestRequest request, Writer out) throws IOException { pad(cell, width, request, out, false); } - public static void pad(Table.Cell cell, int width, RestRequest request, UTF8StreamWriter out, boolean isLast) throws IOException { + public static void pad(Table.Cell cell, int width, RestRequest request, Writer out, boolean isLast) throws IOException { String sValue = renderValue(request, cell.value); int length = sValue == null ? 0 : sValue.length(); byte leftOver = (byte) (width - length); diff --git a/server/src/test/java/org/elasticsearch/rest/action/cat/RestCatComponentTemplateActionTests.java b/server/src/test/java/org/elasticsearch/rest/action/cat/RestCatComponentTemplateActionTests.java index cab92c4e2292..7b22091341bb 100644 --- a/server/src/test/java/org/elasticsearch/rest/action/cat/RestCatComponentTemplateActionTests.java +++ b/server/src/test/java/org/elasticsearch/rest/action/cat/RestCatComponentTemplateActionTests.java @@ -23,6 +23,7 @@ import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponseUtils; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.test.client.NoOpNodeClient; import org.elasticsearch.test.rest.FakeRestChannel; @@ -86,7 +87,7 @@ public void testRestCatComponentAction() throws Exception { // validate results assertThat(channel.responses().get(), equalTo(1)); assertThat(channel.capturedResponse().status(), equalTo(RestStatus.OK)); - assertThat(channel.capturedResponse().content().utf8ToString(), equalTo("test_ct 2 0 0 2 0 []\n")); + assertThat(RestResponseUtils.getBodyContent(channel.capturedResponse()).utf8ToString(), equalTo("test_ct 2 0 0 2 0 []\n")); } public void testRestCatComponentActionWithParam() throws Exception { diff --git a/server/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java b/server/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java index 0856c5c787e6..780b7776ff9e 100644 --- a/server/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java +++ b/server/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java @@ -8,7 +8,9 @@ package org.elasticsearch.rest.action.cat; +import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.Table; +import org.elasticsearch.common.recycler.Recycler; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.rest.AbstractRestChannel; import org.elasticsearch.rest.RestResponse; @@ -17,17 +19,21 @@ import org.elasticsearch.xcontent.XContentType; import org.junit.Before; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import static org.elasticsearch.rest.action.cat.RestTable.buildDisplayHeaders; import static org.elasticsearch.rest.action.cat.RestTable.buildResponse; +import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalToIgnoringCase; +import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.not; @@ -253,6 +259,92 @@ public void testMultiSort() { assertEquals(Arrays.asList(1, 0, 2), rowOrder); } + public void testPlainTextChunking() throws Exception { + final var expectedBody = new StringBuilder(); + final var rowCount = between(10000, 20000); + for (int i = 0; i < rowCount; i++) { + table.startRow(); + table.addCell("foo"); + table.addCell("foo"); + table.addCell("foo"); + table.addCell("foo"); + table.addCell("foo"); + table.addCell("foo"); + table.addCell("foo"); + table.addCell("foo"); + table.endRow(); + expectedBody.append(TEXT_TABLE_BODY); + } + + final var request = new FakeRestRequest.Builder(xContentRegistry()).withHeaders( + Collections.singletonMap(ACCEPT, Collections.singletonList(TEXT_PLAIN)) + ).build(); + + final var response = buildResponse(table, new AbstractRestChannel(request, true) { + @Override + public void sendResponse(RestResponse response) {} + }); + + // OutputStreamWriter has an 8kiB buffer so all chunks are at least that big anyway + final var bodyChunks = getBodyChunks(response, 8192); + final var rowLength = TEXT_TABLE_BODY.length(); + final var expectedChunkSize = ((8193 + rowLength) / rowLength) * rowLength; // ceil(8193/rowLength) * rowLength + assertThat(bodyChunks.size(), allOf(greaterThan(1), equalTo((rowCount * rowLength + expectedChunkSize) / expectedChunkSize))); + assertThat(bodyChunks.get(0).length(), equalTo(expectedChunkSize)); + assertEquals(expectedBody.toString(), String.join("", bodyChunks)); + } + + public void testEmptyTable() throws Exception { + final var request = new FakeRestRequest.Builder(xContentRegistry()).withHeaders( + Collections.singletonMap(ACCEPT, Collections.singletonList(TEXT_PLAIN)) + ).build(); + + final var response = buildResponse(table, new AbstractRestChannel(request, true) { + @Override + public void sendResponse(RestResponse response) {} + }); + + assertFalse(response.isChunked()); + assertThat(response.content().length(), equalTo(0)); + } + + public void testXContentChunking() throws Exception { + final var jsonTableRow = JSON_TABLE_BODY.substring(1, JSON_TABLE_BODY.length() - 2).replaceAll("\\s+", ""); + final var expectedBody = new StringBuilder("["); + final var rowCount = between(10000, 20000); + for (int i = 0; i < rowCount; i++) { + if (i != 0) { + expectedBody.append(","); + } + table.startRow(); + table.addCell("foo"); + table.addCell("foo"); + table.addCell("foo"); + table.addCell("foo"); + table.addCell("foo"); + table.addCell("foo"); + table.addCell("foo"); + table.addCell("foo"); + table.endRow(); + expectedBody.append(jsonTableRow); + } + expectedBody.append("]"); + + final var request = new FakeRestRequest.Builder(xContentRegistry()).withHeaders( + Collections.singletonMap(ACCEPT, Collections.singletonList(APPLICATION_JSON)) + ).build(); + + final var response = buildResponse(table, new AbstractRestChannel(request, true) { + @Override + public void sendResponse(RestResponse response) {} + }); + + // layers of buffering make it hard to be precise here: + final var bodyChunks = getBodyChunks(response, 8192); + assertEquals(expectedBody.toString(), String.join("", bodyChunks)); + assertThat(bodyChunks.size(), greaterThan(1)); + } + private RestResponse assertResponseContentType(Map> headers, String mediaType) throws Exception { FakeRestRequest requestWithAcceptHeader = new FakeRestRequest.Builder(xContentRegistry()).withHeaders(headers).build(); table.startRow(); @@ -277,7 +369,50 @@ public void sendResponse(RestResponse response) {} private void assertResponse(Map> headers, String mediaType, String body) throws Exception { RestResponse response = assertResponseContentType(headers, mediaType); - assertThat(response.content().utf8ToString(), equalTo(body)); + assertTrue(response.isChunked()); + assertThat(String.join("", getBodyChunks(response, between(1, 1024))), equalTo(body)); + } + + private static List getBodyChunks(RestResponse response, final int pageSize) throws IOException { + assertTrue(response.isChunked()); + + final var openPages = new AtomicInteger(); + final var recycler = new Recycler() { + @Override + public V obtain() { + openPages.incrementAndGet(); + return new V<>() { + final BytesRef page = new BytesRef(new byte[pageSize], 0, pageSize); + + @Override + public BytesRef v() { + return page; + } + + @Override + public boolean isRecycled() { + return false; + } + + @Override + public void close() { + openPages.decrementAndGet(); + } + }; + } + }; + + final var bodyChunks = new ArrayList(); + final var chunkedRestResponseBody = response.chunkedContent(); + + while (chunkedRestResponseBody.isDone() == false) { + try (var chunk = chunkedRestResponseBody.encodeChunk(pageSize, recycler)) { + assertThat(chunk.length(), greaterThan(0)); + bodyChunks.add(chunk.utf8ToString()); + } + } + assertEquals(0, openPages.get()); + return bodyChunks; } private List getHeaderNames(List headers) { diff --git a/test/framework/src/main/java/org/elasticsearch/rest/RestResponseUtils.java b/test/framework/src/main/java/org/elasticsearch/rest/RestResponseUtils.java new file mode 100644 index 000000000000..47b679e89463 --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/rest/RestResponseUtils.java @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.rest; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.BytesStreamOutput; + +import java.io.IOException; + +import static org.elasticsearch.transport.BytesRefRecycler.NON_RECYCLING_INSTANCE; + +public class RestResponseUtils { + private RestResponseUtils() {} + + public static BytesReference getBodyContent(RestResponse restResponse) throws IOException { + if (restResponse.isChunked() == false) { + return restResponse.content(); + } + + final var chunkedRestResponseBody = restResponse.chunkedContent(); + assert chunkedRestResponseBody.isDone() == false; + + final int pageSize; + try (var page = NON_RECYCLING_INSTANCE.obtain()) { + pageSize = page.v().length; + } + + try (var out = new BytesStreamOutput()) { + while (chunkedRestResponseBody.isDone() == false) { + try (var chunk = chunkedRestResponseBody.encodeChunk(pageSize, NON_RECYCLING_INSTANCE)) { + chunk.writeTo(out); + } + } + + out.flush(); + return out.bytes(); + } + } +} From 337b65ccea230b58c247a7ace5c05d6b56bd3b44 Mon Sep 17 00:00:00 2001 From: Rene Groeschke Date: Tue, 6 Dec 2022 11:33:11 +0100 Subject: [PATCH 159/919] [CI] Fix PluginServiceTests on windows (#92096) Ensure we close internal url classloader of UberModuleClassLoader in tests closes #88275 --- .../elasticsearch/plugins/UberModuleClassLoader.java | 4 ++++ .../org/elasticsearch/plugins/PluginsServiceTests.java | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/plugins/UberModuleClassLoader.java b/server/src/main/java/org/elasticsearch/plugins/UberModuleClassLoader.java index 5f4bec977bcf..dca3afb2ed74 100644 --- a/server/src/main/java/org/elasticsearch/plugins/UberModuleClassLoader.java +++ b/server/src/main/java/org/elasticsearch/plugins/UberModuleClassLoader.java @@ -318,4 +318,8 @@ public void close() throws Exception { AccessController.doPrivileged(pa); } + // visible for testing + public URLClassLoader getInternalLoader() { + return internalLoader; + } } diff --git a/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java b/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java index 5a75a9c89d45..2fd848370ce9 100644 --- a/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java +++ b/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java @@ -841,7 +841,6 @@ public Reader create(Reader reader) { Class stableClass = stablePluginClassLoader.loadClass("p.A"); assertThat(stableClass.getModule().getName(), equalTo("synthetic.stable.plugin")); - // TODO should we add something to pluginInfos.get(0).pluginApiInfo() ? } finally { closePluginLoaders(pluginService); @@ -877,7 +876,7 @@ static final class Loader extends ClassLoader { } } - // Closes the URLClassLoaders of plugins loaded by the given plugin service. + // Closes the URLClassLoaders and UberModuleClassloaders of plugins loaded by the given plugin service. static void closePluginLoaders(PluginsService pluginService) { for (var lp : pluginService.plugins()) { if (lp.loader()instanceof URLClassLoader urlClassLoader) { @@ -887,6 +886,13 @@ static void closePluginLoaders(PluginsService pluginService) { throw new UncheckedIOException(unexpected); } } + if (lp.loader()instanceof UberModuleClassLoader loader) { + try { + PrivilegedOperations.closeURLClassLoader(loader.getInternalLoader()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } } } From c9ae9123fe589c6e0800eb072b51feda22166d22 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 6 Dec 2022 11:10:18 +0000 Subject: [PATCH 160/919] Add docs for desired balance allocator (#92109) These docs cover the new allocator and the settings controlling the heuristics for combining disk usage and write load into the overall weight. --- .../cluster/shards_allocation.asciidoc | 118 ++++++++++++------ 1 file changed, 82 insertions(+), 36 deletions(-) diff --git a/docs/reference/modules/cluster/shards_allocation.asciidoc b/docs/reference/modules/cluster/shards_allocation.asciidoc index b9fd5cb868dd..0c3f028a6c3a 100644 --- a/docs/reference/modules/cluster/shards_allocation.asciidoc +++ b/docs/reference/modules/cluster/shards_allocation.asciidoc @@ -57,17 +57,18 @@ one of the active allocation ids in the cluster state. [[shards-rebalancing-settings]] ==== Shard rebalancing settings -A cluster is _balanced_ when it has an equal number of shards on each node -without having a concentration of shards from any index on any node. {es} runs -an automatic process called _rebalancing_ which moves shards between the nodes -in your cluster to improve its balance. Rebalancing obeys all other shard -allocation rules such as <> and <> which may prevent it from -completely balancing the cluster. In that case, rebalancing strives to achieve -the most balanced cluster possible within the rules you have configured. If you -are using <> then {es} automatically applies allocation -filtering rules to place each shard within the appropriate tier. These rules -mean that the balancer works independently within each tier. +A cluster is _balanced_ when it has an equal number of shards on each node, with +all nodes needing equal resources, without having a concentration of shards from +any index on any node. {es} runs an automatic process called _rebalancing_ which +moves shards between the nodes in your cluster to improve its balance. +Rebalancing obeys all other shard allocation rules such as +<> and +<> which may prevent it from completely +balancing the cluster. In that case, rebalancing strives to achieve the most +balanced cluster possible within the rules you have configured. If you are using +<> then {es} automatically applies allocation filtering +rules to place each shard within the appropriate tier. These rules mean that the +balancer works independently within each tier. You can use the following settings to control the rebalancing of shards across the cluster: @@ -84,7 +85,6 @@ Enable or disable rebalancing for specific kinds of shards: * `none` - No shard balancing of any kind are allowed for any indices. -- - `cluster.routing.allocation.allow_rebalance`:: + -- @@ -98,13 +98,32 @@ Specify when shard rebalancing is allowed: -- `cluster.routing.allocation.cluster_concurrent_rebalance`:: - (<>) - Allow to control how many concurrent shard rebalances are - allowed cluster wide. Defaults to `2`. Note that this setting - only controls the number of concurrent shard relocations due - to imbalances in the cluster. This setting does not limit shard - relocations due to <> or <>. +(<>) +Defines the number of concurrent shard rebalances are allowed across the whole +cluster. Defaults to `2`. Note that this setting only controls the number of +concurrent shard relocations due to imbalances in the cluster. This setting does +not limit shard relocations due to +<> or +<>. + +`cluster.routing.allocation.type`:: ++ +-- +Selects the algorithm used for computing the cluster balance. Defaults to +`desired_balance` which selects the _desired balance allocator_. This allocator +runs a background task which computes the desired balance of shards in the +cluster. Once this background task completes, {es} moves shards to their +desired locations. + +May also be set to `balanced` to select the legacy _balanced allocator_. This +allocator was the default allocator in versions of {es} before 8.6.0. It runs +in the foreground, preventing the master from doing other work in parallel. It +works by selecting a small number of shard movements which immediately improve +the balance of the cluster, and when those shard movements complete it runs +again and selects another few shards to move. Since this allocator makes its +decisions based only on the current state of the cluster, it will sometimes +move a shard several times while balancing the cluster. +-- [[shards-rebalancing-heuristics]] ==== Shard balancing heuristics settings @@ -114,28 +133,55 @@ of shards, and then moving shards between nodes to reduce the weight of the heavier nodes and increase the weight of the lighter ones. The cluster is balanced when there is no possible shard movement that can bring the weight of any node closer to the weight of any other node by more than a configurable -threshold. The following settings allow you to control the details of these -calculations. +threshold. + +The weight of a node depends on the number of shards it holds and on the total +estimated resource usage of those shards expressed in terms of the size of the +shard on disk and the number of threads needed to support write traffic to the +shard. {es} estimates the resource usage of shards belonging to data streams +when they are created by a rollover. The estimated disk size of the new shard +is the mean size of the other shards in the data stream. The estimated write +load of the new shard is a weighted average of the actual write loads of recent +shards in the data stream. Shards that do not belong to the write index of a +data stream have an estimated write load of zero. + +The following settings control how {es} combines these values into an overall +measure of each node's weight. `cluster.routing.allocation.balance.shard`:: - (<>) - Defines the weight factor for the total number of shards allocated on a node - (float). Defaults to `0.45f`. Raising this raises the tendency to - equalize the number of shards across all nodes in the cluster. +(float, <>) +Defines the weight factor for the total number of shards allocated to each node. +Defaults to `0.45f`. Raising this value increases the tendency of {es} to +equalize the total number of shards across nodes ahead of the other balancing +variables. `cluster.routing.allocation.balance.index`:: - (<>) - Defines the weight factor for the number of shards per index allocated - on a specific node (float). Defaults to `0.55f`. Raising this raises the - tendency to equalize the number of shards per index across all nodes in - the cluster. +(float, <>) +Defines the weight factor for the number of shards per index allocated to each +node. Defaults to `0.55f`. Raising this value increases the tendency of {es} to +equalize the number of shards of each index across nodes ahead of the other +balancing variables. + +`cluster.routing.allocation.balance.disk_usage`:: +(float, <>) +Defines the weight factor for balancing shards according to their predicted disk +size in bytes. Defaults to `2e-11f`. Raising this value increases the tendency +of {es} to equalize the total disk usage across nodes ahead of the other +balancing variables. + +`cluster.routing.allocation.balance.write_load`:: +(float, <>) +Defines the weight factor for the write load of each shard, in terms of the +estimated number of indexing threads needed by the shard. Defaults to `10.0f`. +Raising this value increases the tendency of {es} to equalize the total write +load across nodes ahead of the other balancing variables. `cluster.routing.allocation.balance.threshold`:: - (<>) - Minimal optimization value of operations that should be performed (non - negative float). Defaults to `1.0f`. Raising this will cause the cluster - to be less aggressive about optimizing the shard balance. - +(float, <>) +The minimum improvement in weight which triggers a rebalancing shard movement. +Defaults to `1.0f`. Raising this value will cause {es} to stop rebalancing +shards sooner, leaving the cluster in a more unbalanced state. NOTE: Regardless of the result of the balancing algorithm, rebalancing might -not be allowed due to forced awareness or allocation filtering. +not be allowed due to allocation rules such as forced awareness and allocation +filtering. From 121199d038a427260afbfc5e969fa3a6cf9ba9cf Mon Sep 17 00:00:00 2001 From: Craig Taverner Date: Tue, 6 Dec 2022 13:03:48 +0100 Subject: [PATCH 161/919] Mute AzimuthTests.testLatLonVec3d for #92136 (#92145) --- libs/h3/src/test/java/org/elasticsearch/h3/AzimuthTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/h3/src/test/java/org/elasticsearch/h3/AzimuthTests.java b/libs/h3/src/test/java/org/elasticsearch/h3/AzimuthTests.java index 1f342d0cc43d..034ab39b0b28 100644 --- a/libs/h3/src/test/java/org/elasticsearch/h3/AzimuthTests.java +++ b/libs/h3/src/test/java/org/elasticsearch/h3/AzimuthTests.java @@ -25,6 +25,7 @@ public class AzimuthTests extends ESTestCase { + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/92136") public void testLatLonVec3d() { final double lat = Math.toRadians(GeoTestUtil.nextLatitude()); final double lon = Math.toRadians(GeoTestUtil.nextLongitude()); From 222c463b815d7aa14a20e96f19332b14c15520de Mon Sep 17 00:00:00 2001 From: Craig Taverner Date: Tue, 6 Dec 2022 13:04:15 +0100 Subject: [PATCH 162/919] Mute LatLonGeometryRelationVisitorTests.testLine (#92146) --- .../index/fielddata/LatLonGeometryRelationVisitorTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/LatLonGeometryRelationVisitorTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/LatLonGeometryRelationVisitorTests.java index bc83b1efb068..0e3a8cf3d88d 100644 --- a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/LatLonGeometryRelationVisitorTests.java +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/LatLonGeometryRelationVisitorTests.java @@ -26,6 +26,7 @@ public void testPoint() throws Exception { doTestShapes(GeoTestUtil::nextPoint); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/92142") public void testLine() throws Exception { doTestShapes(GeoTestUtil::nextLine); } From 7c3ced2f5c5d6dfa1985e1f2fc9f43a7a1a488c5 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Tue, 6 Dec 2022 13:10:43 +0100 Subject: [PATCH 163/919] Chunked X-Content Serialisation for ClusterState Customs (#92095) Adds chunking to all CS metadata customs. Added a few utilities to make this a little less painful to write and read. Admittedly didn't add tests to all the chunking implementations since I don't think they really matter all that much size wise with the exception of `DataStreamMetadata` which I'd like to take one step further in a follow-up anyway (the index name lists can become very long and should be individually chunked, but that would make this PR even more complicated than it already is). --- .../get/GetRepositoriesResponse.java | 4 +- .../ElasticsearchNodeCommand.java | 7 +- .../metadata/ComponentTemplateMetadata.java | 13 ++-- .../ComposableIndexTemplateMetadata.java | 13 ++-- .../cluster/metadata/DataStreamMetadata.java | 20 +++--- .../metadata/DesiredNodesMetadata.java | 9 +-- .../cluster/metadata/IndexGraveyard.java | 11 ++- .../cluster/metadata/Metadata.java | 5 +- .../metadata/NodesShutdownMetadata.java | 9 +-- .../metadata/RepositoriesMetadata.java | 17 +++-- .../xcontent/ChunkedToXContentHelper.java | 72 +++++++++++++++++++ .../elasticsearch/ingest/IngestMetadata.java | 13 ++-- .../PersistentTasksCustomMetadata.java | 18 +++-- .../elasticsearch/script/ScriptMetadata.java | 27 ++----- .../upgrades/FeatureMigrationResults.java | 9 +-- .../get/GetSnapshotsResponseTests.java | 5 +- .../metadata/DataStreamMetadataTests.java | 29 ++++---- ...esiredNodesMetadataSerializationTests.java | 9 ++- .../cluster/metadata/IndexGraveyardTests.java | 20 +++++- .../cluster/metadata/MetadataTests.java | 7 +- .../metadata/NodesShutdownMetadataTests.java | 9 ++- .../ingest/IngestMetadataTests.java | 26 ++++++- .../PersistentTasksCustomMetadataTests.java | 11 ++- .../script/ScriptMetadataTests.java | 9 ++- ...epositoriesMetadataSerializationTests.java | 8 ++- .../FeatureMigrationResultsTests.java | 9 ++- .../AbstractChunkedSerializingTestCase.java | 17 ++++- .../test/AbstractXContentTestCase.java | 9 ++- ...XContentDiffableSerializationTestCase.java | 43 +++++++++++ .../test/TestCustomMetadata.java | 9 +-- .../autoscaling/AutoscalingMetadata.java | 9 +-- ...ingMetadataDiffableSerializationTests.java | 11 +-- .../xpack/ccr/AutoFollowMetadataTests.java | 9 ++- .../license/LicensesMetadata.java | 30 ++++---- .../xpack/core/ccr/AutoFollowMetadata.java | 35 +++------ .../core/ilm/IndexLifecycleMetadata.java | 14 ++-- .../xpack/core/ml/MlMetadata.java | 13 ++-- .../core/slm/SnapshotLifecycleMetadata.java | 19 +++-- .../core/transform/TransformMetadata.java | 8 +-- .../xpack/core/watcher/WatcherMetadata.java | 9 +-- .../actions/stats/WatcherStatsResponse.java | 5 +- .../LicensesMetadataSerializationTests.java | 7 +- .../slm/SnapshotLifecycleMetadataTests.java | 9 ++- .../xpack/enrich/EnrichMetadata.java | 15 ++-- .../xpack/enrich/EnrichMetadataTests.java | 9 ++- .../ilm/IndexLifecycleMetadataTests.java | 9 ++- .../ml/inference/ModelAliasMetadata.java | 12 ++-- .../TrainedModelAssignmentMetadata.java | 11 +-- .../xpack/ml/MlMetadataTests.java | 9 ++- .../TrainedModelAssignmentMetadataTests.java | 8 ++- .../transform/TransformMetadataTests.java | 9 ++- .../WatcherMetadataSerializationTests.java | 3 +- 52 files changed, 483 insertions(+), 247 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContentHelper.java create mode 100644 test/framework/src/main/java/org/elasticsearch/test/ChunkedToXContentDiffableSerializationTestCase.java diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/repositories/get/GetRepositoriesResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/repositories/get/GetRepositoriesResponse.java index da8642241f99..a465a53cb5fd 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/repositories/get/GetRepositoriesResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/repositories/get/GetRepositoriesResponse.java @@ -13,6 +13,7 @@ import org.elasticsearch.cluster.metadata.RepositoryMetadata; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; @@ -55,7 +56,8 @@ public void writeTo(StreamOutput out) throws IOException { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - repositories.toXContent(builder, new DelegatingMapParams(Map.of(RepositoriesMetadata.HIDE_GENERATIONS_PARAM, "true"), params)); + ChunkedToXContent.wrapAsXContentObject(repositories) + .toXContent(builder, new DelegatingMapParams(Map.of(RepositoriesMetadata.HIDE_GENERATIONS_PARAM, "true"), params)); builder.endObject(); return builder; } diff --git a/server/src/main/java/org/elasticsearch/cluster/coordination/ElasticsearchNodeCommand.java b/server/src/main/java/org/elasticsearch/cluster/coordination/ElasticsearchNodeCommand.java index 6e996bc29a52..b4b7c0be66c3 100644 --- a/server/src/main/java/org/elasticsearch/cluster/coordination/ElasticsearchNodeCommand.java +++ b/server/src/main/java/org/elasticsearch/cluster/coordination/ElasticsearchNodeCommand.java @@ -28,6 +28,7 @@ import org.elasticsearch.cluster.metadata.DataStreamMetadata; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.cli.EnvironmentAwareCommand; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; @@ -37,6 +38,7 @@ import org.elasticsearch.env.NodeMetadata; import org.elasticsearch.gateway.PersistedClusterStateService; import org.elasticsearch.xcontent.NamedXContentRegistry; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; @@ -45,6 +47,7 @@ import java.nio.file.Path; import java.util.Arrays; import java.util.EnumSet; +import java.util.Iterator; import java.util.Map; import java.util.Objects; @@ -229,8 +232,8 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - return builder.mapContents(contents); + public Iterator toXContentChunked(ToXContent.Params ignored) { + return Iterators.single(((builder, params) -> builder.mapContents(contents))); } } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/ComponentTemplateMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/ComponentTemplateMetadata.java index 92fe6f18f9b5..6e1ff6e89c7e 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/ComponentTemplateMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/ComponentTemplateMetadata.java @@ -15,14 +15,16 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; import java.util.EnumSet; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.Objects; @@ -98,13 +100,8 @@ public static ComponentTemplateMetadata fromXContent(XContentParser parser) thro } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(COMPONENT_TEMPLATE.getPreferredName()); - for (Map.Entry template : componentTemplates.entrySet()) { - builder.field(template.getKey(), template.getValue(), params); - } - builder.endObject(); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return ChunkedToXContentHelper.xContentValuesMap(COMPONENT_TEMPLATE.getPreferredName(), componentTemplates); } @Override diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplateMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplateMetadata.java index 1b5193510aa6..162d128464fc 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplateMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/ComposableIndexTemplateMetadata.java @@ -15,14 +15,16 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; import java.util.EnumSet; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.Objects; @@ -99,13 +101,8 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(INDEX_TEMPLATE.getPreferredName()); - for (Map.Entry template : indexTemplates.entrySet()) { - builder.field(template.getKey(), template.getValue(), params); - } - builder.endObject(); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return ChunkedToXContentHelper.xContentValuesMap(INDEX_TEMPLATE.getPreferredName(), indexTemplates); } @Override diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamMetadata.java index ff05aab210f6..8099c456e89e 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamMetadata.java @@ -15,12 +15,14 @@ import org.elasticsearch.cluster.NamedDiff; import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.ImmutableOpenMap; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentFactory; import org.elasticsearch.xcontent.XContentParser; @@ -28,6 +30,7 @@ import java.util.ArrayList; import java.util.EnumSet; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -222,14 +225,13 @@ public static DataStreamMetadata fromXContent(XContentParser parser) throws IOEx } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.xContentValuesMap(DATA_STREAM.getPreferredName(), dataStreams); - builder.startObject(DATA_STREAM_ALIASES.getPreferredName()); - for (Map.Entry dataStream : dataStreamAliases.entrySet()) { - dataStream.getValue().toXContent(builder, params); - } - builder.endObject(); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return Iterators.concat( + ChunkedToXContentHelper.xContentValuesMap(DATA_STREAM.getPreferredName(), dataStreams), + ChunkedToXContentHelper.startObject(DATA_STREAM_ALIASES.getPreferredName()), + dataStreamAliases.values().iterator(), + ChunkedToXContentHelper.endObject() + ); } @Override diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/DesiredNodesMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/DesiredNodesMetadata.java index 64a384f77e12..b7f18a7ba007 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/DesiredNodesMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/DesiredNodesMetadata.java @@ -12,16 +12,18 @@ import org.elasticsearch.cluster.AbstractNamedDiffable; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.NamedDiff; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.core.Nullable; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; import java.util.EnumSet; +import java.util.Iterator; import java.util.Objects; public class DesiredNodesMetadata extends AbstractNamedDiffable implements Metadata.Custom { @@ -67,9 +69,8 @@ public static DesiredNodesMetadata fromXContent(XContentParser parser) throws IO } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.field(LATEST_FIELD.getPreferredName(), latestDesiredNodes, params); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return Iterators.single((builder, params) -> builder.field(LATEST_FIELD.getPreferredName(), latestDesiredNodes, params)); } public static DesiredNodesMetadata fromClusterState(ClusterState clusterState) { diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexGraveyard.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexGraveyard.java index 7b600d875c79..900c4ff80f00 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexGraveyard.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexGraveyard.java @@ -17,10 +17,12 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.time.DateFormatter; +import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.index.Index; import org.elasticsearch.xcontent.ContextParser; import org.elasticsearch.xcontent.ObjectParser; import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; @@ -32,6 +34,7 @@ import java.util.Collection; import java.util.Collections; import java.util.EnumSet; +import java.util.Iterator; import java.util.List; import java.util.Objects; @@ -123,12 +126,8 @@ public boolean containsIndex(final Index index) { } @Override - public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { - builder.startArray(TOMBSTONES_FIELD.getPreferredName()); - for (Tombstone tombstone : tombstones) { - tombstone.toXContent(builder, params); - } - return builder.endArray(); + public Iterator toXContentChunked(ToXContent.Params ignored) { + return ChunkedToXContentHelper.array(TOMBSTONES_FIELD.getPreferredName(), tombstones); } public static IndexGraveyard fromXContent(final XContentParser parser) throws IOException { diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java index 21dd59e79199..33bd5a0a1fe6 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java @@ -37,6 +37,7 @@ import org.elasticsearch.common.util.ArrayUtils; import org.elasticsearch.common.util.Maps; import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParserUtils; import org.elasticsearch.core.Nullable; @@ -137,7 +138,7 @@ public enum XContentContext { * Custom metadata that persists (via XContent) across restarts. The deserialization method for each implementation must be registered * with the {@link NamedXContentRegistry}. */ - public interface Custom extends NamedDiffable, ToXContentFragment { + public interface Custom extends NamedDiffable, ChunkedToXContent { EnumSet context(); @@ -2508,7 +2509,7 @@ public static void toXContent(Metadata metadata, XContentBuilder builder, ToXCon for (Map.Entry cursor : metadata.customs().entrySet()) { if (cursor.getValue().context().contains(context)) { builder.startObject(cursor.getKey()); - cursor.getValue().toXContent(builder, params); + ChunkedToXContent.wrapAsXContentObject(cursor.getValue()).toXContent(builder, params); builder.endObject(); } } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/NodesShutdownMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/NodesShutdownMetadata.java index ab1f1a6812f5..2200fee5b6c2 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/NodesShutdownMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/NodesShutdownMetadata.java @@ -16,14 +16,16 @@ import org.elasticsearch.cluster.SimpleDiffable; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; import java.util.EnumSet; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -167,9 +169,8 @@ public int hashCode() { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.field(NODES_FIELD.getPreferredName(), nodes); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return ChunkedToXContentHelper.xContentValuesMap(NODES_FIELD.getPreferredName(), nodes); } /** diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/RepositoriesMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/RepositoriesMetadata.java index 720e57f7a194..c5548e5138b7 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/RepositoriesMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/RepositoriesMetadata.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; +import java.util.Iterator; import java.util.List; import java.util.function.UnaryOperator; @@ -250,15 +251,11 @@ public static RepositoriesMetadata fromXContent(XContentParser parser) throws IO return new RepositoriesMetadata(repository); } - /** - * {@inheritDoc} - */ @Override - public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { - for (RepositoryMetadata repository : repositories) { - toXContent(repository, builder, params); - } - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return repositories.stream() + .map(repository -> (ToXContent) (builder, params) -> toXContent(repository, builder, params)) + .iterator(); } @Override @@ -273,7 +270,8 @@ public EnumSet context() { * @param builder XContent builder * @param params serialization parameters */ - public static void toXContent(RepositoryMetadata repository, XContentBuilder builder, ToXContent.Params params) throws IOException { + public static XContentBuilder toXContent(RepositoryMetadata repository, XContentBuilder builder, ToXContent.Params params) + throws IOException { builder.startObject(repository.name()); builder.field("type", repository.type()); if (repository.uuid().equals(RepositoryData.MISSING_UUID) == false) { @@ -288,6 +286,7 @@ public static void toXContent(RepositoryMetadata repository, XContentBuilder bui builder.field("pending_generation", repository.pendingGeneration()); } builder.endObject(); + return builder; } @Override diff --git a/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContentHelper.java b/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContentHelper.java new file mode 100644 index 000000000000..bfae6c2a082d --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContentHelper.java @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.common.xcontent; + +import org.elasticsearch.common.collect.Iterators; +import org.elasticsearch.xcontent.ToXContent; + +import java.util.Iterator; +import java.util.Map; +import java.util.function.Function; + +public enum ChunkedToXContentHelper { + ; + + public static Iterator startObject() { + return Iterators.single(((builder, params) -> builder.startObject())); + } + + public static Iterator startObject(String name) { + return Iterators.single(((builder, params) -> builder.startObject(name))); + } + + public static Iterator endObject() { + return Iterators.single(((builder, params) -> builder.endObject())); + } + + public static Iterator startArray(String name) { + return Iterators.single(((builder, params) -> builder.startArray(name))); + } + + public static Iterator endArray() { + return Iterators.single(((builder, params) -> builder.endArray())); + } + + public static Iterator map(String name, Map map) { + return map(name, map, entry -> (ToXContent) (builder, params) -> builder.field(entry.getKey(), entry.getValue())); + } + + public static Iterator xContentFragmentValuesMap(String name, Map map) { + return map( + name, + map, + entry -> (ToXContent) (builder, params) -> entry.getValue().toXContent(builder.startObject(entry.getKey()), params).endObject() + ); + } + + public static Iterator xContentValuesMap(String name, Map map) { + return map( + name, + map, + entry -> (ToXContent) (builder, params) -> entry.getValue().toXContent(builder.field(entry.getKey()), params) + ); + } + + public static Iterator field(String name, boolean value) { + return Iterators.single(((builder, params) -> builder.field(name, value))); + } + + public static Iterator array(String name, Iterable iterable) { + return Iterators.concat(ChunkedToXContentHelper.startArray(name), iterable.iterator(), ChunkedToXContentHelper.endArray()); + } + + private static Iterator map(String name, Map map, Function, ToXContent> toXContent) { + return Iterators.concat(startObject(name), map.entrySet().stream().map(toXContent).iterator(), endObject()); + } +} diff --git a/server/src/main/java/org/elasticsearch/ingest/IngestMetadata.java b/server/src/main/java/org/elasticsearch/ingest/IngestMetadata.java index 401301e9f0e6..eff4be93e78a 100644 --- a/server/src/main/java/org/elasticsearch/ingest/IngestMetadata.java +++ b/server/src/main/java/org/elasticsearch/ingest/IngestMetadata.java @@ -16,9 +16,10 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.util.Maps; +import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.xcontent.ObjectParser; import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; @@ -26,6 +27,7 @@ import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; @@ -95,13 +97,8 @@ public static IngestMetadata fromXContent(XContentParser parser) throws IOExcept } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startArray(PIPELINES_FIELD.getPreferredName()); - for (PipelineConfiguration pipeline : pipelines.values()) { - pipeline.toXContent(builder, params); - } - builder.endArray(); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return ChunkedToXContentHelper.array(PIPELINES_FIELD.getPreferredName(), pipelines.values()); } @Override diff --git a/server/src/main/java/org/elasticsearch/persistent/PersistentTasksCustomMetadata.java b/server/src/main/java/org/elasticsearch/persistent/PersistentTasksCustomMetadata.java index d7d542c7117b..515540a0d79d 100644 --- a/server/src/main/java/org/elasticsearch/persistent/PersistentTasksCustomMetadata.java +++ b/server/src/main/java/org/elasticsearch/persistent/PersistentTasksCustomMetadata.java @@ -15,10 +15,12 @@ import org.elasticsearch.cluster.NamedDiff; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.VersionedNamedWriteable; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.core.Nullable; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ObjectParser; @@ -34,6 +36,7 @@ import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -552,16 +555,11 @@ public static NamedDiff readDiffFrom(StreamInput in) throws IOE } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.field("last_allocation_id", lastAllocationId); - builder.startArray("tasks"); - { - for (PersistentTask entry : tasks.values()) { - entry.toXContent(builder, params); - } - } - builder.endArray(); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return Iterators.concat( + Iterators.single((builder, params) -> builder.field("last_allocation_id", lastAllocationId)), + ChunkedToXContentHelper.array("tasks", tasks.values()) + ); } public static Builder builder() { diff --git a/server/src/main/java/org/elasticsearch/script/ScriptMetadata.java b/server/src/main/java/org/elasticsearch/script/ScriptMetadata.java index f63593df5c44..bda1e7af5877 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptMetadata.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptMetadata.java @@ -20,8 +20,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.xcontent.ToXContentFragment; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentParser.Token; @@ -29,13 +28,14 @@ import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; /** * {@link ScriptMetadata} is used to store user-defined scripts * as part of the {@link ClusterState} using only an id as the key. */ -public final class ScriptMetadata implements Metadata.Custom, Writeable, ToXContentFragment { +public final class ScriptMetadata implements Metadata.Custom, Writeable { /** * Standard logger used to warn about dropped scripts. @@ -260,25 +260,12 @@ public void writeTo(StreamOutput out) throws IOException { out.writeMap(scripts, StreamOutput::writeString, (o, v) -> v.writeTo(o)); } - /** - * This will write XContent from {@link ScriptMetadata}. The following format will be written: - * - * {@code - * { - * "" : "<{@link StoredScriptSource#toXContent(XContentBuilder, Params)}>", - * "" : "<{@link StoredScriptSource#toXContent(XContentBuilder, Params)}>", - * ... - * } - * } - */ @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - for (Map.Entry entry : scripts.entrySet()) { + public Iterator toXContentChunked(ToXContent.Params ignored) { + return scripts.entrySet().stream().map(entry -> (ToXContent) (builder, params) -> { builder.field(entry.getKey()); - entry.getValue().toXContent(builder, params); - } - - return builder; + return entry.getValue().toXContent(builder, params); + }).iterator(); } @Override diff --git a/server/src/main/java/org/elasticsearch/upgrades/FeatureMigrationResults.java b/server/src/main/java/org/elasticsearch/upgrades/FeatureMigrationResults.java index 5fda8daf0038..1d32babce9b2 100644 --- a/server/src/main/java/org/elasticsearch/upgrades/FeatureMigrationResults.java +++ b/server/src/main/java/org/elasticsearch/upgrades/FeatureMigrationResults.java @@ -16,16 +16,18 @@ import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.core.Tuple; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -78,9 +80,8 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.field(RESULTS_FIELD.getPreferredName(), featureStatuses); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return ChunkedToXContentHelper.xContentValuesMap(RESULTS_FIELD.getPreferredName(), featureStatuses); } public static FeatureMigrationResults fromXContent(XContentParser parser) { diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponseTests.java index bd7e416409ef..e730665fe5c8 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponseTests.java @@ -165,9 +165,8 @@ public void testFromXContent() throws IOException { .asMatchPredicate() .or(Pattern.compile("snapshots\\.\\d+\\.index_details").asMatchPredicate()) .or(Pattern.compile("failures\\.*").asMatchPredicate()); - chunkedXContentTester(this::createParser, (XContentType t) -> createTestInstance(), params, this::doParseInstance).numberOfTestRuns( - 1 - ) + chunkedXContentTester(this::createParser, (XContentType t) -> createTestInstance(), params, this::doParseInstance, false) + .numberOfTestRuns(1) .supportsUnknownFields(true) .shuffleFieldsExceptions(Strings.EMPTY_ARRAY) .randomFieldsExcludeFilter(predicate) diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamMetadataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamMetadataTests.java index 9cb4d5b70902..65254061bf12 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamMetadataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamMetadataTests.java @@ -10,8 +10,9 @@ import org.elasticsearch.common.collect.ImmutableOpenMap; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; -import org.elasticsearch.test.AbstractNamedWriteableTestCase; -import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractChunkedSerializingTestCase; +import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; import java.util.Collections; @@ -20,15 +21,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; -import static org.elasticsearch.test.AbstractXContentTestCase.xContentTester; - -public class DataStreamMetadataTests extends AbstractNamedWriteableTestCase { - - public void testFromXContent() throws IOException { - xContentTester(this::createParser, this::createTestInstance, ToXContent.EMPTY_PARAMS, DataStreamMetadata::fromXContent) - .assertEqualsConsumer(this::assertEqualInstances) - .test(); - } +public class DataStreamMetadataTests extends AbstractChunkedSerializingTestCase { @Override protected DataStreamMetadata createTestInstance() { @@ -61,7 +54,17 @@ protected NamedWriteableRegistry getNamedWriteableRegistry() { } @Override - protected Class categoryClass() { - return DataStreamMetadata.class; + protected DataStreamMetadata doParseInstance(XContentParser parser) throws IOException { + return DataStreamMetadata.fromXContent(parser); + } + + @Override + protected Writeable.Reader instanceReader() { + return DataStreamMetadata::new; + } + + @Override + protected boolean isFragment() { + return true; } } diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/DesiredNodesMetadataSerializationTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/DesiredNodesMetadataSerializationTests.java index d3fc52f05b05..3e79ab0e5132 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/DesiredNodesMetadataSerializationTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/DesiredNodesMetadataSerializationTests.java @@ -11,7 +11,7 @@ import org.elasticsearch.cluster.Diff; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.test.SimpleDiffableSerializationTestCase; +import org.elasticsearch.test.ChunkedToXContentDiffableSerializationTestCase; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; @@ -20,7 +20,7 @@ import static org.elasticsearch.cluster.metadata.DesiredNodesSerializationTests.mutateDesiredNodes; import static org.elasticsearch.cluster.metadata.DesiredNodesTestCase.randomDesiredNodes; -public class DesiredNodesMetadataSerializationTests extends SimpleDiffableSerializationTestCase { +public class DesiredNodesMetadataSerializationTests extends ChunkedToXContentDiffableSerializationTestCase { @Override protected Metadata.Custom makeTestChanges(Metadata.Custom testInstance) { if (randomBoolean()) { @@ -65,4 +65,9 @@ private static DesiredNodesMetadata randomDesiredNodesMetadata() { private DesiredNodesMetadata mutate(DesiredNodesMetadata base) { return new DesiredNodesMetadata(mutateDesiredNodes(base.getLatestDesiredNodes())); } + + @Override + protected boolean isFragment() { + return true; + } } diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexGraveyardTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexGraveyardTests.java index cbf1f6dc4f7a..fa3a779c1a9d 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexGraveyardTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexGraveyardTests.java @@ -11,8 +11,10 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.common.xcontent.XContentElasticsearchExtension; import org.elasticsearch.index.Index; import org.elasticsearch.test.ESTestCase; @@ -57,12 +59,26 @@ public void testXContent() throws IOException { final IndexGraveyard graveyard = createRandom(); final XContentBuilder builder = JsonXContent.contentBuilder(); builder.startObject(); - graveyard.toXContent(builder, ToXContent.EMPTY_PARAMS); + final var iterator = graveyard.toXContentChunked(ToXContent.EMPTY_PARAMS); + int chunks = 0; + while (iterator.hasNext()) { + ++chunks; + iterator.next().toXContent(builder, ToXContent.EMPTY_PARAMS); + } + assertEquals(2 + graveyard.getTombstones().size(), chunks); builder.endObject(); if (graveyard.getTombstones().size() > 0) { // check that date properly printed assertThat( - Strings.toString(graveyard, false, true), + Strings.toString( + ignored -> Iterators.concat( + ChunkedToXContentHelper.startObject(), + graveyard.toXContentChunked(ToXContent.EMPTY_PARAMS), + ChunkedToXContentHelper.endObject() + ), + false, + true + ), containsString( XContentElasticsearchExtension.DEFAULT_FORMATTER.format( Instant.ofEpochMilli(graveyard.getTombstones().get(0).getDeleteDateInMillis()) diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataTests.java index 92b228b17a8f..49fb4000763e 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataTests.java @@ -44,9 +44,11 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -2348,9 +2350,10 @@ private static class CreateIndexResult { } private static class TestCustomMetadata implements Metadata.Custom { + @Override - public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { - return null; + public Iterator toXContentChunked(ToXContent.Params params) { + return Collections.emptyIterator(); } @Override diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/NodesShutdownMetadataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/NodesShutdownMetadataTests.java index 381d64a249ab..88c7a31a89ad 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/NodesShutdownMetadataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/NodesShutdownMetadataTests.java @@ -17,7 +17,7 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.TimeValue; -import org.elasticsearch.test.SimpleDiffableSerializationTestCase; +import org.elasticsearch.test.ChunkedToXContentDiffableSerializationTestCase; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; @@ -35,7 +35,7 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; -public class NodesShutdownMetadataTests extends SimpleDiffableSerializationTestCase { +public class NodesShutdownMetadataTests extends ChunkedToXContentDiffableSerializationTestCase { public void testInsertNewNodeShutdownMetadata() { NodesShutdownMetadata nodesShutdownMetadata = new NodesShutdownMetadata(new HashMap<>()); @@ -142,4 +142,9 @@ protected Metadata.Custom makeTestChanges(Metadata.Custom testInstance) { protected Metadata.Custom mutateInstance(Metadata.Custom instance) throws IOException { return makeTestChanges(instance); } + + @Override + protected boolean isFragment() { + return true; + } } diff --git a/server/src/test/java/org/elasticsearch/ingest/IngestMetadataTests.java b/server/src/test/java/org/elasticsearch/ingest/IngestMetadataTests.java index dd9e81318336..2f5690380eb8 100644 --- a/server/src/test/java/org/elasticsearch/ingest/IngestMetadataTests.java +++ b/server/src/test/java/org/elasticsearch/ingest/IngestMetadataTests.java @@ -12,7 +12,9 @@ import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.Streams; import org.elasticsearch.common.util.Maps; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentBuilder; @@ -23,6 +25,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.Set; import static org.hamcrest.Matchers.aMapWithSize; import static org.hamcrest.Matchers.allOf; @@ -46,7 +49,7 @@ public void testFromXContent() throws IOException { XContentBuilder builder = XContentFactory.contentBuilder(randomFrom(XContentType.values())); builder.prettyPrint(); builder.startObject(); - ingestMetadata.toXContent(builder, ToXContent.EMPTY_PARAMS); + ChunkedToXContent.wrapAsXContentObject(ingestMetadata).toXContent(builder, ToXContent.EMPTY_PARAMS); builder.endObject(); XContentBuilder shuffled = shuffleXContent(builder); try (XContentParser parser = createParser(shuffled)) { @@ -121,4 +124,25 @@ public void testDiff() throws Exception { equalTo(new PipelineConfiguration("2", new BytesArray("{\"key\" : \"value\"}"), XContentType.JSON)) ); } + + public void testChunkedToXContent() throws IOException { + final BytesReference pipelineConfig = new BytesArray("{}"); + final int pipelines = randomInt(10); + final Map pipelineConfigurations = new HashMap<>(); + for (int i = 0; i < pipelines; i++) { + final String id = Integer.toString(i); + pipelineConfigurations.put(id, new PipelineConfiguration(id, pipelineConfig, XContentType.JSON)); + } + int chunksSeen = 0; + try (XContentBuilder builder = new XContentBuilder(randomFrom(XContentType.values()), Streams.NULL_OUTPUT_STREAM, Set.of())) { + final var iterator = new IngestMetadata(pipelineConfigurations).toXContentChunked(ToXContent.EMPTY_PARAMS); + builder.startObject(); + while (iterator.hasNext()) { + iterator.next().toXContent(builder, ToXContent.EMPTY_PARAMS); + chunksSeen++; + } + builder.endObject(); + } + assertEquals(2 + pipelines, chunksSeen); + } } diff --git a/server/src/test/java/org/elasticsearch/persistent/PersistentTasksCustomMetadataTests.java b/server/src/test/java/org/elasticsearch/persistent/PersistentTasksCustomMetadataTests.java index a143150f8ba3..678790e3d49d 100644 --- a/server/src/test/java/org/elasticsearch/persistent/PersistentTasksCustomMetadataTests.java +++ b/server/src/test/java/org/elasticsearch/persistent/PersistentTasksCustomMetadataTests.java @@ -32,7 +32,7 @@ import org.elasticsearch.persistent.TestPersistentTasksPlugin.State; import org.elasticsearch.persistent.TestPersistentTasksPlugin.TestParams; import org.elasticsearch.persistent.TestPersistentTasksPlugin.TestPersistentTasksExecutor; -import org.elasticsearch.test.SimpleDiffableSerializationTestCase; +import org.elasticsearch.test.ChunkedToXContentDiffableSerializationTestCase; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.ToXContent; @@ -58,7 +58,7 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.sameInstance; -public class PersistentTasksCustomMetadataTests extends SimpleDiffableSerializationTestCase { +public class PersistentTasksCustomMetadataTests extends ChunkedToXContentDiffableSerializationTestCase { @Override protected PersistentTasksCustomMetadata createTestInstance() { @@ -174,7 +174,7 @@ public void testSerializationContext() throws Exception { ); XContentType xContentType = randomFrom(XContentType.values()); - BytesReference shuffled = toShuffledXContent(testInstance, xContentType, params, false); + BytesReference shuffled = toShuffledXContent(asXContent(testInstance), xContentType, params, false); PersistentTasksCustomMetadata newInstance; try (XContentParser parser = createParser(XContentFactory.xContent(xContentType), shuffled)) { @@ -396,4 +396,9 @@ private Assignment randomAssignment() { } return new Assignment(randomAlphaOfLength(10), randomAlphaOfLength(10)); } + + @Override + protected boolean isFragment() { + return true; + } } diff --git a/server/src/test/java/org/elasticsearch/script/ScriptMetadataTests.java b/server/src/test/java/org/elasticsearch/script/ScriptMetadataTests.java index 51e30000474c..f9dccf874f83 100644 --- a/server/src/test/java/org/elasticsearch/script/ScriptMetadataTests.java +++ b/server/src/test/java/org/elasticsearch/script/ScriptMetadataTests.java @@ -12,7 +12,7 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.util.Maps; -import org.elasticsearch.test.AbstractXContentSerializingTestCase; +import org.elasticsearch.test.AbstractChunkedSerializingTestCase; import org.elasticsearch.xcontent.DeprecationHandler; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.XContentBuilder; @@ -28,7 +28,7 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.hasKey; -public class ScriptMetadataTests extends AbstractXContentSerializingTestCase { +public class ScriptMetadataTests extends AbstractChunkedSerializingTestCase { public void testGetScript() throws Exception { ScriptMetadata.Builder builder = new ScriptMetadata.Builder(null); @@ -170,4 +170,9 @@ protected ScriptMetadata doParseInstance(XContentParser parser) { throw new UncheckedIOException(ioe); } } + + @Override + protected boolean isFragment() { + return true; + } } diff --git a/server/src/test/java/org/elasticsearch/snapshots/RepositoriesMetadataSerializationTests.java b/server/src/test/java/org/elasticsearch/snapshots/RepositoriesMetadataSerializationTests.java index 91dd9983ca70..0cafaf74ddcb 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/RepositoriesMetadataSerializationTests.java +++ b/server/src/test/java/org/elasticsearch/snapshots/RepositoriesMetadataSerializationTests.java @@ -16,7 +16,7 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.test.SimpleDiffableSerializationTestCase; +import org.elasticsearch.test.ChunkedToXContentDiffableSerializationTestCase; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; @@ -24,7 +24,7 @@ import java.util.Comparator; import java.util.List; -public class RepositoriesMetadataSerializationTests extends SimpleDiffableSerializationTestCase { +public class RepositoriesMetadataSerializationTests extends ChunkedToXContentDiffableSerializationTestCase { @Override protected Custom createTestInstance() { @@ -117,4 +117,8 @@ protected Custom doParseInstance(XContentParser parser) throws IOException { return new RepositoriesMetadata(repos); } + @Override + protected boolean isFragment() { + return true; + } } diff --git a/server/src/test/java/org/elasticsearch/upgrades/FeatureMigrationResultsTests.java b/server/src/test/java/org/elasticsearch/upgrades/FeatureMigrationResultsTests.java index 9d997843f70e..fc9d786945db 100644 --- a/server/src/test/java/org/elasticsearch/upgrades/FeatureMigrationResultsTests.java +++ b/server/src/test/java/org/elasticsearch/upgrades/FeatureMigrationResultsTests.java @@ -12,12 +12,12 @@ import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.core.Tuple; -import org.elasticsearch.test.SimpleDiffableSerializationTestCase; +import org.elasticsearch.test.ChunkedToXContentDiffableSerializationTestCase; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; -public class FeatureMigrationResultsTests extends SimpleDiffableSerializationTestCase { +public class FeatureMigrationResultsTests extends ChunkedToXContentDiffableSerializationTestCase { @Override protected FeatureMigrationResults createTestInstance() { @@ -72,4 +72,9 @@ protected Metadata.Custom makeTestChanges(Metadata.Custom testInstance) { protected Writeable.Reader> diffReader() { return FeatureMigrationResults.ResultsDiff::new; } + + @Override + protected boolean isFragment() { + return true; + } } diff --git a/test/framework/src/main/java/org/elasticsearch/test/AbstractChunkedSerializingTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/AbstractChunkedSerializingTestCase.java index b8182e3c7ba5..39f8ba0eab12 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/AbstractChunkedSerializingTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractChunkedSerializingTestCase.java @@ -11,6 +11,7 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentType; @@ -24,14 +25,22 @@ public abstract class AbstractChunkedSerializingTestCase createXContentTester() { return chunkedXContentTester( this::createParser, - xContentType -> createTestInstance(), + this::createXContextTestInstance, getToXContentParams(), - this::doParseInstance + this::doParseInstance, + isFragment() ); } @Override protected ToXContent asXContent(T instance) { + if (isFragment()) { + return (ToXContentObject) ((builder, params) -> { + builder.startObject(); + ChunkedToXContent.wrapAsXContentObject(instance).toXContent(builder, params); + return builder.endObject(); + }); + } return ChunkedToXContent.wrapAsXContentObject(instance); } @@ -44,4 +53,8 @@ protected T createXContextTestInstance(XContentType xContentType) { * Parses to a new instance using the provided {@link XContentParser} */ protected abstract T doParseInstance(XContentParser parser) throws IOException; + + protected boolean isFragment() { + return false; + } } diff --git a/test/framework/src/main/java/org/elasticsearch/test/AbstractXContentTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/AbstractXContentTestCase.java index 28010f5b2f5b..00406418f9af 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/AbstractXContentTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractXContentTestCase.java @@ -87,14 +87,21 @@ public static XContentTester chunkedXContentTes CheckedBiFunction createParser, Function instanceSupplier, ToXContent.Params toXContentParams, - CheckedFunction fromXContent + CheckedFunction fromXContent, + boolean fragment ) { return new XContentTester<>(createParser, instanceSupplier, (testInstance, xContentType) -> { try (XContentBuilder builder = XContentBuilder.builder(xContentType.xContent())) { var serialization = testInstance.toXContentChunked(toXContentParams); + if (fragment) { + builder.startObject(); + } while (serialization.hasNext()) { serialization.next().toXContent(builder, toXContentParams); } + if (fragment) { + builder.endObject(); + } return BytesReference.bytes(builder); } }, fromXContent); diff --git a/test/framework/src/main/java/org/elasticsearch/test/ChunkedToXContentDiffableSerializationTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ChunkedToXContentDiffableSerializationTestCase.java new file mode 100644 index 000000000000..d1724ad831bd --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/test/ChunkedToXContentDiffableSerializationTestCase.java @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.test; + +import org.elasticsearch.cluster.Diff; +import org.elasticsearch.cluster.Diffable; +import org.elasticsearch.common.io.stream.Writeable.Reader; +import org.elasticsearch.common.xcontent.ChunkedToXContent; + +import java.io.IOException; + +/** + * An abstract test case to ensure correct behavior of Diffable. + * + * This class can be used as a based class for tests of {@link org.elasticsearch.cluster.ClusterState.Custom} classes and other classes + * that support, Writable serialization, chunked XContent-based serialization and are diffable. + */ +public abstract class ChunkedToXContentDiffableSerializationTestCase & ChunkedToXContent> extends + AbstractChunkedSerializingTestCase { + + /** + * Introduces random changes into the test object + */ + protected abstract T makeTestChanges(T testInstance); + + protected abstract Reader> diffReader(); + + public final void testDiffableSerialization() throws IOException { + DiffableTestUtils.testDiffableSerialization( + this::createTestInstance, + this::makeTestChanges, + getNamedWriteableRegistry(), + instanceReader(), + diffReader() + ); + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/test/TestCustomMetadata.java b/test/framework/src/main/java/org/elasticsearch/test/TestCustomMetadata.java index e617a3ffa40e..5a63bc135393 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/TestCustomMetadata.java +++ b/test/framework/src/main/java/org/elasticsearch/test/TestCustomMetadata.java @@ -12,12 +12,14 @@ import org.elasticsearch.cluster.AbstractNamedDiffable; import org.elasticsearch.cluster.NamedDiff; import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; +import java.util.Iterator; import java.util.function.Function; public abstract class TestCustomMetadata extends AbstractNamedDiffable implements Metadata.Custom { @@ -91,9 +93,8 @@ public static T fromXContent(Function sup } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.field("data", getData()); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return Iterators.single((builder, params) -> builder.field("data", getData())); } @Override diff --git a/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/AutoscalingMetadata.java b/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/AutoscalingMetadata.java index 1f83a57985c7..03703df878e7 100644 --- a/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/AutoscalingMetadata.java +++ b/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/AutoscalingMetadata.java @@ -15,15 +15,17 @@ import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.autoscaling.policy.AutoscalingPolicyMetadata; import java.io.IOException; import java.util.Collections; import java.util.EnumSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -115,9 +117,8 @@ public Version getMinimalSupportedVersion() { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.field(POLICIES_FIELD.getPreferredName(), policies); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return ChunkedToXContentHelper.xContentValuesMap(POLICIES_FIELD.getPreferredName(), policies); } @Override diff --git a/x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingMetadataDiffableSerializationTests.java b/x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingMetadataDiffableSerializationTests.java index 75ed5bf32e33..024a049f8ca3 100644 --- a/x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingMetadataDiffableSerializationTests.java +++ b/x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingMetadataDiffableSerializationTests.java @@ -11,13 +11,12 @@ import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.test.SimpleDiffableSerializationTestCase; +import org.elasticsearch.test.ChunkedToXContentDiffableSerializationTestCase; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.autoscaling.policy.AutoscalingPolicy; import org.elasticsearch.xpack.autoscaling.policy.AutoscalingPolicyMetadata; -import java.io.IOException; import java.util.SortedMap; import java.util.TreeMap; @@ -25,7 +24,7 @@ import static org.elasticsearch.xpack.autoscaling.AutoscalingTestCase.randomAutoscalingMetadata; import static org.elasticsearch.xpack.autoscaling.AutoscalingTestCase.randomAutoscalingPolicy; -public class AutoscalingMetadataDiffableSerializationTests extends SimpleDiffableSerializationTestCase { +public class AutoscalingMetadataDiffableSerializationTests extends ChunkedToXContentDiffableSerializationTestCase { @Override protected NamedWriteableRegistry getNamedWriteableRegistry() { @@ -38,7 +37,7 @@ protected NamedXContentRegistry xContentRegistry() { } @Override - protected AutoscalingMetadata doParseInstance(final XContentParser parser) throws IOException { + protected AutoscalingMetadata doParseInstance(final XContentParser parser) { return AutoscalingMetadata.parse(parser); } @@ -79,4 +78,8 @@ protected Writeable.Reader> diffReader() { return AutoscalingMetadata.AutoscalingMetadataDiff::new; } + @Override + protected boolean isFragment() { + return true; + } } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/AutoFollowMetadataTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/AutoFollowMetadataTests.java index 58d0f7a4b6a3..9f2b6050fc02 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/AutoFollowMetadataTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/AutoFollowMetadataTests.java @@ -12,7 +12,7 @@ import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.util.Maps; import org.elasticsearch.core.TimeValue; -import org.elasticsearch.test.AbstractXContentSerializingTestCase; +import org.elasticsearch.test.AbstractChunkedSerializingTestCase; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.ccr.AutoFollowMetadata; @@ -23,7 +23,7 @@ import java.util.Map; import java.util.function.Predicate; -public class AutoFollowMetadataTests extends AbstractXContentSerializingTestCase { +public class AutoFollowMetadataTests extends AbstractChunkedSerializingTestCase { @Override protected Predicate getRandomFieldsExcludeFilter() { @@ -80,4 +80,9 @@ protected AutoFollowMetadata createTestInstance() { protected Writeable.Reader instanceReader() { return AutoFollowMetadata::new; } + + @Override + protected boolean isFragment() { + return true; + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensesMetadata.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensesMetadata.java index 195d97e9b486..612f100e59a5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensesMetadata.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensesMetadata.java @@ -10,14 +10,16 @@ import org.elasticsearch.cluster.AbstractNamedDiffable; import org.elasticsearch.cluster.NamedDiff; import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.core.Nullable; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; import java.util.EnumSet; +import java.util.Iterator; import java.util.Objects; /** @@ -138,18 +140,20 @@ public static LicensesMetadata fromXContent(XContentParser parser) throws IOExce } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - if (license == LICENSE_TOMBSTONE) { - builder.nullField(Fields.LICENSE); - } else { - builder.startObject(Fields.LICENSE); - license.toInnerXContent(builder, params); - builder.endObject(); - } - if (trialVersion != null) { - builder.field(Fields.TRIAL_LICENSE, trialVersion.toString()); - } - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return Iterators.single(((builder, params) -> { + if (license == LICENSE_TOMBSTONE) { + builder.nullField(Fields.LICENSE); + } else { + builder.startObject(Fields.LICENSE); + license.toInnerXContent(builder, params); + builder.endObject(); + } + if (trialVersion != null) { + builder.field(Fields.TRIAL_LICENSE, trialVersion.toString()); + } + return builder; + })); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadata.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadata.java index d74be00ef18f..6b28b9ced800 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadata.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadata.java @@ -11,14 +11,17 @@ import org.elasticsearch.cluster.AbstractNamedDiffable; import org.elasticsearch.cluster.metadata.IndexAbstraction; import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.core.TimeValue; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.ToXContentFragment; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; @@ -28,6 +31,7 @@ import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -147,31 +151,12 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(PATTERNS_FIELD.getPreferredName()); - for (Map.Entry entry : patterns.entrySet()) { - builder.startObject(entry.getKey()); - builder.value(entry.getValue()); - builder.endObject(); - } - builder.endObject(); - - builder.startObject(FOLLOWED_LEADER_INDICES_FIELD.getPreferredName()); - for (Map.Entry> entry : followedLeaderIndexUUIDs.entrySet()) { - builder.field(entry.getKey(), entry.getValue()); - } - builder.endObject(); - builder.startObject(HEADERS.getPreferredName()); - for (Map.Entry> entry : headers.entrySet()) { - builder.field(entry.getKey(), entry.getValue()); - } - builder.endObject(); - return builder; - } - - @Override - public boolean isFragment() { - return true; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return Iterators.concat( + ChunkedToXContentHelper.xContentFragmentValuesMap(PATTERNS_FIELD.getPreferredName(), patterns), + ChunkedToXContentHelper.map(FOLLOWED_LEADER_INDICES_FIELD.getPreferredName(), followedLeaderIndexUUIDs), + ChunkedToXContentHelper.map(HEADERS.getPreferredName(), headers) + ); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleMetadata.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleMetadata.java index dab9b1f420e0..449998e4307f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleMetadata.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleMetadata.java @@ -14,15 +14,18 @@ import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.metadata.Metadata.Custom; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import java.io.IOException; import java.util.Collections; import java.util.EnumSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -100,10 +103,11 @@ public Diff diff(Custom previousState) { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.xContentValuesMap(POLICIES_FIELD.getPreferredName(), policyMetadatas); - builder.field(OPERATION_MODE_FIELD.getPreferredName(), operationMode); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return Iterators.concat( + ChunkedToXContentHelper.xContentValuesMap(POLICIES_FIELD.getPreferredName(), policyMetadatas), + Iterators.single((builder, params) -> builder.field(OPERATION_MODE_FIELD.getPreferredName(), operationMode)) + ); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MlMetadata.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MlMetadata.java index 64645a64e4bc..e43bf88c2887 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MlMetadata.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MlMetadata.java @@ -14,19 +14,21 @@ import org.elasticsearch.cluster.SimpleDiffable; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.core.Nullable; import org.elasticsearch.xcontent.ObjectParser; import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig; import org.elasticsearch.xpack.core.ml.job.config.Job; import java.io.IOException; import java.util.Collections; import java.util.EnumSet; +import java.util.Iterator; import java.util.Map; import java.util.Objects; import java.util.SortedMap; @@ -118,10 +120,11 @@ private static void writeMap(Map map, StreamOut } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.field(UPGRADE_MODE.getPreferredName(), upgradeMode); - builder.field(RESET_MODE.getPreferredName(), resetMode); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return Iterators.single( + ((builder, params) -> builder.field(UPGRADE_MODE.getPreferredName(), upgradeMode) + .field(RESET_MODE.getPreferredName(), resetMode)) + ); } public static class MlMetadataDiff implements NamedDiff { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SnapshotLifecycleMetadata.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SnapshotLifecycleMetadata.java index 9fcd82cba7df..0398382f4d8a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SnapshotLifecycleMetadata.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SnapshotLifecycleMetadata.java @@ -14,17 +14,20 @@ import org.elasticsearch.cluster.SimpleDiffable; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xpack.core.ilm.OperationMode; import java.io.IOException; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -132,11 +135,15 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.field(POLICIES_FIELD.getPreferredName(), this.snapshotConfigurations); - builder.field(OPERATION_MODE_FIELD.getPreferredName(), operationMode); - builder.field(STATS_FIELD.getPreferredName(), this.slmStats); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return Iterators.concat( + ChunkedToXContentHelper.xContentValuesMap(POLICIES_FIELD.getPreferredName(), this.snapshotConfigurations), + Iterators.single((builder, params) -> { + builder.field(OPERATION_MODE_FIELD.getPreferredName(), operationMode); + builder.field(STATS_FIELD.getPreferredName(), this.slmStats); + return builder; + }) + ); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/TransformMetadata.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/TransformMetadata.java index 5a3df91cd25a..e5cac570a2ac 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/TransformMetadata.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/TransformMetadata.java @@ -15,14 +15,15 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.core.Nullable; import org.elasticsearch.xcontent.ObjectParser; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.ToXContent; -import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; import java.util.EnumSet; +import java.util.Iterator; import java.util.Objects; public class TransformMetadata implements Metadata.Custom { @@ -81,9 +82,8 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { - builder.field(RESET_MODE.getPreferredName(), resetMode); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return ChunkedToXContentHelper.field(RESET_MODE.getPreferredName(), resetMode); } public static class TransformMetadataDiff implements NamedDiff { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/WatcherMetadata.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/WatcherMetadata.java index ea3e3b4df25b..b0368f5fdd80 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/WatcherMetadata.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/WatcherMetadata.java @@ -12,12 +12,14 @@ import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; import java.util.EnumSet; +import java.util.Iterator; import java.util.Objects; public class WatcherMetadata extends AbstractNamedDiffable implements Metadata.Custom { @@ -105,9 +107,8 @@ public static Metadata.Custom fromXContent(XContentParser parser) throws IOExcep } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.field(Field.MANUALLY_STOPPED.getPreferredName(), manuallyStopped); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return ChunkedToXContentHelper.field(Field.MANUALLY_STOPPED.getPreferredName(), manuallyStopped); } interface Field { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/transport/actions/stats/WatcherStatsResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/transport/actions/stats/WatcherStatsResponse.java index 7a12b5803b1d..e9a756071fc3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/transport/actions/stats/WatcherStatsResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/transport/actions/stats/WatcherStatsResponse.java @@ -13,6 +13,7 @@ import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.core.Nullable; import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; @@ -28,7 +29,7 @@ public class WatcherStatsResponse extends BaseNodesResponse implements ToXContentObject { - private WatcherMetadata watcherMetadata; + private final WatcherMetadata watcherMetadata; public WatcherStatsResponse(StreamInput in) throws IOException { super(in); @@ -63,7 +64,7 @@ protected void writeNodesTo(StreamOutput out, List nodes) throws IOExcepti @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - watcherMetadata.toXContent(builder, params); + ChunkedToXContent.wrapAsXContentObject(watcherMetadata).toXContent(builder, params); builder.startArray("stats"); for (Node node : getNodes()) { node.toXContent(builder, params); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesMetadataSerializationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesMetadataSerializationTests.java index bb0aa97a4895..49cc784e0d5e 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesMetadataSerializationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesMetadataSerializationTests.java @@ -12,6 +12,7 @@ import org.elasticsearch.cluster.metadata.RepositoriesMetadata; import org.elasticsearch.cluster.metadata.RepositoryMetadata; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.core.TimeValue; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xcontent.NamedXContentRegistry; @@ -38,7 +39,7 @@ public void testXContentSerializationOneSignedLicense() throws Exception { XContentBuilder builder = XContentFactory.jsonBuilder(); builder.startObject(); builder.startObject("licenses"); - licensesMetadata.toXContent(builder, ToXContent.EMPTY_PARAMS); + ChunkedToXContent.wrapAsXContentObject(licensesMetadata).toXContent(builder, ToXContent.EMPTY_PARAMS); builder.endObject(); builder.endObject(); LicensesMetadata licensesMetadataFromXContent = getLicensesMetadataFromXContent(createParser(builder)); @@ -52,7 +53,7 @@ public void testXContentSerializationOneSignedLicenseWithUsedTrial() throws Exce XContentBuilder builder = XContentFactory.jsonBuilder(); builder.startObject(); builder.startObject("licenses"); - licensesMetadata.toXContent(builder, ToXContent.EMPTY_PARAMS); + ChunkedToXContent.wrapAsXContentObject(licensesMetadata).toXContent(builder, ToXContent.EMPTY_PARAMS); builder.endObject(); builder.endObject(); LicensesMetadata licensesMetadataFromXContent = getLicensesMetadataFromXContent(createParser(builder)); @@ -101,7 +102,7 @@ public void testXContentSerializationOneTrial() throws Exception { XContentBuilder builder = XContentFactory.jsonBuilder(); builder.startObject(); builder.startObject("licenses"); - licensesMetadata.toXContent(builder, ToXContent.EMPTY_PARAMS); + ChunkedToXContent.wrapAsXContentObject(licensesMetadata).toXContent(builder, ToXContent.EMPTY_PARAMS); builder.endObject(); builder.endObject(); LicensesMetadata licensesMetadataFromXContent = getLicensesMetadataFromXContent(createParser(builder)); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/slm/SnapshotLifecycleMetadataTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/slm/SnapshotLifecycleMetadataTests.java index 2f62452b7b8d..290ceac45bdd 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/slm/SnapshotLifecycleMetadataTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/slm/SnapshotLifecycleMetadataTests.java @@ -9,14 +9,14 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.util.Maps; -import org.elasticsearch.test.AbstractXContentSerializingTestCase; +import org.elasticsearch.test.AbstractChunkedSerializingTestCase; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.ilm.OperationMode; import java.io.IOException; import java.util.Map; -public class SnapshotLifecycleMetadataTests extends AbstractXContentSerializingTestCase { +public class SnapshotLifecycleMetadataTests extends AbstractChunkedSerializingTestCase { @Override protected SnapshotLifecycleMetadata doParseInstance(XContentParser parser) throws IOException { return SnapshotLifecycleMetadata.PARSER.apply(parser, null); @@ -41,4 +41,9 @@ protected SnapshotLifecycleMetadata createTestInstance() { protected Writeable.Reader instanceReader() { return SnapshotLifecycleMetadata::new; } + + @Override + protected boolean isFragment() { + return true; + } } diff --git a/x-pack/plugin/enrich/src/main/java/org/elasticsearch/xpack/enrich/EnrichMetadata.java b/x-pack/plugin/enrich/src/main/java/org/elasticsearch/xpack/enrich/EnrichMetadata.java index 83b4aff08879..e4d87f0920a3 100644 --- a/x-pack/plugin/enrich/src/main/java/org/elasticsearch/xpack/enrich/EnrichMetadata.java +++ b/x-pack/plugin/enrich/src/main/java/org/elasticsearch/xpack/enrich/EnrichMetadata.java @@ -12,9 +12,10 @@ import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.enrich.EnrichPolicy; @@ -22,6 +23,7 @@ import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.Objects; @@ -96,15 +98,8 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(POLICIES.getPreferredName()); - for (Map.Entry entry : policies.entrySet()) { - builder.startObject(entry.getKey()); - builder.value(entry.getValue()); - builder.endObject(); - } - builder.endObject(); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return ChunkedToXContentHelper.xContentFragmentValuesMap(POLICIES.getPreferredName(), policies); } @Override diff --git a/x-pack/plugin/enrich/src/test/java/org/elasticsearch/xpack/enrich/EnrichMetadataTests.java b/x-pack/plugin/enrich/src/test/java/org/elasticsearch/xpack/enrich/EnrichMetadataTests.java index 6057255bd456..1c40fa6bfdef 100644 --- a/x-pack/plugin/enrich/src/test/java/org/elasticsearch/xpack/enrich/EnrichMetadataTests.java +++ b/x-pack/plugin/enrich/src/test/java/org/elasticsearch/xpack/enrich/EnrichMetadataTests.java @@ -8,7 +8,7 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.util.Maps; -import org.elasticsearch.test.AbstractXContentSerializingTestCase; +import org.elasticsearch.test.AbstractChunkedSerializingTestCase; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.core.enrich.EnrichPolicy; @@ -19,7 +19,7 @@ import static org.elasticsearch.xpack.enrich.EnrichPolicyTests.randomEnrichPolicy; import static org.hamcrest.Matchers.equalTo; -public class EnrichMetadataTests extends AbstractXContentSerializingTestCase { +public class EnrichMetadataTests extends AbstractChunkedSerializingTestCase { @Override protected EnrichMetadata doParseInstance(XContentParser parser) throws IOException { @@ -61,4 +61,9 @@ protected void assertEqualInstances(EnrichMetadata expectedInstance, EnrichMetad EnrichPolicyTests.assertEqualPolicies(expected, actual); } } + + @Override + protected boolean isFragment() { + return true; + } } diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleMetadataTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleMetadataTests.java index 9c4c0ec2c860..5e510320a1ec 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleMetadataTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleMetadataTests.java @@ -15,7 +15,7 @@ import org.elasticsearch.common.io.stream.Writeable.Reader; import org.elasticsearch.common.util.Maps; import org.elasticsearch.core.TimeValue; -import org.elasticsearch.test.SimpleDiffableSerializationTestCase; +import org.elasticsearch.test.ChunkedToXContentDiffableSerializationTestCase; import org.elasticsearch.test.VersionUtils; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.ParseField; @@ -55,7 +55,7 @@ import static org.elasticsearch.xpack.ilm.LifecyclePolicyTestsUtils.newTestLifecyclePolicy; import static org.elasticsearch.xpack.ilm.LifecyclePolicyTestsUtils.randomTimeseriesLifecyclePolicy; -public class IndexLifecycleMetadataTests extends SimpleDiffableSerializationTestCase { +public class IndexLifecycleMetadataTests extends ChunkedToXContentDiffableSerializationTestCase { @Override protected IndexLifecycleMetadata createTestInstance() { @@ -212,4 +212,9 @@ public static IndexLifecycleMetadata createTestInstance(int numPolicies, Operati } return new IndexLifecycleMetadata(policies, mode); } + + @Override + protected boolean isFragment() { + return true; + } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ModelAliasMetadata.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ModelAliasMetadata.java index daa6a89b6434..446f7d4b66d1 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ModelAliasMetadata.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/ModelAliasMetadata.java @@ -16,8 +16,10 @@ import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; @@ -26,6 +28,7 @@ import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.Objects; @@ -88,13 +91,8 @@ public Map modelAliases() { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(MODEL_ALIASES.getPreferredName()); - for (Map.Entry modelAliasEntry : modelAliases.entrySet()) { - builder.field(modelAliasEntry.getKey(), modelAliasEntry.getValue()); - } - builder.endObject(); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return ChunkedToXContentHelper.xContentValuesMap(MODEL_ALIASES.getPreferredName(), modelAliases); } @Override diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/assignment/TrainedModelAssignmentMetadata.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/assignment/TrainedModelAssignmentMetadata.java index 13085e43499c..f3d311719fc1 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/assignment/TrainedModelAssignmentMetadata.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/assignment/TrainedModelAssignmentMetadata.java @@ -19,7 +19,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.ml.inference.assignment.TrainedModelAssignment; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; @@ -27,6 +27,7 @@ import java.io.IOException; import java.util.Collections; import java.util.EnumSet; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; @@ -107,9 +108,11 @@ public Map modelAssignments() { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.mapContents(modelRoutingEntries); - return builder; + public Iterator toXContentChunked(ToXContent.Params ignored) { + return modelRoutingEntries.entrySet() + .stream() + .map(entry -> (ToXContent) (builder, params) -> entry.getValue().toXContent(builder.field(entry.getKey()), params)) + .iterator(); } @Override diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/MlMetadataTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/MlMetadataTests.java index d9b749628292..09f1afb020e2 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/MlMetadataTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/MlMetadataTests.java @@ -10,7 +10,7 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.search.SearchModule; -import org.elasticsearch.test.AbstractXContentSerializingTestCase; +import org.elasticsearch.test.AbstractChunkedSerializingTestCase; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.ml.MlMetadata; @@ -19,7 +19,7 @@ import static org.hamcrest.Matchers.equalTo; -public class MlMetadataTests extends AbstractXContentSerializingTestCase { +public class MlMetadataTests extends AbstractChunkedSerializingTestCase { @Override protected MlMetadata createTestInstance() { @@ -71,4 +71,9 @@ protected MlMetadata mutateInstance(MlMetadata instance) { return metadataBuilder.build(); } + + @Override + protected boolean isFragment() { + return true; + } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/assignment/TrainedModelAssignmentMetadataTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/assignment/TrainedModelAssignmentMetadataTests.java index d2a5f89350ac..3056ce316620 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/assignment/TrainedModelAssignmentMetadataTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/assignment/TrainedModelAssignmentMetadataTests.java @@ -9,7 +9,7 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.unit.ByteSizeValue; -import org.elasticsearch.test.AbstractXContentSerializingTestCase; +import org.elasticsearch.test.AbstractChunkedSerializingTestCase; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.ml.action.StartTrainedModelDeploymentAction; import org.elasticsearch.xpack.core.ml.inference.assignment.Priority; @@ -25,7 +25,7 @@ import static org.hamcrest.Matchers.is; -public class TrainedModelAssignmentMetadataTests extends AbstractXContentSerializingTestCase { +public class TrainedModelAssignmentMetadataTests extends AbstractChunkedSerializingTestCase { public static TrainedModelAssignmentMetadata randomInstance() { LinkedHashMap map = Stream.generate(() -> randomAlphaOfLength(10)) @@ -72,4 +72,8 @@ private static StartTrainedModelDeploymentAction.TaskParams randomParams(String ); } + @Override + protected boolean isFragment() { + return true; + } } diff --git a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/TransformMetadataTests.java b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/TransformMetadataTests.java index e2bc0c74dcdb..8b316ad32ed4 100644 --- a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/TransformMetadataTests.java +++ b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/TransformMetadataTests.java @@ -8,11 +8,11 @@ package org.elasticsearch.xpack.transform; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.test.AbstractXContentSerializingTestCase; +import org.elasticsearch.test.AbstractChunkedSerializingTestCase; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.transform.TransformMetadata; -public class TransformMetadataTests extends AbstractXContentSerializingTestCase { +public class TransformMetadataTests extends AbstractChunkedSerializingTestCase { @Override protected TransformMetadata createTestInstance() { @@ -33,4 +33,9 @@ protected TransformMetadata doParseInstance(XContentParser parser) { protected TransformMetadata mutateInstance(TransformMetadata instance) { return new TransformMetadata.Builder().isResetMode(instance.isResetMode() == false).build(); } + + @Override + protected boolean isFragment() { + return true; + } } diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherMetadataSerializationTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherMetadataSerializationTests.java index 3be81d688335..9aec0fe5c9a5 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherMetadataSerializationTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherMetadataSerializationTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.cluster.metadata.RepositoriesMetadata; import org.elasticsearch.cluster.metadata.RepositoryMetadata; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.ToXContent; @@ -35,7 +36,7 @@ public void testXContentSerializationOneSignedWatcher() throws Exception { XContentBuilder builder = XContentFactory.jsonBuilder(); builder.startObject(); builder.startObject("watcher"); - watcherMetadata.toXContent(builder, ToXContent.EMPTY_PARAMS); + ChunkedToXContent.wrapAsXContentObject(watcherMetadata).toXContent(builder, ToXContent.EMPTY_PARAMS); builder.endObject(); builder.endObject(); WatcherMetadata watchersMetadataFromXContent = getWatcherMetadataFromXContent(createParser(builder)); From a7a016f34d50e00e64ebacebed8536d059966e00 Mon Sep 17 00:00:00 2001 From: Pooya Salehi Date: Tue, 6 Dec 2022 13:42:33 +0100 Subject: [PATCH 164/919] [CI] Fix failing PrevalidateShardPathIT#testCheckShards (#92112) The test fails sometimes since the removal of the shards after relocation does not always succeed. This change enforces some cluster state update to allow retrying the shard directory removal and avoids the WindowsFS in the test since it has known issues that cause test failures. Closes #92056 --- .../cluster/PrevalidateShardPathIT.java | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/cluster/PrevalidateShardPathIT.java b/server/src/internalClusterTest/java/org/elasticsearch/cluster/PrevalidateShardPathIT.java index 9b920d31c1a6..66bcf3ab3242 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/cluster/PrevalidateShardPathIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/cluster/PrevalidateShardPathIT.java @@ -8,6 +8,7 @@ package org.elasticsearch.cluster; +import org.apache.lucene.tests.util.LuceneTestCase; import org.elasticsearch.action.admin.cluster.node.shutdown.NodePrevalidateShardPathResponse; import org.elasticsearch.action.admin.cluster.node.shutdown.PrevalidateShardPathRequest; import org.elasticsearch.action.admin.cluster.node.shutdown.PrevalidateShardPathResponse; @@ -25,6 +26,12 @@ import static org.hamcrest.Matchers.equalTo; +/* + * We rely on the shard directory being deleted after the relocation. This removal sometimes fails + * with "java.io.IOException: access denied" when using WindowsFS which seems to be a known issue. + * See {@link FileSystemUtilsTests}. + */ +@LuceneTestCase.SuppressFileSystems(value = "WindowsFS") @ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 0) public class PrevalidateShardPathIT extends ESIntegTestCase { @@ -63,12 +70,25 @@ public void testCheckShards() throws Exception { updateIndexSettings(indexName, Settings.builder().put("index.routing.allocation.exclude._name", node2)); ensureGreen(indexName); assertBusy(() -> { - // The excluded node should eventually delete the shards - PrevalidateShardPathRequest req2 = new PrevalidateShardPathRequest(shardIdsToCheck, node2Id); - PrevalidateShardPathResponse resp2 = client().execute(TransportPrevalidateShardPathAction.TYPE, req2).get(); - assertThat(resp2.getNodes().size(), equalTo(1)); - assertTrue(resp.failures().isEmpty()); - assertTrue(resp2.getNodes().get(0).getShardIds().isEmpty()); + try { + // The excluded node should eventually delete the shards + PrevalidateShardPathRequest req2 = new PrevalidateShardPathRequest(shardIdsToCheck, node2Id); + PrevalidateShardPathResponse resp2 = client().execute(TransportPrevalidateShardPathAction.TYPE, req2).get(); + assertThat(resp2.getNodes().size(), equalTo(1)); + assertThat(resp2.getNodes().get(0).getNode().getId(), equalTo(node2Id)); + assertTrue("There should be no failures in the response", resp.failures().isEmpty()); + assertTrue("The relocation source node should have removed the shard(s)", resp2.getNodes().get(0).getShardIds().isEmpty()); + } catch (AssertionError e) { + // Removal of shards which are no longer allocated to the node is attempted on every cluster state change in IndicesStore. + // If for whatever reason the removal is not triggered (e.g. not enough nodes reported that the shards are active) or it + // temporarily failed to clean up the shard folder, we need to trigger another cluster state change for this removal to + // finally succeed. + updateIndexSettings( + indexName, + Settings.builder().put("index.routing.allocation.exclude.name", "non-existent" + randomAlphaOfLength(5)) + ); + throw e; + } }); } } From e864173caffac28ee2a5d0fbbfd301826a50036e Mon Sep 17 00:00:00 2001 From: satyam kale <94701487+satyamkale27@users.noreply.github.com> Date: Tue, 6 Dec 2022 18:26:50 +0530 Subject: [PATCH 165/919] fixed link in apache RequestConfig.Builder on documentation (#92073) * fixed link in apache RequestConfig.Builder on documentation * HttpAsyncClientBuilder link fixed --- docs/java-rest/low-level/usage.asciidoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/java-rest/low-level/usage.asciidoc b/docs/java-rest/low-level/usage.asciidoc index 7638fe2b5178..342388f1370c 100644 --- a/docs/java-rest/low-level/usage.asciidoc +++ b/docs/java-rest/low-level/usage.asciidoc @@ -203,7 +203,7 @@ include-tagged::{doc-tests}/RestClientDocumentation.java[rest-client-init-reques -------------------------------------------------- <1> Set a callback that allows to modify the default request configuration (e.g. request timeouts, authentication, or anything that the -https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/client/config/RequestConfig.Builder.html[`org.apache.http.client.config.RequestConfig.Builder`] +https://hc.apache.org/httpcomponents-client-5.2.x/current/httpclient5/apidocs/org/apache/hc/client5/http/config/RequestConfig.Builder.html[`org.apache.hc.client5.http.config.RequestConfig.Builder`] allows to set) ["source","java",subs="attributes,callouts,macros"] @@ -212,7 +212,7 @@ include-tagged::{doc-tests}/RestClientDocumentation.java[rest-client-init-client -------------------------------------------------- <1> Set a callback that allows to modify the http client configuration (e.g. encrypted communication over ssl, or anything that the -https://hc.apache.org/httpcomponents-asyncclient-dev/httpasyncclient/apidocs/org/apache/http/impl/nio/client/HttpAsyncClientBuilder.html[`org.apache.http.impl.nio.client.HttpAsyncClientBuilder`] +https://hc.apache.org/httpcomponents-asyncclient-4.1.x/current/httpasyncclient/apidocs/org/apache/http/impl/nio/client/HttpAsyncClientBuilder.html[`org.apache.http.impl.nio.client.HttpAsyncClientBuilder`] allows to set) @@ -339,9 +339,9 @@ translate to the execution of that request being cancelled, which needs to be specifically implemented in the API itself. The use of the `Cancellable` instance is optional and you can safely ignore this -if you don't need it. A typical usecase for this would be using this together with +if you don't need it. A typical usecase for this would be using this together with frameworks like Rx Java or the Kotlin's `suspendCancellableCoRoutine`. Cancelling -no longer needed requests is a good way to avoid putting unnecessary +no longer needed requests is a good way to avoid putting unnecessary load on Elasticsearch. ["source","java",subs="attributes,callouts,macros"] From b8122f3c6b4b211507465ba9c19d90035c649c78 Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Tue, 6 Dec 2022 13:57:29 +0100 Subject: [PATCH 166/919] Make desired balance iteration count logging more intelligent (#91643) --- .../elasticsearch/cluster/ClusterModule.java | 2 + .../allocator/DesiredBalanceComputer.java | 93 ++++++-- .../DesiredBalanceShardsAllocator.java | 17 +- .../common/settings/ClusterSettings.java | 2 + .../ClusterAllocationSimulationTests.java | 2 + .../DesiredBalanceComputerTests.java | 216 ++++++++++++++---- .../DesiredBalanceShardsAllocatorTests.java | 21 +- .../cluster/ESAllocationTestCase.java | 9 +- 8 files changed, 293 insertions(+), 69 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/ClusterModule.java b/server/src/main/java/org/elasticsearch/cluster/ClusterModule.java index b8c15dae0be2..ba4e02dfd002 100644 --- a/server/src/main/java/org/elasticsearch/cluster/ClusterModule.java +++ b/server/src/main/java/org/elasticsearch/cluster/ClusterModule.java @@ -353,6 +353,8 @@ private static ShardsAllocator createShardsAllocator( allocators.put( DESIRED_BALANCE_ALLOCATOR, () -> new DesiredBalanceShardsAllocator( + settings, + clusterSettings, new BalancedShardsAllocator(settings, clusterSettings, writeLoadForecaster), threadPool, clusterService, diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceComputer.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceComputer.java index 97a39321fba3..b73efc431aa3 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceComputer.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceComputer.java @@ -16,10 +16,16 @@ import org.elasticsearch.cluster.routing.RoutingNodes; import org.elasticsearch.cluster.routing.ShardRouting; import org.elasticsearch.cluster.routing.UnassignedInfo; +import org.elasticsearch.cluster.routing.allocation.RoutingAllocation; import org.elasticsearch.cluster.routing.allocation.command.MoveAllocationCommand; import org.elasticsearch.common.metrics.MeanMetric; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.Strings; +import org.elasticsearch.core.TimeValue; import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.threadpool.ThreadPool; import java.util.ArrayList; import java.util.HashMap; @@ -31,6 +37,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; +import java.util.function.Consumer; import java.util.function.Predicate; import static java.util.stream.Collectors.toSet; @@ -43,12 +50,35 @@ public class DesiredBalanceComputer { private static final Logger logger = LogManager.getLogger(DesiredBalanceComputer.class); + private final ThreadPool threadPool; private final ShardsAllocator delegateAllocator; protected final MeanMetric iterations = new MeanMetric(); - public DesiredBalanceComputer(ShardsAllocator delegateAllocator) { + public static final Setting PROGRESS_LOG_INTERVAL_SETTING = Setting.timeSetting( + "cluster.routing.allocation.desired_balance.progress_log_interval", + TimeValue.timeValueMinutes(1), + TimeValue.ZERO, + Setting.Property.Dynamic, + Setting.Property.NodeScope + ); + + private TimeValue progressLogInterval; + + public DesiredBalanceComputer( + Settings settings, + ClusterSettings clusterSettings, + ThreadPool threadPool, + ShardsAllocator delegateAllocator + ) { + this.threadPool = threadPool; this.delegateAllocator = delegateAllocator; + watchSetting(settings, clusterSettings, PROGRESS_LOG_INTERVAL_SETTING, value -> this.progressLogInterval = value); + } + + private void watchSetting(Settings settings, ClusterSettings clusterSettings, Setting setting, Consumer consumer) { + consumer.accept(setting.get(settings)); + clusterSettings.addSettingsUpdateConsumer(setting, consumer); } public DesiredBalance compute( @@ -211,6 +241,11 @@ public DesiredBalance compute( } } + final int iterationCountReportInterval = computeIterationCountReportInterval(routingAllocation); + final long timeWarningInterval = progressLogInterval.millis(); + final long computationStartedTime = threadPool.relativeTimeInMillis(); + long nextReportTime = computationStartedTime + timeWarningInterval; + int i = 0; boolean hasChanges = false; while (true) { @@ -246,30 +281,45 @@ public DesiredBalance compute( } i++; + final int iterations = i; + final long currentTime = threadPool.relativeTimeInMillis(); + final boolean reportByTime = nextReportTime <= currentTime; + final boolean reportByIterationCount = i % iterationCountReportInterval == 0; + if (reportByTime || reportByIterationCount) { + nextReportTime = currentTime + timeWarningInterval; + } + if (hasChanges == false) { - logger.debug("Desired balance computation for [{}] converged after [{}] iterations", desiredBalanceInput.index(), i); + logger.debug( + "Desired balance computation for [{}] converged after [{}] and [{}] iterations", + desiredBalanceInput.index(), + TimeValue.timeValueMillis(currentTime - computationStartedTime).toString(), + i + ); break; } if (isFresh.test(desiredBalanceInput) == false) { // we run at least one iteration, but if another reroute happened meanwhile // then publish the interim state and restart the calculation - logger.debug(""" - Newer cluster state received after [{}] iterations, publishing incomplete desired balance for [{}] and restarting \ - computation - """, i, desiredBalanceInput.index()); - break; - } - if (i % 100 == 0) { - // TODO this warning should be time based, iteration count should be proportional to the number of shards - logger.log( - i % 1000000 == 0 ? Level.INFO : Level.DEBUG, - Strings.format( - "Desired balance computation for [%d] is still not converged after [%d] iterations", - desiredBalanceInput.index(), - i - ) + logger.debug( + "Desired balance computation for [{}] interrupted after [{}] and [{}] iterations as newer cluster state received. " + + "Publishing intermediate desired balance and restarting computation", + desiredBalanceInput.index(), + i, + TimeValue.timeValueMillis(currentTime - computationStartedTime).toString() ); + break; } + + logger.log( + reportByIterationCount || reportByTime ? Level.INFO : i % 100 == 0 ? Level.DEBUG : Level.TRACE, + () -> Strings.format( + "Desired balance computation for [%d] is still not converged after [%s] and [%d] iterations", + desiredBalanceInput.index(), + TimeValue.timeValueMillis(currentTime - computationStartedTime).toString(), + iterations + ) + ); } iterations.inc(i); @@ -347,4 +397,13 @@ private static UnassignedInfo discardAllocationStatus(UnassignedInfo info) { info.getLastAllocatedNodeId() ); } + + private static int computeIterationCountReportInterval(RoutingAllocation allocation) { + final int relativeSize = allocation.metadata().getTotalNumberOfShards(); + int iterations = 1000; + while (iterations < relativeSize && iterations < 1_000_000_000) { + iterations *= 10; + } + return iterations; + } } diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceShardsAllocator.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceShardsAllocator.java index 1e1495e0aada..9f811b2d649d 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceShardsAllocator.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceShardsAllocator.java @@ -28,6 +28,8 @@ import org.elasticsearch.cluster.service.MasterService; import org.elasticsearch.common.Priority; import org.elasticsearch.common.metrics.CounterMetric; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.threadpool.ThreadPool; @@ -77,12 +79,20 @@ public interface DesiredBalanceReconcilerAction { } public DesiredBalanceShardsAllocator( + Settings settings, + ClusterSettings clusterSettings, ShardsAllocator delegateAllocator, ThreadPool threadPool, ClusterService clusterService, DesiredBalanceReconcilerAction reconciler ) { - this(delegateAllocator, threadPool, clusterService, new DesiredBalanceComputer(delegateAllocator), reconciler); + this( + delegateAllocator, + threadPool, + clusterService, + new DesiredBalanceComputer(settings, clusterSettings, threadPool, delegateAllocator), + reconciler + ); } public DesiredBalanceShardsAllocator( @@ -211,6 +221,11 @@ protected void reconcile(DesiredBalance desiredBalance, RoutingAllocation alloca } allocationOrdering.retainNodes(getNodeIds(allocation.routingNodes())); recordTime(cumulativeReconciliationTime, new DesiredBalanceReconciler(desiredBalance, allocation, allocationOrdering)::run); + if (logger.isTraceEnabled()) { + logger.trace("Reconciled desired balance: {}", desiredBalance); + } else { + logger.debug("Reconciled desired balance for [{}]", desiredBalance.lastConvergedIndex()); + } } public DesiredBalance getDesiredBalance() { diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index f45758df42d8..b7e3d3360f7b 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -40,6 +40,7 @@ import org.elasticsearch.cluster.routing.allocation.DataTier; import org.elasticsearch.cluster.routing.allocation.DiskThresholdSettings; import org.elasticsearch.cluster.routing.allocation.allocator.BalancedShardsAllocator; +import org.elasticsearch.cluster.routing.allocation.allocator.DesiredBalanceComputer; import org.elasticsearch.cluster.routing.allocation.decider.AwarenessAllocationDecider; import org.elasticsearch.cluster.routing.allocation.decider.ClusterRebalanceAllocationDecider; import org.elasticsearch.cluster.routing.allocation.decider.ConcurrentRebalanceAllocationDecider; @@ -198,6 +199,7 @@ public void apply(Settings value, Settings current, Settings previous) { BalancedShardsAllocator.WRITE_LOAD_BALANCE_FACTOR_SETTING, BalancedShardsAllocator.DISK_USAGE_BALANCE_FACTOR_SETTING, BalancedShardsAllocator.THRESHOLD_SETTING, + DesiredBalanceComputer.PROGRESS_LOG_INTERVAL_SETTING, BreakerSettings.CIRCUIT_BREAKER_LIMIT_SETTING, BreakerSettings.CIRCUIT_BREAKER_OVERHEAD_SETTING, BreakerSettings.CIRCUIT_BREAKER_TYPE, diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/ClusterAllocationSimulationTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/ClusterAllocationSimulationTests.java index 98c068d874d9..bb76a62afb8d 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/ClusterAllocationSimulationTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/ClusterAllocationSimulationTests.java @@ -472,6 +472,8 @@ private Map.Entry createNewAllocationSer ) { var strategyRef = new SetOnce(); var desiredBalanceShardsAllocator = new DesiredBalanceShardsAllocator( + Settings.EMPTY, + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), new BalancedShardsAllocator( Settings.EMPTY, new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceComputerTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceComputerTests.java index c66fdf2ad91b..140460a267fc 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceComputerTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceComputerTests.java @@ -8,6 +8,9 @@ package org.elasticsearch.cluster.routing.allocation.allocator; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.Version; import org.elasticsearch.cluster.ClusterInfo; import org.elasticsearch.cluster.ClusterModule; @@ -36,11 +39,14 @@ import org.elasticsearch.cluster.routing.allocation.decider.ThrottlingAllocationDecider; import org.elasticsearch.common.Randomness; import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.snapshots.SnapshotShardSizeInfo; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.MockLogAppender; +import org.elasticsearch.threadpool.ThreadPool; import java.util.ArrayList; import java.util.HashMap; @@ -52,6 +58,7 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_INDEX_VERSION_CREATED; import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_NUMBER_OF_REPLICAS; @@ -65,6 +72,8 @@ import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasEntry; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class DesiredBalanceComputerTests extends ESTestCase { @@ -376,29 +385,34 @@ public void testRespectsAssignmentByGatewayAllocators() { public void testSimulatesAchievingDesiredBalanceBeforeDelegating() { var allocateCalled = new AtomicBoolean(); - var desiredBalanceComputer = new DesiredBalanceComputer(new ShardsAllocator() { - @Override - public void allocate(RoutingAllocation allocation) { - assertTrue(allocateCalled.compareAndSet(false, true)); - // whatever the allocation in the current cluster state, the desired balance service should start by moving all the - // known shards to their desired locations before delegating to the inner allocator - for (var routingNode : allocation.routingNodes()) { - assertThat( - allocation.routingNodes().toString(), - routingNode.numberOfOwningShards(), - equalTo(routingNode.nodeId().equals("node-2") ? 0 : 2) - ); - for (var shardRouting : routingNode) { - assertTrue(shardRouting.toString(), shardRouting.started()); + var desiredBalanceComputer = new DesiredBalanceComputer( + Settings.EMPTY, + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), + mock(ThreadPool.class), + new ShardsAllocator() { + @Override + public void allocate(RoutingAllocation allocation) { + assertTrue(allocateCalled.compareAndSet(false, true)); + // whatever the allocation in the current cluster state, the desired balance service should start by moving all the + // known shards to their desired locations before delegating to the inner allocator + for (var routingNode : allocation.routingNodes()) { + assertThat( + allocation.routingNodes().toString(), + routingNode.numberOfOwningShards(), + equalTo(routingNode.nodeId().equals("node-2") ? 0 : 2) + ); + for (var shardRouting : routingNode) { + assertTrue(shardRouting.toString(), shardRouting.started()); + } } } - } - @Override - public ShardAllocationDecision decideShardAllocation(ShardRouting shard, RoutingAllocation allocation) { - throw new AssertionError("only used for allocation explain"); + @Override + public ShardAllocationDecision decideShardAllocation(ShardRouting shard, RoutingAllocation allocation) { + throw new AssertionError("only used for allocation explain"); + } } - }); + ); var clusterState = createInitialClusterState(3); var index = clusterState.metadata().index(TEST_INDEX).getIndex(); @@ -645,12 +659,12 @@ public void testDesiredBalanceShouldConvergeInABigCluster() { routingAllocationWithDecidersOf(clusterState, ClusterInfo.EMPTY, Settings.EMPTY), List.of() ); - var desiredBalance = new DesiredBalanceComputer(new BalancedShardsAllocator(Settings.EMPTY)).compute( - DesiredBalance.INITIAL, - input, - queue(), - ignored -> iteration.incrementAndGet() < 1000 - ); + var desiredBalance = new DesiredBalanceComputer( + Settings.EMPTY, + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), + mock(ThreadPool.class), + new BalancedShardsAllocator(Settings.EMPTY) + ).compute(DesiredBalance.INITIAL, input, queue(), ignored -> iteration.incrementAndGet() < 1000); try { assertThat( @@ -811,7 +825,12 @@ public void testComputeConsideringShardSizes() { ) ); - var desiredBalance = new DesiredBalanceComputer(new BalancedShardsAllocator(settings)).compute( + var desiredBalance = new DesiredBalanceComputer( + Settings.EMPTY, + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), + mock(ThreadPool.class), + new BalancedShardsAllocator(settings) + ).compute( initial, new DesiredBalanceInput(randomInt(), routingAllocationWithDecidersOf(clusterState, clusterInfo, settings), List.of()), queue(), @@ -829,6 +848,103 @@ public void testComputeConsideringShardSizes() { assertThat(resultDiskUsage, allOf(aMapWithSize(2), hasEntry("node-0", 950L), hasEntry("node-1", 850L))); } + public void testShouldLogComputationIteration() { + checkIterationLogging( + 999, + 10L, + new MockLogAppender.UnseenEventExpectation( + "Should not report long computation too early", + DesiredBalanceComputer.class.getCanonicalName(), + Level.INFO, + "Desired balance computation for [*] is still not converged after [*] and [*] iterations" + ) + ); + + checkIterationLogging( + 1001, + 10L, + new MockLogAppender.SeenEventExpectation( + "Should report long computation based on iteration count", + DesiredBalanceComputer.class.getCanonicalName(), + Level.INFO, + "Desired balance computation for [*] is still not converged after [10s] and [1000] iterations" + ) + ); + + checkIterationLogging( + 61, + 1000L, + new MockLogAppender.SeenEventExpectation( + "Should report long computation based on time", + DesiredBalanceComputer.class.getCanonicalName(), + Level.INFO, + "Desired balance computation for [*] is still not converged after [1m] and [60] iterations" + ) + ); + } + + private void checkIterationLogging(int iterations, long eachIterationDuration, MockLogAppender.AbstractEventExpectation expectation) { + + var mockThreadPool = mock(ThreadPool.class); + var currentTime = new AtomicLong(0L); + when(mockThreadPool.relativeTimeInMillis()).thenAnswer(invocation -> currentTime.addAndGet(eachIterationDuration)); + + var desiredBalanceComputer = new DesiredBalanceComputer( + Settings.EMPTY, + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), + mockThreadPool, + new ShardsAllocator() { + @Override + public void allocate(RoutingAllocation allocation) { + final var unassignedIterator = allocation.routingNodes().unassigned().iterator(); + while (unassignedIterator.hasNext()) { + final var shardRouting = unassignedIterator.next(); + if (shardRouting.primary()) { + unassignedIterator.initialize("node-0", null, 0L, allocation.changes()); + } else { + unassignedIterator.removeAndIgnore(UnassignedInfo.AllocationStatus.NO_ATTEMPT, allocation.changes()); + } + } + + // move shard on each iteration + for (var shard : allocation.routingNodes().node("node-0").shardsWithState(STARTED).toList()) { + allocation.routingNodes().relocateShard(shard, "node-1", 0L, allocation.changes()); + } + for (var shard : allocation.routingNodes().node("node-1").shardsWithState(STARTED).toList()) { + allocation.routingNodes().relocateShard(shard, "node-0", 0L, allocation.changes()); + } + } + + @Override + public ShardAllocationDecision decideShardAllocation(ShardRouting shard, RoutingAllocation allocation) { + throw new AssertionError("only used for allocation explain"); + } + } + ); + + MockLogAppender mockAppender = new MockLogAppender(); + mockAppender.start(); + mockAppender.addExpectation(expectation); + + Logger logger = LogManager.getLogger(DesiredBalanceComputer.class); + Loggers.addAppender(logger, mockAppender); + + try { + var iteration = new AtomicInteger(0); + desiredBalanceComputer.compute( + DesiredBalance.INITIAL, + createInput(createInitialClusterState(3)), + queue(), + input -> iteration.incrementAndGet() < iterations + ); + + mockAppender.assertAllExpectationsMatched(); + } finally { + Loggers.removeAppender(logger, mockAppender); + mockAppender.stop(); + } + } + private static Map.Entry indexSize(ClusterState clusterState, String name, long size, boolean primary) { return Map.entry(ClusterInfo.shardIdentifierFromRouting(findShardId(clusterState, name), primary), size); } @@ -919,31 +1035,39 @@ private static DiscoveryNode createDiscoveryNode(String id, Set r.primary() && r.started()); - } + private static boolean isCorrespondingPrimaryStarted(ShardRouting shardRouting, RoutingAllocation allocation) { + return allocation.routingNodes() + .assignedShards(shardRouting.shardId()) + .stream() + .anyMatch(r -> r.primary() && r.started()); + } - @Override - public ShardAllocationDecision decideShardAllocation(ShardRouting shard, RoutingAllocation allocation) { - throw new AssertionError("only used for allocation explain"); + @Override + public ShardAllocationDecision decideShardAllocation(ShardRouting shard, RoutingAllocation allocation) { + throw new AssertionError("only used for allocation explain"); + } } - }); + ); } private static void assertDesiredAssignments(DesiredBalance desiredBalance, Map expected) { diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceShardsAllocatorTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceShardsAllocatorTests.java index d854a3cf6def..7acbf79cf5f4 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceShardsAllocatorTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/allocator/DesiredBalanceShardsAllocatorTests.java @@ -113,9 +113,10 @@ public void testAllocate( .blocks(ClusterBlocks.EMPTY_CLUSTER_BLOCK) .build(); - var clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); + var settings = Settings.EMPTY; + var clusterSettings = new ClusterSettings(settings, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); var clusterService = new ClusterService( - Settings.EMPTY, + settings, clusterSettings, new FakeThreadPoolMasterService(LOCAL_NODE_ID, "test", threadPool, deterministicTaskQueue::scheduleNow), new ClusterApplierService(LOCAL_NODE_ID, Settings.EMPTY, clusterSettings, threadPool) { @@ -141,6 +142,8 @@ public ClusterState apply(ClusterState clusterState, Consumer }; final var desiredBalanceShardsAllocator = new DesiredBalanceShardsAllocator( + settings, + clusterSettings, createShardsAllocator(), threadPool, clusterService, @@ -221,7 +224,12 @@ public ClusterState apply(ClusterState clusterState, Consumer shardsAllocator, threadPool, clusterService, - new DesiredBalanceComputer(shardsAllocator) { + new DesiredBalanceComputer( + Settings.EMPTY, + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), + threadPool, + shardsAllocator + ) { @Override public DesiredBalance compute( DesiredBalance previousDesiredBalance, @@ -319,7 +327,12 @@ public ClusterState apply(ClusterState clusterState, Consumer shardsAllocator, threadPool, clusterService, - new DesiredBalanceComputer(shardsAllocator) { + new DesiredBalanceComputer( + Settings.EMPTY, + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), + threadPool, + shardsAllocator + ) { @Override public DesiredBalance compute( DesiredBalance previousDesiredBalance, diff --git a/test/framework/src/main/java/org/elasticsearch/cluster/ESAllocationTestCase.java b/test/framework/src/main/java/org/elasticsearch/cluster/ESAllocationTestCase.java index deab959aa17a..d7089b4bab8a 100644 --- a/test/framework/src/main/java/org/elasticsearch/cluster/ESAllocationTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/cluster/ESAllocationTestCase.java @@ -116,7 +116,14 @@ private static ShardsAllocator createShardsAllocator(Settings settings) { private static DesiredBalanceShardsAllocator createDesiredBalanceShardsAllocator(Settings settings) { var queue = new DeterministicTaskQueue(); - return new DesiredBalanceShardsAllocator(new BalancedShardsAllocator(settings), queue.getThreadPool(), null, null) { + return new DesiredBalanceShardsAllocator( + Settings.EMPTY, + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), + new BalancedShardsAllocator(settings), + queue.getThreadPool(), + null, + null + ) { private RoutingAllocation lastAllocation; @Override From 92d096cd4b4cb7da8a1ccf94ce3775002fcf3ed8 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Tue, 6 Dec 2022 13:38:05 +0000 Subject: [PATCH 167/919] Muting BWC tests while serialization fix is tested (#92158) Relates #92153, #92157 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index e1e11e60e110..54dab18b3da3 100644 --- a/build.gradle +++ b/build.gradle @@ -137,9 +137,9 @@ tasks.register("verifyVersions") { * after the backport of the backcompat code is complete. */ -boolean bwc_tests_enabled = true +boolean bwc_tests_enabled = false // place a PR link here when committing bwc changes: -String bwc_tests_disabled_issue = "" +String bwc_tests_disabled_issue = "https://github.com/elastic/elasticsearch/issues/92153" if (bwc_tests_enabled == false) { if (bwc_tests_disabled_issue.isEmpty()) { throw new GradleException("bwc_tests_disabled_issue must be set when bwc_tests_enabled == false") From 47e573949334c39b986887a5bda7418863dbc233 Mon Sep 17 00:00:00 2001 From: Craig Taverner Date: Tue, 6 Dec 2022 15:01:45 +0100 Subject: [PATCH 168/919] Mute reference/text-structure/apis/find-structure/line_264 for #92141 (#92144) * Mute test for #92141 * Update docs/reference/text-structure/apis/find-structure.asciidoc --- docs/reference/text-structure/apis/find-structure.asciidoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/reference/text-structure/apis/find-structure.asciidoc b/docs/reference/text-structure/apis/find-structure.asciidoc index a65f87290b0a..6798bd953da3 100644 --- a/docs/reference/text-structure/apis/find-structure.asciidoc +++ b/docs/reference/text-structure/apis/find-structure.asciidoc @@ -538,7 +538,8 @@ If the request does not encounter errors, you receive the following result: } } ---- -// TESTRESPONSE[s/"sample_start" : ".*",/"sample_start" : "$body.sample_start",/] +// TESTRESPONSE[skip:"AwaitsFix https://github.com/elastic/elasticsearch/issues/92141"] +// original (put back after unmuting)[s/"sample_start" : ".*",/"sample_start" : "$body.sample_start",/] // The substitution is because the text is pre-processed by the test harness, // so the fields may get reordered in the JSON the endpoint sees From f989f0f7ec803d3c6dd8a5af1aea1ce683d0292c Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Tue, 6 Dec 2022 15:01:57 +0100 Subject: [PATCH 169/919] Save needless Files.exists when opening Translog (#92143) Just a random find from profiling experiments last week. We can save a syscall for the exists check since we try to open the channel here anyway. Also, this technically makes the check more exact as well. --- .../org/elasticsearch/index/translog/Translog.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/translog/Translog.java b/server/src/main/java/org/elasticsearch/index/translog/Translog.java index f15ea8beceb0..b556e1ebec06 100644 --- a/server/src/main/java/org/elasticsearch/index/translog/Translog.java +++ b/server/src/main/java/org/elasticsearch/index/translog/Translog.java @@ -41,6 +41,7 @@ import java.io.IOException; import java.nio.channels.FileChannel; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; @@ -234,7 +235,13 @@ private ArrayList recoverFromFiles(Checkpoint checkpoint) throws // translog was found. for (long i = checkpoint.generation; i >= minGenerationToRecoverFrom; i--) { Path committedTranslogFile = location.resolve(getFilename(i)); - if (Files.exists(committedTranslogFile) == false) { + final Checkpoint readerCheckpoint = i == checkpoint.generation + ? checkpoint + : Checkpoint.read(location.resolve(getCommitCheckpointFileName(i))); + final TranslogReader reader; + try { + reader = openReader(committedTranslogFile, readerCheckpoint); + } catch (NoSuchFileException fnfe) { throw new TranslogCorruptedException( committedTranslogFile.toString(), "translog file doesn't exist with generation: " @@ -246,10 +253,6 @@ private ArrayList recoverFromFiles(Checkpoint checkpoint) throws + " - translog ids must be consecutive" ); } - final Checkpoint readerCheckpoint = i == checkpoint.generation - ? checkpoint - : Checkpoint.read(location.resolve(getCommitCheckpointFileName(i))); - final TranslogReader reader = openReader(committedTranslogFile, readerCheckpoint); assert reader.getPrimaryTerm() <= primaryTermSupplier.getAsLong() : "Primary terms go backwards; current term [" + primaryTermSupplier.getAsLong() From e130617b1b5ada8a0a8c1020f203a3e94ba92c3f Mon Sep 17 00:00:00 2001 From: amyjtechwriter <61687663+amyjtechwriter@users.noreply.github.com> Date: Tue, 6 Dec 2022 14:29:20 +0000 Subject: [PATCH 170/919] putting Miscellaneous cluster settings on it's own page (#92150) --- docs/reference/modules/cluster/misc.asciidoc | 22 ++++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/reference/modules/cluster/misc.asciidoc b/docs/reference/modules/cluster/misc.asciidoc index 83adaef9ec1a..024ca80bb5e3 100644 --- a/docs/reference/modules/cluster/misc.asciidoc +++ b/docs/reference/modules/cluster/misc.asciidoc @@ -1,8 +1,9 @@ [[misc-cluster-settings]] -==== Miscellaneous cluster settings +=== Miscellaneous cluster settings +[discrete] [[cluster-read-only]] -===== Metadata +==== Metadata An entire cluster may be set to read-only with the following setting: @@ -21,9 +22,9 @@ WARNING: Don't rely on this setting to prevent changes to your cluster. Any user with access to the <> API can make the cluster read-write again. - +[discrete] [[cluster-shard-limit]] -===== Cluster shard limit +==== Cluster shard limit There is a soft limit on the number of shards in a cluster, based on the number of nodes in the cluster. This is intended to prevent operations which may @@ -101,8 +102,9 @@ number of shards for each node, use the setting. -- +[discrete] [[user-defined-data]] -===== User-defined cluster metadata +==== User-defined cluster metadata User-defined metadata can be stored and retrieved using the Cluster Settings API. This can be used to store arbitrary, infrequently-changing data about the cluster @@ -127,8 +129,9 @@ metadata will be viewable by anyone with access to the <> API, and is recorded in the {es} logs. +[discrete] [[cluster-max-tombstones]] -===== Index tombstones +==== Index tombstones The cluster state maintains index tombstones to explicitly denote indices that have been deleted. The number of tombstones maintained in the cluster state is @@ -148,8 +151,9 @@ include::{es-repo-dir}/indices/dangling-indices-list.asciidoc[tag=dangling-index You can use the <> to manage this situation. +[discrete] [[cluster-logger]] -===== Logger +==== Logger The settings which control logging can be updated <> with the `logger.` prefix. For instance, to increase the logging level of the @@ -165,9 +169,9 @@ PUT /_cluster/settings } ------------------------------- - +[discrete] [[persistent-tasks-allocation]] -===== Persistent tasks allocation +==== Persistent tasks allocation Plugins can create a kind of tasks called persistent tasks. Those tasks are usually long-lived tasks and are stored in the cluster state, allowing the From a04d0235471dc84032fe8b7b6ff196fa4b81237e Mon Sep 17 00:00:00 2001 From: David Kyle Date: Tue, 6 Dec 2022 15:09:32 +0000 Subject: [PATCH 171/919] [ML] Adjust serialisation of inference requests for BWC (#92157) Adjust serialisation for BWC --- build.gradle | 4 ++-- .../core/ml/action/InferTrainedModelDeploymentAction.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 54dab18b3da3..e1e11e60e110 100644 --- a/build.gradle +++ b/build.gradle @@ -137,9 +137,9 @@ tasks.register("verifyVersions") { * after the backport of the backcompat code is complete. */ -boolean bwc_tests_enabled = false +boolean bwc_tests_enabled = true // place a PR link here when committing bwc changes: -String bwc_tests_disabled_issue = "https://github.com/elastic/elasticsearch/issues/92153" +String bwc_tests_disabled_issue = "" if (bwc_tests_enabled == false) { if (bwc_tests_disabled_issue.isEmpty()) { throw new GradleException("bwc_tests_disabled_issue must be set when bwc_tests_enabled == false") diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/InferTrainedModelDeploymentAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/InferTrainedModelDeploymentAction.java index 539bf59da174..2217403c75b5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/InferTrainedModelDeploymentAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/InferTrainedModelDeploymentAction.java @@ -138,7 +138,7 @@ public Request(StreamInput in) throws IOException { if (in.getVersion().onOrAfter(Version.V_8_3_0)) { skipQueue = in.readBoolean(); } - if (in.getVersion().onOrAfter(Version.V_8_6_0)) { + if (in.getVersion().onOrAfter(Version.V_8_7_0)) { textInput = in.readOptionalString(); } else { textInput = null; @@ -214,7 +214,7 @@ public void writeTo(StreamOutput out) throws IOException { if (out.getVersion().onOrAfter(Version.V_8_3_0)) { out.writeBoolean(skipQueue); } - if (out.getVersion().onOrAfter(Version.V_8_6_0)) { + if (out.getVersion().onOrAfter(Version.V_8_7_0)) { out.writeOptionalString(textInput); } } From 3c3b3a7ded257517732097c3b75b00df0c33347c Mon Sep 17 00:00:00 2001 From: Daniel Mitterdorfer Date: Tue, 6 Dec 2022 16:12:46 +0100 Subject: [PATCH 172/919] Use all profiling events on startup (#92087) With this commit we use `profiling-events-all` to query profiling events if the appropriate index for a sample count is not present yet. This can happen on cluster startup when only a few events have been accumulated because additional indices are only created when there are enough events. This aligns behavior of the Elasticsearch plugin with the Kibana plugin. We also refactor the profiling-related test cases so we can simulate situations after cluster startup when not all profiling-related indices have been created. As testing also cancellation would require additional complexity and decreases test maintainability (need to adjust `slack` based on the scenario which is a very low-level change), we also separate tests for the regular case and cancellation. Relates #91640 --- docs/changelog/92087.yaml | 5 + .../xpack/profiler/CancellationIT.java | 233 ++++++++++++++ .../xpack/profiler/GetProfilingActionIT.java | 292 +----------------- .../xpack/profiler/ProfilingTestCase.java | 110 +++++++ .../profiler/TransportGetProfilingAction.java | 16 +- 5 files changed, 366 insertions(+), 290 deletions(-) create mode 100644 docs/changelog/92087.yaml create mode 100644 x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/CancellationIT.java create mode 100644 x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/ProfilingTestCase.java diff --git a/docs/changelog/92087.yaml b/docs/changelog/92087.yaml new file mode 100644 index 000000000000..14faa9abd005 --- /dev/null +++ b/docs/changelog/92087.yaml @@ -0,0 +1,5 @@ +pr: 92087 +summary: Use all profiling events on startup +area: Search +type: bug +issues: [] diff --git a/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/CancellationIT.java b/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/CancellationIT.java new file mode 100644 index 000000000000..d04289298a35 --- /dev/null +++ b/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/CancellationIT.java @@ -0,0 +1,233 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.profiler; + +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.logging.log4j.LogManager; +import org.apache.lucene.util.SetOnce; +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; +import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; +import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.client.Cancellable; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.plugins.PluginsService; +import org.elasticsearch.script.MockScriptPlugin; +import org.elasticsearch.search.lookup.LeafStoredFieldsLookup; +import org.elasticsearch.tasks.CancellableTask; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.tasks.TaskId; +import org.elasticsearch.tasks.TaskInfo; +import org.elasticsearch.tasks.TaskManager; +import org.elasticsearch.transport.TransportService; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CancellationException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +import static org.elasticsearch.action.support.ActionTestUtils.wrapAsRestResponseListener; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.instanceOf; + +public class CancellationIT extends ProfilingTestCase { + @Override + protected Collection> nodePlugins() { + List> plugins = new ArrayList<>(super.nodePlugins()); + plugins.add(ScriptedBlockPlugin.class); + return plugins; + } + + @Override + protected boolean useOnlyAllEvents() { + // we assume that all indices have been created to simplify the testing logic. + return false; + } + + public void testAutomaticCancellation() throws Exception { + Request restRequest = new Request("POST", "/_profiling/stacktraces"); + restRequest.setEntity(new StringEntity(""" + { + "sample_size": 10000, + "query": { + "bool": { + "filter": [ + { + "script": { + "script": { + "lang": "mockscript", + "source": "search_block", + "params": {} + } + } + } + ] + } + } + } + """, ContentType.APPLICATION_JSON.withCharset(StandardCharsets.UTF_8))); + verifyCancellation(GetProfilingAction.NAME, restRequest); + } + + void verifyCancellation(String action, Request restRequest) throws Exception { + Map nodeIdToName = readNodesInfo(); + List plugins = initBlockFactory(); + + PlainActionFuture future = PlainActionFuture.newFuture(); + Cancellable cancellable = getRestClient().performRequestAsync(restRequest, wrapAsRestResponseListener(future)); + + awaitForBlock(plugins); + Collection profilingTasks = collectProfilingRelatedTasks(action); + cancellable.cancel(); + ensureTasksAreCancelled(profilingTasks, nodeIdToName::get); + + disableBlocks(plugins); + expectThrows(CancellationException.class, future::actionGet); + } + + private static Map readNodesInfo() { + Map nodeIdToName = new HashMap<>(); + NodesInfoResponse nodesInfoResponse = client().admin().cluster().prepareNodesInfo().get(); + assertFalse(nodesInfoResponse.hasFailures()); + for (NodeInfo node : nodesInfoResponse.getNodes()) { + nodeIdToName.put(node.getNode().getId(), node.getNode().getName()); + } + return nodeIdToName; + } + + private static Collection collectProfilingRelatedTasks(String transportAction) { + SetOnce profilingTask = new SetOnce<>(); + Map> taskToParent = new HashMap<>(); + ListTasksResponse listTasksResponse = client().admin().cluster().prepareListTasks().get(); + for (TaskInfo task : listTasksResponse.getTasks()) { + TaskId parentTaskId = task.parentTaskId(); + if (parentTaskId != null) { + if (taskToParent.containsKey(parentTaskId) == false) { + taskToParent.put(parentTaskId, new HashSet<>()); + } + taskToParent.get(parentTaskId).add(task.taskId()); + } + if (task.action().equals(transportAction)) { + profilingTask.set(task); + } + } + assertNotNull(profilingTask.get()); + Set childTaskIds = taskToParent.get(profilingTask.get().taskId()); + Set profilingTaskIds = new HashSet<>(); + profilingTaskIds.add(profilingTask.get().taskId()); + if (childTaskIds != null) { + profilingTaskIds.addAll(childTaskIds); + } + return profilingTaskIds; + } + + private static void ensureTasksAreCancelled(Collection taskIds, Function nodeIdToName) throws Exception { + assertBusy(() -> { + for (TaskId taskId : taskIds) { + String nodeName = nodeIdToName.apply(taskId.getNodeId()); + TaskManager taskManager = internalCluster().getInstance(TransportService.class, nodeName).getTaskManager(); + Task task = taskManager.getTask(taskId.getId()); + // as we capture the task hierarchy at the beginning but cancel in the middle of execution, some tasks have been + // unregistered already by the time we verify cancellation. + if (task != null) { + assertThat(task, instanceOf(CancellableTask.class)); + assertTrue(((CancellableTask) task).isCancelled()); + } + } + }); + } + + private static List initBlockFactory() { + List plugins = new ArrayList<>(); + for (PluginsService pluginsService : internalCluster().getDataNodeInstances(PluginsService.class)) { + plugins.addAll(pluginsService.filterPlugins(ScriptedBlockPlugin.class)); + } + for (ScriptedBlockPlugin plugin : plugins) { + plugin.reset(); + plugin.enableBlock(); + // Allow to execute one search and only block starting with the second one. This + // is done so we have at least one child action and can check that all active children + // are cancelled with the parent action. + plugin.setSlack(1); + } + return plugins; + } + + private void awaitForBlock(List plugins) throws Exception { + assertBusy(() -> { + int numberOfBlockedPlugins = 0; + for (ScriptedBlockPlugin plugin : plugins) { + numberOfBlockedPlugins += plugin.hits.get(); + } + logger.info("The plugin blocked on {} shards", numberOfBlockedPlugins); + assertThat(numberOfBlockedPlugins, greaterThan(0)); + }, 10, TimeUnit.SECONDS); + } + + private static void disableBlocks(List plugins) { + for (ScriptedBlockPlugin plugin : plugins) { + plugin.disableBlock(); + } + } + + public static class ScriptedBlockPlugin extends MockScriptPlugin { + static final String SCRIPT_NAME = "search_block"; + + private final AtomicInteger hits = new AtomicInteger(); + + private final AtomicInteger slack = new AtomicInteger(0); + + private final AtomicBoolean shouldBlock = new AtomicBoolean(true); + + void reset() { + hits.set(0); + } + + void disableBlock() { + shouldBlock.set(false); + } + + void enableBlock() { + shouldBlock.set(true); + } + + void setSlack(int slack) { + this.slack.set(slack); + } + + @Override + public Map, Object>> pluginScripts() { + return Collections.singletonMap(SCRIPT_NAME, params -> { + LeafStoredFieldsLookup fieldsLookup = (LeafStoredFieldsLookup) params.get("_fields"); + LogManager.getLogger(CancellationIT.class).info("Blocking on the document {}", fieldsLookup.get("_id")); + hits.incrementAndGet(); + if (slack.decrementAndGet() < 0) { + try { + waitUntil(() -> shouldBlock.get() == false); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return true; + }); + } + } +} diff --git a/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/GetProfilingActionIT.java b/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/GetProfilingActionIT.java index dd9d43e21374..89b26049da82 100644 --- a/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/GetProfilingActionIT.java +++ b/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/GetProfilingActionIT.java @@ -7,129 +7,12 @@ package org.elasticsearch.xpack.profiler; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; -import org.apache.logging.log4j.LogManager; -import org.apache.lucene.util.SetOnce; -import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; -import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; -import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse; -import org.elasticsearch.action.index.IndexResponse; -import org.elasticsearch.action.support.PlainActionFuture; -import org.elasticsearch.client.Cancellable; -import org.elasticsearch.client.Request; -import org.elasticsearch.client.Response; -import org.elasticsearch.common.network.NetworkModule; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.plugins.PluginsService; import org.elasticsearch.rest.RestStatus; -import org.elasticsearch.script.MockScriptPlugin; -import org.elasticsearch.search.lookup.LeafStoredFieldsLookup; -import org.elasticsearch.tasks.CancellableTask; -import org.elasticsearch.tasks.Task; -import org.elasticsearch.tasks.TaskId; -import org.elasticsearch.tasks.TaskInfo; -import org.elasticsearch.tasks.TaskManager; -import org.elasticsearch.test.ESIntegTestCase; -import org.elasticsearch.transport.TransportService; -import org.elasticsearch.transport.netty4.Netty4Plugin; -import org.elasticsearch.xcontent.XContentType; -import org.junit.Before; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CancellationException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; - -import static org.elasticsearch.action.support.ActionTestUtils.wrapAsRestResponseListener; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.instanceOf; - -@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 1) -public class GetProfilingActionIT extends ESIntegTestCase { +public class GetProfilingActionIT extends ProfilingTestCase { @Override - protected Collection> nodePlugins() { - return List.of(ProfilingPlugin.class, ScriptedBlockPlugin.class, getTestTransportPlugin()); - } - - @Override - protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) { - return Settings.builder() - .put(super.nodeSettings(nodeOrdinal, otherSettings)) - .put(ProfilingPlugin.PROFILING_ENABLED.getKey(), true) - .put(NetworkModule.TRANSPORT_TYPE_KEY, Netty4Plugin.NETTY_TRANSPORT_NAME) - .put(NetworkModule.HTTP_TYPE_KEY, Netty4Plugin.NETTY_HTTP_TRANSPORT_NAME) - .build(); - } - - @Override - protected boolean addMockHttpTransport() { - return false; // enable http - } - - @Override - protected boolean ignoreExternalCluster() { - return true; - } - - private byte[] read(String resource) throws IOException { - return GetProfilingAction.class.getClassLoader().getResourceAsStream(resource).readAllBytes(); - } - - private void createIndex(String name, String bodyFileName) throws Exception { - client().admin().indices().prepareCreate(name).setSource(read(bodyFileName), XContentType.JSON).execute().get(); - } - - private void indexDoc(String index, String id, Map source) { - IndexResponse indexResponse = client().prepareIndex(index).setId(id).setSource(source).get(); - assertEquals(RestStatus.CREATED, indexResponse.status()); - } - - @Before - public void setupData() throws Exception { - - for (String idx : EventsIndex.indexNames()) { - createIndex(idx, "events.json"); - } - createIndex("profiling-stackframes", "stackframes.json"); - createIndex("profiling-stacktraces", "stacktraces.json"); - createIndex("profiling-executables", "executables.json"); - ensureGreen(); - - // ensure that we have this in every index, so we find an event - for (String idx : EventsIndex.indexNames()) { - indexDoc( - idx, - "QjoLteG7HX3VUUXr-J4kHQ", - Map.of("@timestamp", 1668761065, "Stacktrace.id", "QjoLteG7HX3VUUXr-J4kHQ", "Stacktrace.count", 1) - ); - } - - indexDoc( - "profiling-stacktraces", - "QjoLteG7HX3VUUXr-J4kHQ", - Map.of("Stacktrace.frame.ids", "QCCDqjSg3bMK1C4YRK6TiwAAAAAAEIpf", "Stacktrace.frame.types", "AQI") - ); - indexDoc( - "profiling-stackframes", - "QCCDqjSg3bMK1C4YRK6TiwAAAAAAEIpf", - Map.of("Stackframe.function.name", "_raw_spin_unlock_irqrestore") - ); - indexDoc("profiling-executables", "QCCDqjSg3bMK1C4YRK6Tiw", Map.of("Executable.file.name", "libc.so.6")); - - refresh(); + protected boolean useOnlyAllEvents() { + return randomBoolean(); } public void testGetProfilingDataUnfiltered() throws Exception { @@ -153,173 +36,4 @@ public void testGetProfilingDataUnfiltered() throws Exception { assertNotNull(response.getExecutables()); assertNotNull("libc.so.6", response.getExecutables().get("QCCDqjSg3bMK1C4YRK6Tiw")); } - - public void testAutomaticCancellation() throws Exception { - Request restRequest = new Request("POST", "/_profiling/stacktraces"); - restRequest.setEntity(new StringEntity(""" - { - "sample_size": 10000, - "query": { - "bool": { - "filter": [ - { - "script": { - "script": { - "lang": "mockscript", - "source": "search_block", - "params": {} - } - } - } - ] - } - } - } - """, ContentType.APPLICATION_JSON.withCharset(StandardCharsets.UTF_8))); - verifyCancellation(GetProfilingAction.NAME, restRequest); - } - - void verifyCancellation(String action, Request restRequest) throws Exception { - Map nodeIdToName = readNodesInfo(); - List plugins = initBlockFactory(); - - PlainActionFuture future = PlainActionFuture.newFuture(); - Cancellable cancellable = getRestClient().performRequestAsync(restRequest, wrapAsRestResponseListener(future)); - - awaitForBlock(plugins); - Collection profilingTasks = collectProfilingRelatedTasks(action); - cancellable.cancel(); - ensureTasksAreCancelled(profilingTasks, nodeIdToName::get); - - disableBlocks(plugins); - expectThrows(CancellationException.class, future::actionGet); - } - - private static Map readNodesInfo() { - Map nodeIdToName = new HashMap<>(); - NodesInfoResponse nodesInfoResponse = client().admin().cluster().prepareNodesInfo().get(); - assertFalse(nodesInfoResponse.hasFailures()); - for (NodeInfo node : nodesInfoResponse.getNodes()) { - nodeIdToName.put(node.getNode().getId(), node.getNode().getName()); - } - return nodeIdToName; - } - - private static Collection collectProfilingRelatedTasks(String transportAction) { - SetOnce profilingTask = new SetOnce<>(); - Map> taskToParent = new HashMap<>(); - ListTasksResponse listTasksResponse = client().admin().cluster().prepareListTasks().get(); - for (TaskInfo task : listTasksResponse.getTasks()) { - TaskId parentTaskId = task.parentTaskId(); - if (parentTaskId != null) { - if (taskToParent.containsKey(parentTaskId) == false) { - taskToParent.put(parentTaskId, new HashSet<>()); - } - taskToParent.get(parentTaskId).add(task.taskId()); - } - if (task.action().equals(transportAction)) { - profilingTask.set(task); - } - } - assertNotNull(profilingTask.get()); - Set childTaskIds = taskToParent.get(profilingTask.get().taskId()); - Set profilingTaskIds = new HashSet<>(); - profilingTaskIds.add(profilingTask.get().taskId()); - if (childTaskIds != null) { - profilingTaskIds.addAll(childTaskIds); - } - return profilingTaskIds; - } - - private static void ensureTasksAreCancelled(Collection taskIds, Function nodeIdToName) throws Exception { - assertBusy(() -> { - for (TaskId taskId : taskIds) { - String nodeName = nodeIdToName.apply(taskId.getNodeId()); - TaskManager taskManager = internalCluster().getInstance(TransportService.class, nodeName).getTaskManager(); - Task task = taskManager.getTask(taskId.getId()); - // as we capture the task hierarchy at the beginning but cancel in the middle of execution, some tasks have been - // unregistered already by the time we verify cancellation. - if (task != null) { - assertThat(task, instanceOf(CancellableTask.class)); - assertTrue(((CancellableTask) task).isCancelled()); - } - } - }); - } - - private static List initBlockFactory() { - List plugins = new ArrayList<>(); - for (PluginsService pluginsService : internalCluster().getDataNodeInstances(PluginsService.class)) { - plugins.addAll(pluginsService.filterPlugins(ScriptedBlockPlugin.class)); - } - for (ScriptedBlockPlugin plugin : plugins) { - plugin.reset(); - plugin.enableBlock(); - // Allow to execute one search and only block starting with the second one. This - // is done so we have at least one child action and can check that all active children - // are cancelled with the parent action. - plugin.setSlack(1); - } - return plugins; - } - - private void awaitForBlock(List plugins) throws Exception { - assertBusy(() -> { - int numberOfBlockedPlugins = 0; - for (ScriptedBlockPlugin plugin : plugins) { - numberOfBlockedPlugins += plugin.hits.get(); - } - logger.info("The plugin blocked on {} shards", numberOfBlockedPlugins); - assertThat(numberOfBlockedPlugins, greaterThan(0)); - }, 10, TimeUnit.SECONDS); - } - - private static void disableBlocks(List plugins) { - for (ScriptedBlockPlugin plugin : plugins) { - plugin.disableBlock(); - } - } - - public static class ScriptedBlockPlugin extends MockScriptPlugin { - static final String SCRIPT_NAME = "search_block"; - - private final AtomicInteger hits = new AtomicInteger(); - - private final AtomicInteger slack = new AtomicInteger(0); - - private final AtomicBoolean shouldBlock = new AtomicBoolean(true); - - void reset() { - hits.set(0); - } - - void disableBlock() { - shouldBlock.set(false); - } - - void enableBlock() { - shouldBlock.set(true); - } - - void setSlack(int slack) { - this.slack.set(slack); - } - - @Override - public Map, Object>> pluginScripts() { - return Collections.singletonMap(SCRIPT_NAME, params -> { - LeafStoredFieldsLookup fieldsLookup = (LeafStoredFieldsLookup) params.get("_fields"); - LogManager.getLogger(GetProfilingActionIT.class).info("Blocking on the document {}", fieldsLookup.get("_id")); - hits.incrementAndGet(); - if (slack.decrementAndGet() < 0) { - try { - waitUntil(() -> shouldBlock.get() == false); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - return true; - }); - } - } } diff --git a/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/ProfilingTestCase.java b/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/ProfilingTestCase.java new file mode 100644 index 000000000000..214d4008d682 --- /dev/null +++ b/x-pack/plugin/profiler/src/internalClusterTest/java/org/elasticsearch/xpack/profiler/ProfilingTestCase.java @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.profiler; + +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.common.network.NetworkModule; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.transport.netty4.Netty4Plugin; +import org.elasticsearch.xcontent.XContentType; +import org.junit.Before; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 1) +public abstract class ProfilingTestCase extends ESIntegTestCase { + @Override + protected Collection> nodePlugins() { + return List.of(ProfilingPlugin.class, getTestTransportPlugin()); + } + + @Override + protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) { + return Settings.builder() + .put(super.nodeSettings(nodeOrdinal, otherSettings)) + .put(ProfilingPlugin.PROFILING_ENABLED.getKey(), true) + .put(NetworkModule.TRANSPORT_TYPE_KEY, Netty4Plugin.NETTY_TRANSPORT_NAME) + .put(NetworkModule.HTTP_TYPE_KEY, Netty4Plugin.NETTY_HTTP_TRANSPORT_NAME) + .build(); + } + + @Override + protected boolean addMockHttpTransport() { + return false; // enable http + } + + @Override + protected boolean ignoreExternalCluster() { + return true; + } + + private byte[] read(String resource) throws IOException { + return GetProfilingAction.class.getClassLoader().getResourceAsStream(resource).readAllBytes(); + } + + private void createIndex(String name, String bodyFileName) throws Exception { + client().admin().indices().prepareCreate(name).setSource(read(bodyFileName), XContentType.JSON).execute().get(); + } + + private void indexDoc(String index, String id, Map source) { + IndexResponse indexResponse = client().prepareIndex(index).setId(id).setSource(source).get(); + assertEquals(RestStatus.CREATED, indexResponse.status()); + } + + /** + * Only the index "profiling-events-all" is always present. All other indices (e.g. "profiling-events-5pow02") are created on demand + * at a later point when there are enough samples. With this flag we simulate that data should be retrieved briefly after cluster + * start when only profiling-events-all is present. We expect that also in this case, available data is returned but we rely only + * on the single existing index. + * + * @return true iff this test should rely on only "profiling-events-all" being present. + */ + protected abstract boolean useOnlyAllEvents(); + + @Before + public void setupData() throws Exception { + Collection eventsIndices = useOnlyAllEvents() ? List.of(EventsIndex.FULL_INDEX.getName()) : EventsIndex.indexNames(); + + for (String idx : eventsIndices) { + createIndex(idx, "events.json"); + } + createIndex("profiling-stackframes", "stackframes.json"); + createIndex("profiling-stacktraces", "stacktraces.json"); + createIndex("profiling-executables", "executables.json"); + ensureGreen(); + + // ensure that we have this in every index, so we find an event + for (String idx : eventsIndices) { + indexDoc( + idx, + "QjoLteG7HX3VUUXr-J4kHQ", + Map.of("@timestamp", 1668761065, "Stacktrace.id", "QjoLteG7HX3VUUXr-J4kHQ", "Stacktrace.count", 1) + ); + } + + indexDoc( + "profiling-stacktraces", + "QjoLteG7HX3VUUXr-J4kHQ", + Map.of("Stacktrace.frame.ids", "QCCDqjSg3bMK1C4YRK6TiwAAAAAAEIpf", "Stacktrace.frame.types", "AQI") + ); + indexDoc( + "profiling-stackframes", + "QCCDqjSg3bMK1C4YRK6TiwAAAAAAEIpf", + Map.of("Stackframe.function.name", "_raw_spin_unlock_irqrestore") + ); + indexDoc("profiling-executables", "QCCDqjSg3bMK1C4YRK6Tiw", Map.of("Executable.file.name", "libc.so.6")); + + refresh(); + } +} diff --git a/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/TransportGetProfilingAction.java b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/TransportGetProfilingAction.java index fa9c9f1fd15f..8a09e2f0a4ec 100644 --- a/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/TransportGetProfilingAction.java +++ b/x-pack/plugin/profiler/src/main/java/org/elasticsearch/xpack/profiler/TransportGetProfilingAction.java @@ -6,6 +6,8 @@ */ package org.elasticsearch.xpack.profiler; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.get.MultiGetItemResponse; import org.elasticsearch.action.get.MultiGetResponse; @@ -17,6 +19,7 @@ import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.util.Maps; +import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.search.aggregations.bucket.terms.StringTerms; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.Sum; @@ -33,6 +36,7 @@ import java.util.Set; public class TransportGetProfilingAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(TransportGetProfilingAction.class); private final NodeClient nodeClient; private final TransportService transportService; @@ -61,7 +65,17 @@ public void onResponse(SearchResponse searchResponse) { @Override public void onFailure(Exception e) { - submitListener.onFailure(e); + // Apart from profiling-events-all, indices are created lazily. In a relatively empty cluster it can happen + // that there are so few data that we need to resort to the full index. As this is an edge case we'd rather + // fail instead of prematurely checking for existence in all cases. + if (e instanceof IndexNotFoundException) { + String missingIndex = ((IndexNotFoundException) e).getIndex().getName(); + EventsIndex fullIndex = EventsIndex.FULL_INDEX; + log.debug("Index [{}] does not exist. Using [{}] instead.", missingIndex, fullIndex.getName()); + searchEventGroupByStackTrace(client, request, fullIndex, submitListener); + } else { + submitListener.onFailure(e); + } } }); } From 2d8c93a9a63ceba463652982b3ea1d77577c473c Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Tue, 6 Dec 2022 16:39:12 +0100 Subject: [PATCH 173/919] Add primary term supplier to Engine.IndexCommitListener (#92101) This change allows IndexCommitListener to have access to the index shard's primary term when they are invoked. Relates #92017 --- docs/changelog/92101.yaml | 5 +++++ .../main/java/org/elasticsearch/index/engine/Engine.java | 9 ++++++--- .../org/elasticsearch/index/engine/InternalEngine.java | 3 ++- .../java/org/elasticsearch/index/IndexModuleTests.java | 6 +++++- .../elasticsearch/index/engine/InternalEngineTests.java | 8 +++++++- 5 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 docs/changelog/92101.yaml diff --git a/docs/changelog/92101.yaml b/docs/changelog/92101.yaml new file mode 100644 index 000000000000..509166d7330f --- /dev/null +++ b/docs/changelog/92101.yaml @@ -0,0 +1,5 @@ +pr: 92101 +summary: Add primary term supplier to Engine.IndexCommitListener +area: Engine +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/index/engine/Engine.java b/server/src/main/java/org/elasticsearch/index/engine/Engine.java index 0e28541ac3f3..d97849195372 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/Engine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/Engine.java @@ -222,15 +222,18 @@ public interface IndexCommitListener { * {@link IndexCommitRef} files to be deleted from disk until the reference is closed. As such, the listener must close the * reference as soon as it is done with it. * + * @param shardId the {@link ShardId} of shard + * @param primaryTerm the shard's primary term value * @param indexCommitRef a reference on the newly created index commit */ - void onNewCommit(ShardId shardId, Engine.IndexCommitRef indexCommitRef); + void onNewCommit(ShardId shardId, long primaryTerm, Engine.IndexCommitRef indexCommitRef); /** * This method is invoked after the policy deleted the given {@link IndexCommit}. A listener is never notified of a deleted commit - * until the corresponding {@link Engine.IndexCommitRef} received through {@link #onNewCommit(ShardId, IndexCommitRef)} has been - * closed; closing which in turn can call this method directly. + * until the corresponding {@link Engine.IndexCommitRef} received through + * {@link #onNewCommit(ShardId, long, IndexCommitRef)} has been closed; closing which in turn can call this method directly. * + * @param shardId the {@link ShardId} of shard * @param deletedCommit the deleted {@link IndexCommit} */ void onIndexCommitDelete(ShardId shardId, IndexCommit deletedCommit); diff --git a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java index 005090bc969c..407e99df3971 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -328,12 +328,13 @@ private SoftDeletesPolicy newSoftDeletesPolicy() throws IOException { private CombinedDeletionPolicy.CommitsListener newCommitsListener() { final Engine.IndexCommitListener listener = engineConfig.getIndexCommitListener(); if (listener != null) { + var primaryTerm = config().getPrimaryTermSupplier().getAsLong(); return new CombinedDeletionPolicy.CommitsListener() { @Override public void onNewAcquiredCommit(final IndexCommit commit) { final IndexCommitRef indexCommitRef = acquireIndexCommitRef(() -> commit); assert indexCommitRef.getIndexCommit() == commit; - listener.onNewCommit(shardId, indexCommitRef); + listener.onNewCommit(shardId, primaryTerm, indexCommitRef); } @Override diff --git a/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java b/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java index 9535eb671750..d5d7cd504bc6 100644 --- a/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java +++ b/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java @@ -104,6 +104,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import static java.util.Collections.emptyMap; @@ -637,12 +638,14 @@ public void testIndexCommitListenerIsBound() throws IOException, ExecutionExcept Collections.emptyMap() ); + final AtomicLong lastAcquiredPrimaryTerm = new AtomicLong(); final AtomicReference lastAcquiredCommit = new AtomicReference<>(); final AtomicReference lastDeletedCommit = new AtomicReference<>(); module.setIndexCommitListener(new Engine.IndexCommitListener() { @Override - public void onNewCommit(ShardId shardId, Engine.IndexCommitRef indexCommitRef) { + public void onNewCommit(ShardId shardId, long primaryTerm, Engine.IndexCommitRef indexCommitRef) { + lastAcquiredPrimaryTerm.set(primaryTerm); lastAcquiredCommit.set(indexCommitRef); } @@ -687,6 +690,7 @@ public void onIndexCommitDelete(ShardId shardId, IndexCommit deletedCommit) { indexShard.recoverFromStore(recoveryFuture); recoveryFuture.get(); + assertThat(lastAcquiredPrimaryTerm.get(), equalTo(indexShard.getOperationPrimaryTerm())); Engine.IndexCommitRef lastCommitRef = lastAcquiredCommit.get(); assertThat(lastCommitRef, notNullValue()); IndexCommit lastCommit = lastCommitRef.getIndexCommit(); diff --git a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java index 418830c7b2eb..804a2e29b4d2 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -7478,12 +7478,15 @@ public void testTrimUnsafeCommitHasESVersionInUserData() throws IOException { public void testIndexCommitsListener() throws Exception { final Map acquiredCommits = new HashMap<>(); final List deletedCommits = new ArrayList<>(); + final List acquiredPrimaryTerms = new ArrayList<>(); final Engine.IndexCommitListener indexCommitListener = new Engine.IndexCommitListener() { @Override - public void onNewCommit(ShardId shardId, Engine.IndexCommitRef indexCommitRef) { + public void onNewCommit(ShardId shardId, long primaryTerm, Engine.IndexCommitRef indexCommitRef) { assertThat(acquiredCommits.put(indexCommitRef.getIndexCommit(), indexCommitRef), nullValue()); assertThat(shardId, equalTo(InternalEngineTests.this.shardId)); + assertThat(primaryTerm, greaterThanOrEqualTo(0L)); + acquiredPrimaryTerms.add(primaryTerm); } @Override @@ -7567,6 +7570,9 @@ public void onIndexCommitDelete(ShardId shardId, IndexCommit deletedCommit) { } releaseCommitRef(acquiredCommits, 7L); + + final long primaryTerm = engine.config().getPrimaryTermSupplier().getAsLong(); + assertThat(acquiredPrimaryTerms.stream().allMatch(value -> value == primaryTerm), is(true)); } } From a334a530efa1460872ebb2bf53706cba3fbdac9e Mon Sep 17 00:00:00 2001 From: Nikola Grcevski <6207777+grcevski@users.noreply.github.com> Date: Tue, 6 Dec 2022 12:08:21 -0500 Subject: [PATCH 174/919] [TEST] Retry file move for SnaphotsAndFileSettingsIT (#91863) --- ...T.java => SnapshotsAndFileSettingsIT.java} | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) rename server/src/internalClusterTest/java/org/elasticsearch/reservedstate/service/{SnaphotsAndFileSettingsIT.java => SnapshotsAndFileSettingsIT.java} (94%) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/reservedstate/service/SnaphotsAndFileSettingsIT.java b/server/src/internalClusterTest/java/org/elasticsearch/reservedstate/service/SnapshotsAndFileSettingsIT.java similarity index 94% rename from server/src/internalClusterTest/java/org/elasticsearch/reservedstate/service/SnaphotsAndFileSettingsIT.java rename to server/src/internalClusterTest/java/org/elasticsearch/reservedstate/service/SnapshotsAndFileSettingsIT.java index 360dd4a68415..7330583a3421 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/reservedstate/service/SnaphotsAndFileSettingsIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/reservedstate/service/SnapshotsAndFileSettingsIT.java @@ -17,6 +17,7 @@ import org.elasticsearch.cluster.metadata.ReservedStateHandlerMetadata; import org.elasticsearch.cluster.metadata.ReservedStateMetadata; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.Randomness; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.Strings; import org.elasticsearch.core.TimeValue; @@ -26,6 +27,7 @@ import org.elasticsearch.snapshots.SnapshotState; import org.junit.After; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -42,7 +44,7 @@ * Tests that snapshot restore behaves correctly when we have file based settings that reserve part of the * cluster state */ -public class SnaphotsAndFileSettingsIT extends AbstractSnapshotIntegTestCase { +public class SnapshotsAndFileSettingsIT extends AbstractSnapshotIntegTestCase { private static AtomicLong versionCounter = new AtomicLong(1); private static String testFileSettingsJSON = """ @@ -74,6 +76,10 @@ public void cleanUp() throws Exception { awaitNoMoreRunningOperations(); } + private long retryDelay(int retryCount) { + return 100 * (1 << retryCount) + Randomness.get().nextInt(10); + } + private void writeJSONFile(String node, String json) throws Exception { long version = versionCounter.incrementAndGet(); @@ -83,7 +89,21 @@ private void writeJSONFile(String node, String json) throws Exception { Path tempFilePath = createTempFile(); Files.write(tempFilePath, Strings.format(json, version).getBytes(StandardCharsets.UTF_8)); - Files.move(tempFilePath, fileSettingsService.operatorSettingsFile(), StandardCopyOption.ATOMIC_MOVE); + int retryCount = 0; + do { + try { + // this can fail on Windows because of timing + Files.move(tempFilePath, fileSettingsService.operatorSettingsFile(), StandardCopyOption.ATOMIC_MOVE); + return; + } catch (IOException e) { + logger.info("--> retrying writing a settings file [" + retryCount + "]"); + if (retryCount == 4) { // retry 5 times + throw e; + } + Thread.sleep(retryDelay(retryCount)); + retryCount++; + } + } while (true); } private Tuple setupClusterStateListener(String node) { From 809ccc4aecd08711e34a43b8d7032f403452247b Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Wed, 7 Dec 2022 09:05:28 +0100 Subject: [PATCH 175/919] mute LatLonGeometryRelationVisitorTests#testPoint (#92178) tes mute --- .../index/fielddata/LatLonGeometryRelationVisitorTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/LatLonGeometryRelationVisitorTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/LatLonGeometryRelationVisitorTests.java index 0e3a8cf3d88d..25b7a381e862 100644 --- a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/LatLonGeometryRelationVisitorTests.java +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/LatLonGeometryRelationVisitorTests.java @@ -22,6 +22,7 @@ public class LatLonGeometryRelationVisitorTests extends ESTestCase { + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/92151") public void testPoint() throws Exception { doTestShapes(GeoTestUtil::nextPoint); } From 6d7237e67b36f7d0a3a9ab0de59c56fae21f64bc Mon Sep 17 00:00:00 2001 From: Ryan Eno Date: Tue, 6 Dec 2022 22:23:04 -1000 Subject: [PATCH 176/919] Update ilm-index-lifecycle.asciidoc (#91782) Adds a missing list item `-` indicator for the cold phase. Was causing `migrate` and `downsample` to be rendered on the same line. --- docs/reference/ilm/ilm-index-lifecycle.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/ilm/ilm-index-lifecycle.asciidoc b/docs/reference/ilm/ilm-index-lifecycle.asciidoc index aa97993054b6..a35d332c5b7e 100644 --- a/docs/reference/ilm/ilm-index-lifecycle.asciidoc +++ b/docs/reference/ilm/ilm-index-lifecycle.asciidoc @@ -107,7 +107,7 @@ actions in the order listed. - <> - <> - <> - <> + - <> * Frozen - <> - <> From f5b98b318204412c593b6461f23d74c1a1e3196c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Wed, 7 Dec 2022 09:28:38 +0100 Subject: [PATCH 177/919] [DOCS] Documents how aggregate_metric_double works in datafeeds (#92139) --- .../ml-configuring-aggregations.asciidoc | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/docs/reference/ml/anomaly-detection/ml-configuring-aggregations.asciidoc b/docs/reference/ml/anomaly-detection/ml-configuring-aggregations.asciidoc index 469f0bdb12b5..f2c79a2af240 100644 --- a/docs/reference/ml/anomaly-detection/ml-configuring-aggregations.asciidoc +++ b/docs/reference/ml/anomaly-detection/ml-configuring-aggregations.asciidoc @@ -439,3 +439,85 @@ the `error` field. ---------------------------------- // NOTCONSOLE + +[discrete] +[[aggs-amd-dfeeds]] +== Using `aggregate_metric_double` field type in {dfeeds} + + +NOTE: It is not currently possible to use `aggregate_metric_double` type fields +in {dfeeds} without aggregations. + +You can use fields with the +{ref}/aggregate-metric-double.html[`aggregate_metric_double`] field type in a +{dfeed} with aggregations. It is required to retrieve the `value_count` of the +`aggregate_metric_double` filed in an aggregation and then use it as the +`summary_count_field_name` to provide the correct count that represents the +aggregation value. + +In the following example, `presum` is an `aggregate_metric_double` type field +that has all the possible metrics: `[ min, max, sum, value_count ]`. To use an +`avg` aggregation on this field, you need to perform a `value_count` aggregation +on `presum` and then set the field that contains the aggregated values +`my_count` as the `summary_count_field_name`: + + +[source,js] +---------------------------------- +{ + "analysis_config": { + "bucket_span": "1h", + "detectors": [ + { + "function": "avg", + "field_name": "my_avg" + } + ], + "summary_count_field_name": "my_count" <1> + }, + "data_description": { + "time_field": "timestamp" + }, + "datafeed_config": { + "indices": [ + "my_index" + ], + "datafeed_id": "datafeed-id", + "aggregations": { + "buckets": { + "date_histogram": { + "field": "time", + "fixed_interval": "360s", + "time_zone": "UTC" + }, + "aggregations": { + "timestamp": { + "max": {"field": "timestamp"} + }, + "my_avg": { <2> + "avg": { + "field": "presum" + } + }, + "my_count": { <3> + "value_count": { + "field": "presum" + } + } + } + } + } + } +} +---------------------------------- +// NOTCONSOLE + +<1> The field `my_count` is set as the `summary_count_field_name`. This field +contains aggregated values from the `presum` `aggregate_metric_double` type +field (refer to footnote 3). +<2> The `avg` aggregation to use on the `presum` `aggregate_metric_double` type +field. +<3> The `value_count` aggregation on the `presum` `aggregate_metric_double` type +field. This aggregated field must be set as the `summary_count_field_name` +(refer to footnote 1) to make it possible to use the `aggregate_metric_double` +type field in another aggregation. \ No newline at end of file From cb9c9f923d5ae65db83345768bf0966afd18580b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Fern=C3=A1ndez=20Casta=C3=B1o?= Date: Wed, 7 Dec 2022 10:08:46 +0100 Subject: [PATCH 178/919] Update Azure SDK to version 12.20.1 (#92119) --- gradle/verification-metadata.xml | 75 +++++++++++++++++++++++++++ modules/repository-azure/build.gradle | 20 +++---- 2 files changed, 85 insertions(+), 10 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 49b2e15bd85f..6928a0fd8231 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -114,21 +114,41 @@ + + + + + + + + + + + + + + + + + + + + @@ -224,6 +244,11 @@ + + + + + @@ -244,6 +269,11 @@ + + + + + @@ -274,6 +304,11 @@ + + + + + @@ -304,6 +339,11 @@ + + + + + @@ -319,6 +359,11 @@ + + + + + @@ -339,6 +384,11 @@ + + + + + @@ -349,6 +399,11 @@ + + + + + @@ -1334,16 +1389,31 @@ + + + + + + + + + + + + + + + @@ -3669,6 +3739,11 @@ + + + + + diff --git a/modules/repository-azure/build.gradle b/modules/repository-azure/build.gradle index 03decb2eefaf..9d71430efd23 100644 --- a/modules/repository-azure/build.gradle +++ b/modules/repository-azure/build.gradle @@ -23,21 +23,21 @@ esplugin { } versions << [ - 'azure': '12.16.0', - 'azureCommon': '12.15.1', - 'azureCore': '1.27.0', - 'azureCoreHttpNetty': '1.11.9', - 'azureJackson': '2.13.2', - 'azureJacksonDatabind': '2.13.2.2', + 'azure': '12.20.1', + 'azureCommon': '12.19.1', + 'azureCore': '1.34.0', + 'azureCoreHttpNetty': '1.12.7', + 'azureJackson': '2.13.4', + 'azureJacksonDatabind': '2.13.4.2', 'jakartaActivation': '1.2.1', 'jakartaXMLBind': '2.3.2', 'stax2API': '4.2.1', - 'woodstox': '6.2.7', + 'woodstox': '6.4.0', - 'reactorNetty': '1.0.15', - 'reactorCore': '3.4.14', - 'reactiveStreams': '1.0.3', + 'reactorNetty': '1.0.23', + 'reactorCore': '3.4.23', + 'reactiveStreams': '1.0.4', ] dependencies { From 59fcccfd961936e150c911193af682090b834094 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Wed, 7 Dec 2022 10:20:23 +0100 Subject: [PATCH 179/919] Chunked XContent encoding for Metadata (#92169) Mostly straight-forward chunked encoding implementation for Metadata. --- .../elasticsearch/cluster/ClusterState.java | 2 +- .../metadata/IndexTemplateMetadata.java | 4 +- .../cluster/metadata/Metadata.java | 123 +++++++++--------- .../xcontent/ChunkedToXContentHelper.java | 6 +- .../gateway/PersistedClusterStateService.java | 6 +- .../blobstore/BlobStoreRepository.java | 16 ++- .../blobstore/ChecksumBlobStoreFormat.java | 22 +++- .../cluster/metadata/MetadataTests.java | 41 ++++++ .../metadata/ToAndFromJsonMetadataTests.java | 13 +- .../snapshots/BlobStoreFormatTests.java | 10 +- .../elasticsearch/test/ESIntegTestCase.java | 7 +- .../LicensesMetadataSerializationTests.java | 2 +- .../xpack/ilm/PolicyStepsRegistryTests.java | 7 +- .../WatcherMetadataSerializationTests.java | 2 +- 14 files changed, 166 insertions(+), 95 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/ClusterState.java b/server/src/main/java/org/elasticsearch/cluster/ClusterState.java index 0fc23ae86a52..ccae40d38dde 100644 --- a/server/src/main/java/org/elasticsearch/cluster/ClusterState.java +++ b/server/src/main/java/org/elasticsearch/cluster/ClusterState.java @@ -573,7 +573,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws // meta data if (metrics.contains(Metric.METADATA)) { - metadata.toXContent(builder, params); + ChunkedToXContent.wrapAsXContentObject(metadata).toXContent(builder, params); } // routing table diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexTemplateMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexTemplateMetadata.java index e5f37ab537ce..dfc05552d3e7 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexTemplateMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexTemplateMetadata.java @@ -315,14 +315,14 @@ public IndexTemplateMetadata build() { * This method is used for serializing templates before storing them in the cluster metadata, * and also in the REST layer when returning a deprecated typed response. */ - public static void toXContentWithTypes( + public static XContentBuilder toXContentWithTypes( IndexTemplateMetadata indexTemplateMetadata, XContentBuilder builder, ToXContent.Params params ) throws IOException { builder.startObject(indexTemplateMetadata.name()); toInnerXContent(indexTemplateMetadata, builder, params, true); - builder.endObject(); + return builder.endObject(); } /** diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java index 33bd5a0a1fe6..46670b84d42c 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java @@ -27,6 +27,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.collect.ImmutableOpenMap; +import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.VersionedNamedWriteable; @@ -38,6 +39,7 @@ import org.elasticsearch.common.util.Maps; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.common.xcontent.ChunkedToXContent; +import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParserUtils; import org.elasticsearch.core.Nullable; @@ -52,7 +54,6 @@ import org.elasticsearch.xcontent.NamedObjectNotFoundException; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.ToXContent; -import org.elasticsearch.xcontent.ToXContentFragment; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; @@ -74,6 +75,8 @@ import java.util.Optional; import java.util.Set; import java.util.SortedMap; +import java.util.Spliterator; +import java.util.Spliterators; import java.util.TreeMap; import java.util.function.BiPredicate; import java.util.function.Consumer; @@ -81,6 +84,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; +import java.util.stream.StreamSupport; import static org.elasticsearch.cluster.metadata.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; import static org.elasticsearch.common.settings.Settings.readSettingsFromStream; @@ -92,7 +96,7 @@ * The details of how this is persisted are covered in {@link org.elasticsearch.gateway.PersistedClusterStateService}. *