From e9b303322c81acb4c1fc742fa67d2aa5b9f48910 Mon Sep 17 00:00:00 2001
From: Brian Seeders
Date: Thu, 26 Sep 2024 05:11:01 -0400
Subject: [PATCH 01/43] Fix packaging tests after addition of new wolfi-based
image (#112831)
* Add more missing wolfi references to fix tests
* packaging tests require access to docker registry
* Fix symlink for es distributions jdk cacerts in wolfi docker
* Fix native support on wolfi images
* Fix provided keystore packaging tests for wolfi
* Add utils used for testing to wolfi image
* Explicitly set default shell to bash in docker images
* Fix docker config issues
* Apply review feedback around docker login
---------
Co-authored-by: Rene Groeschke
---
.buildkite/hooks/pre-command | 12 ++++++++----
.buildkite/pipelines/periodic-packaging.yml | 3 ++-
.ci/scripts/packaging-test.sh | 1 +
.../gradle/internal/docker/DockerBuildTask.java | 11 +++++++++++
distribution/docker/src/docker/Dockerfile | 13 +++++++++++--
.../elasticsearch/packaging/test/DockerTests.java | 3 +++
.../packaging/test/KeystoreManagementTests.java | 5 ++++-
.../packaging/test/PackagingTestCase.java | 6 ++++--
.../elasticsearch/packaging/util/Distribution.java | 7 +++++--
.../elasticsearch/packaging/util/docker/Docker.java | 4 ++--
.../packaging/util/docker/DockerRun.java | 1 +
11 files changed, 52 insertions(+), 14 deletions(-)
diff --git a/.buildkite/hooks/pre-command b/.buildkite/hooks/pre-command
index b6b730fc3de8b..0c0ede8c3a076 100644
--- a/.buildkite/hooks/pre-command
+++ b/.buildkite/hooks/pre-command
@@ -78,11 +78,15 @@ if [[ "${USE_SNYK_CREDENTIALS:-}" == "true" ]]; then
fi
if [[ "${USE_PROD_DOCKER_CREDENTIALS:-}" == "true" ]]; then
- DOCKER_REGISTRY_USERNAME="$(vault read -field=username secret/ci/elastic-elasticsearch/migrated/prod_docker_registry_credentials)"
- export DOCKER_REGISTRY_USERNAME
+ if which docker > /dev/null 2>&1; then
+ DOCKER_REGISTRY_USERNAME="$(vault read -field=username secret/ci/elastic-elasticsearch/migrated/prod_docker_registry_credentials)"
+ export DOCKER_REGISTRY_USERNAME
- DOCKER_REGISTRY_PASSWORD="$(vault read -field=password secret/ci/elastic-elasticsearch/migrated/prod_docker_registry_credentials)"
- export DOCKER_REGISTRY_PASSWORD
+ DOCKER_REGISTRY_PASSWORD="$(vault read -field=password secret/ci/elastic-elasticsearch/migrated/prod_docker_registry_credentials)"
+ export DOCKER_REGISTRY_PASSWORD
+
+ docker login --username "$DOCKER_REGISTRY_USERNAME" --password "$DOCKER_REGISTRY_PASSWORD" docker.elastic.co
+ fi
fi
if [[ "$BUILDKITE_AGENT_META_DATA_PROVIDER" != *"k8s"* ]]; then
diff --git a/.buildkite/pipelines/periodic-packaging.yml b/.buildkite/pipelines/periodic-packaging.yml
index 8ef8f5954887e..76cc543a6898e 100644
--- a/.buildkite/pipelines/periodic-packaging.yml
+++ b/.buildkite/pipelines/periodic-packaging.yml
@@ -30,7 +30,8 @@ steps:
image: family/elasticsearch-{{matrix.image}}
diskSizeGb: 350
machineType: n1-standard-8
- env: {}
+ env:
+ USE_PROD_DOCKER_CREDENTIALS: "true"
- group: packaging-tests-upgrade
steps:
- label: "{{matrix.image}} / 8.0.1 / packaging-tests-upgrade"
diff --git a/.ci/scripts/packaging-test.sh b/.ci/scripts/packaging-test.sh
index 6b9938dabffa8..bb7547933b213 100755
--- a/.ci/scripts/packaging-test.sh
+++ b/.ci/scripts/packaging-test.sh
@@ -77,5 +77,6 @@ sudo -E env \
--unset=ES_JAVA_HOME \
--unset=JAVA_HOME \
SYSTEM_JAVA_HOME=`readlink -f -n $BUILD_JAVA_HOME` \
+ DOCKER_CONFIG="${HOME}/.docker" \
./gradlew -g $HOME/.gradle --scan --parallel --build-cache -Dorg.elasticsearch.build.cache.url=https://gradle-enterprise.elastic.co/cache/ --continue $@
diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/docker/DockerBuildTask.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/docker/DockerBuildTask.java
index 8971f27838578..9b28401994ee2 100644
--- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/docker/DockerBuildTask.java
+++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/docker/DockerBuildTask.java
@@ -30,6 +30,7 @@
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.TaskAction;
import org.gradle.process.ExecOperations;
+import org.gradle.process.ExecSpec;
import org.gradle.workers.WorkAction;
import org.gradle.workers.WorkParameters;
import org.gradle.workers.WorkerExecutor;
@@ -166,6 +167,7 @@ private void pullBaseImage(String baseImage) {
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
LoggedExec.exec(execOperations, spec -> {
+ maybeConfigureDockerConfig(spec);
spec.executable("docker");
spec.args("pull");
spec.args(baseImage);
@@ -181,6 +183,13 @@ private void pullBaseImage(String baseImage) {
throw new GradleException("Failed to pull Docker base image [" + baseImage + "], all attempts failed");
}
+ private void maybeConfigureDockerConfig(ExecSpec spec) {
+ String dockerConfig = System.getenv("DOCKER_CONFIG");
+ if (dockerConfig != null) {
+ spec.environment("DOCKER_CONFIG", dockerConfig);
+ }
+ }
+
@Override
public void execute() {
final Parameters parameters = getParameters();
@@ -193,6 +202,8 @@ public void execute() {
final boolean isCrossPlatform = isCrossPlatform();
LoggedExec.exec(execOperations, spec -> {
+ maybeConfigureDockerConfig(spec);
+
spec.executable("docker");
if (isCrossPlatform) {
diff --git a/distribution/docker/src/docker/Dockerfile b/distribution/docker/src/docker/Dockerfile
index 47f79749cbefa..fd2516f2fdc9a 100644
--- a/distribution/docker/src/docker/Dockerfile
+++ b/distribution/docker/src/docker/Dockerfile
@@ -163,9 +163,16 @@ RUN <%= retry.loop(package_manager,
" ${package_manager} update && \n" +
" ${package_manager} upgrade && \n" +
" ${package_manager} add --no-cache \n" +
- " bash ca-certificates curl libsystemd netcat-openbsd p11-kit p11-kit-trust shadow tini unzip zip zstd && \n" +
+ " bash java-cacerts curl libstdc++ libsystemd netcat-openbsd p11-kit p11-kit-trust posix-libc-utils shadow tini unzip zip zstd && \n" +
" rm -rf /var/cache/apk/* "
) %>
+
+# Set Bash as the default shell for future commands
+SHELL ["/bin/bash", "-c"]
+
+# Optionally set Bash as the default shell in the container at runtime
+CMD ["/bin/bash"]
+
<% } else if (docker_base == "default" || docker_base == "cloud") { %>
# Change default shell to bash, then install required packages with retries.
@@ -224,7 +231,7 @@ COPY --from=builder --chown=0:0 /opt /opt
<% } %>
ENV PATH /usr/share/elasticsearch/bin:\$PATH
-
+ENV SHELL /bin/bash
COPY ${bin_dir}/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
# 1. Sync the user and group permissions of /etc/passwd
@@ -249,6 +256,8 @@ RUN chmod g=u /etc/passwd && \\
# stays up-to-date with changes to Ubuntu's store)
COPY bin/docker-openjdk /etc/ca-certificates/update.d/docker-openjdk
RUN /etc/ca-certificates/update.d/docker-openjdk
+<% } else if (docker_base == 'wolfi') { %>
+RUN ln -sf /etc/ssl/certs/java/cacerts /usr/share/elasticsearch/jdk/lib/security/cacerts
<% } else { %>
RUN ln -sf /etc/pki/ca-trust/extracted/java/cacerts /usr/share/elasticsearch/jdk/lib/security/cacerts
<% } %>
diff --git a/qa/packaging/src/test/java/org/elasticsearch/packaging/test/DockerTests.java b/qa/packaging/src/test/java/org/elasticsearch/packaging/test/DockerTests.java
index a9402c324f7fc..f588b78c78cc8 100644
--- a/qa/packaging/src/test/java/org/elasticsearch/packaging/test/DockerTests.java
+++ b/qa/packaging/src/test/java/org/elasticsearch/packaging/test/DockerTests.java
@@ -386,6 +386,9 @@ public void test040JavaUsesTheOsProvidedKeystore() {
if (distribution.packaging == Packaging.DOCKER_UBI || distribution.packaging == Packaging.DOCKER_IRON_BANK) {
// In these images, the `cacerts` file ought to be a symlink here
assertThat(path, equalTo("/etc/pki/ca-trust/extracted/java/cacerts"));
+ } else if (distribution.packaging == Packaging.DOCKER_WOLFI) {
+ // In these images, the `cacerts` file ought to be a symlink here
+ assertThat(path, equalTo("/etc/ssl/certs/java/cacerts"));
} else {
// Whereas on other images, it's a real file so the real path is the same
assertThat(path, equalTo("/usr/share/elasticsearch/jdk/lib/security/cacerts"));
diff --git a/qa/packaging/src/test/java/org/elasticsearch/packaging/test/KeystoreManagementTests.java b/qa/packaging/src/test/java/org/elasticsearch/packaging/test/KeystoreManagementTests.java
index 5b86796aa80ca..a988a446f561f 100644
--- a/qa/packaging/src/test/java/org/elasticsearch/packaging/test/KeystoreManagementTests.java
+++ b/qa/packaging/src/test/java/org/elasticsearch/packaging/test/KeystoreManagementTests.java
@@ -436,7 +436,10 @@ private void verifyKeystorePermissions() {
switch (distribution.packaging) {
case TAR, ZIP -> assertThat(keystore, file(File, ARCHIVE_OWNER, ARCHIVE_OWNER, p660));
case DEB, RPM -> assertThat(keystore, file(File, "root", "elasticsearch", p660));
- case DOCKER, DOCKER_UBI, DOCKER_IRON_BANK, DOCKER_CLOUD, DOCKER_CLOUD_ESS -> assertThat(keystore, DockerFileMatcher.file(p660));
+ case DOCKER, DOCKER_UBI, DOCKER_IRON_BANK, DOCKER_CLOUD, DOCKER_CLOUD_ESS, DOCKER_WOLFI -> assertThat(
+ keystore,
+ DockerFileMatcher.file(p660)
+ );
default -> throw new IllegalStateException("Unknown Elasticsearch packaging type.");
}
}
diff --git a/qa/packaging/src/test/java/org/elasticsearch/packaging/test/PackagingTestCase.java b/qa/packaging/src/test/java/org/elasticsearch/packaging/test/PackagingTestCase.java
index a1a9af3b6e307..644990105f60f 100644
--- a/qa/packaging/src/test/java/org/elasticsearch/packaging/test/PackagingTestCase.java
+++ b/qa/packaging/src/test/java/org/elasticsearch/packaging/test/PackagingTestCase.java
@@ -245,7 +245,7 @@ protected static void install() throws Exception {
installation = Packages.installPackage(sh, distribution);
Packages.verifyPackageInstallation(installation, distribution, sh);
}
- case DOCKER, DOCKER_UBI, DOCKER_IRON_BANK, DOCKER_CLOUD, DOCKER_CLOUD_ESS -> {
+ case DOCKER, DOCKER_UBI, DOCKER_IRON_BANK, DOCKER_CLOUD, DOCKER_CLOUD_ESS, DOCKER_WOLFI -> {
installation = Docker.runContainer(distribution);
Docker.verifyContainerInstallation(installation);
}
@@ -337,6 +337,7 @@ public Shell.Result runElasticsearchStartCommand(String password, boolean daemon
case DOCKER_IRON_BANK:
case DOCKER_CLOUD:
case DOCKER_CLOUD_ESS:
+ case DOCKER_WOLFI:
// nothing, "installing" docker image is running it
return Shell.NO_OP;
default:
@@ -359,6 +360,7 @@ public void stopElasticsearch() throws Exception {
case DOCKER_IRON_BANK:
case DOCKER_CLOUD:
case DOCKER_CLOUD_ESS:
+ case DOCKER_WOLFI:
// nothing, "installing" docker image is running it
break;
default:
@@ -371,7 +373,7 @@ public void awaitElasticsearchStartup(Shell.Result result) throws Exception {
switch (distribution.packaging) {
case TAR, ZIP -> Archives.assertElasticsearchStarted(installation);
case DEB, RPM -> Packages.assertElasticsearchStarted(sh, installation);
- case DOCKER, DOCKER_UBI, DOCKER_IRON_BANK, DOCKER_CLOUD, DOCKER_CLOUD_ESS -> Docker.waitForElasticsearchToStart();
+ case DOCKER, DOCKER_UBI, DOCKER_IRON_BANK, DOCKER_CLOUD, DOCKER_CLOUD_ESS, DOCKER_WOLFI -> Docker.waitForElasticsearchToStart();
default -> throw new IllegalStateException("Unknown Elasticsearch packaging type.");
}
}
diff --git a/qa/packaging/src/test/java/org/elasticsearch/packaging/util/Distribution.java b/qa/packaging/src/test/java/org/elasticsearch/packaging/util/Distribution.java
index b3ea54425af8e..05cef4a0818ba 100644
--- a/qa/packaging/src/test/java/org/elasticsearch/packaging/util/Distribution.java
+++ b/qa/packaging/src/test/java/org/elasticsearch/packaging/util/Distribution.java
@@ -37,6 +37,8 @@ public Distribution(Path path) {
this.packaging = Packaging.DOCKER_CLOUD;
} else if (filename.endsWith(".cloud-ess.tar")) {
this.packaging = Packaging.DOCKER_CLOUD_ESS;
+ } else if (filename.endsWith(".wolfi.tar")) {
+ this.packaging = Packaging.DOCKER_WOLFI;
} else {
int lastDot = filename.lastIndexOf('.');
this.packaging = Packaging.valueOf(filename.substring(lastDot + 1).toUpperCase(Locale.ROOT));
@@ -61,7 +63,7 @@ public boolean isPackage() {
*/
public boolean isDocker() {
return switch (packaging) {
- case DOCKER, DOCKER_UBI, DOCKER_IRON_BANK, DOCKER_CLOUD, DOCKER_CLOUD_ESS -> true;
+ case DOCKER, DOCKER_UBI, DOCKER_IRON_BANK, DOCKER_CLOUD, DOCKER_CLOUD_ESS, DOCKER_WOLFI -> true;
default -> false;
};
}
@@ -76,7 +78,8 @@ public enum Packaging {
DOCKER_UBI(".ubi.tar", Platforms.isDocker()),
DOCKER_IRON_BANK(".ironbank.tar", Platforms.isDocker()),
DOCKER_CLOUD(".cloud.tar", Platforms.isDocker()),
- DOCKER_CLOUD_ESS(".cloud-ess.tar", Platforms.isDocker());
+ DOCKER_CLOUD_ESS(".cloud-ess.tar", Platforms.isDocker()),
+ DOCKER_WOLFI(".wolfi.tar", Platforms.isDocker());
/** The extension of this distribution's file */
public final String extension;
diff --git a/qa/packaging/src/test/java/org/elasticsearch/packaging/util/docker/Docker.java b/qa/packaging/src/test/java/org/elasticsearch/packaging/util/docker/Docker.java
index cb8a955a5972c..c38eaa58f0552 100644
--- a/qa/packaging/src/test/java/org/elasticsearch/packaging/util/docker/Docker.java
+++ b/qa/packaging/src/test/java/org/elasticsearch/packaging/util/docker/Docker.java
@@ -486,9 +486,9 @@ public static void verifyContainerInstallation(Installation es) {
// Ensure the `elasticsearch` user and group exist.
// These lines will both throw an exception if the command fails
dockerShell.run("id elasticsearch");
- dockerShell.run("getent group elasticsearch");
+ dockerShell.run("grep -E '^elasticsearch:' /etc/group");
- final Shell.Result passwdResult = dockerShell.run("getent passwd elasticsearch");
+ final Shell.Result passwdResult = dockerShell.run("grep -E '^elasticsearch:' /etc/passwd");
final String homeDir = passwdResult.stdout().trim().split(":")[5];
assertThat("elasticsearch user's home directory is incorrect", homeDir, equalTo("/usr/share/elasticsearch"));
diff --git a/qa/packaging/src/test/java/org/elasticsearch/packaging/util/docker/DockerRun.java b/qa/packaging/src/test/java/org/elasticsearch/packaging/util/docker/DockerRun.java
index 6c58bcba09879..2b3eb7ff7a617 100644
--- a/qa/packaging/src/test/java/org/elasticsearch/packaging/util/docker/DockerRun.java
+++ b/qa/packaging/src/test/java/org/elasticsearch/packaging/util/docker/DockerRun.java
@@ -167,6 +167,7 @@ public static String getImageName(Distribution distribution) {
case DOCKER_IRON_BANK -> "-ironbank";
case DOCKER_CLOUD -> "-cloud";
case DOCKER_CLOUD_ESS -> "-cloud-ess";
+ case DOCKER_WOLFI -> "-wolfi";
default -> throw new IllegalStateException("Unexpected distribution packaging type: " + distribution.packaging);
};
From 11c0bf8d6ee4bbd6e982a04211cc3994c5dd48e0 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Thu, 26 Sep 2024 11:19:46 +0200
Subject: [PATCH 02/43] Fix cancellation race condition in
`onIndexAvailableForSearch` (#113386)
This PR fixes following race conditions in `onIndexAvailableForSearch`
introduced in https://github.com/elastic/elasticsearch/pull/112813:
1. If the method is called when the index is already available, cancellation is still scheduled and may execute before successful completion (manifested in test failures https://github.com/elastic/elasticsearch/issues/113336)
2. If the cancel task runs _before_ `addStateListener`, it may fail to remove the listener (noticed while fixing the first issue)
These race conditions only manifest for small timeout windows, and are
completely bypassed for 0 timeout windows based on other checks in prod
code, so the practical impact is fortunately limited.
Resolves: https://github.com/elastic/elasticsearch/issues/113336
---
muted-tests.yml | 3 -
.../SecurityIndexManagerIntegTests.java | 110 +++++++++++++++++-
.../support/SecurityIndexManager.java | 86 ++++++++++----
3 files changed, 168 insertions(+), 31 deletions(-)
diff --git a/muted-tests.yml b/muted-tests.yml
index 602d790246648..728f3d0bd6c72 100644
--- a/muted-tests.yml
+++ b/muted-tests.yml
@@ -251,9 +251,6 @@ tests:
- class: org.elasticsearch.xpack.test.rest.XPackRestIT
method: test {p0=transform/transforms_force_delete/Test force deleting a running transform}
issue: https://github.com/elastic/elasticsearch/issues/113327
-- class: org.elasticsearch.xpack.security.support.SecurityIndexManagerIntegTests
- method: testOnIndexAvailableForSearchIndexAlreadyAvailable
- issue: https://github.com/elastic/elasticsearch/issues/113336
- class: org.elasticsearch.xpack.test.rest.XPackRestIT
method: test {p0=analytics/top_metrics/sort by scaled float field}
issue: https://github.com/elastic/elasticsearch/issues/113340
diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/support/SecurityIndexManagerIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/support/SecurityIndexManagerIntegTests.java
index 32337f0d66896..44cbf03f220a1 100644
--- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/support/SecurityIndexManagerIntegTests.java
+++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/support/SecurityIndexManagerIntegTests.java
@@ -26,18 +26,24 @@
import org.elasticsearch.xpack.core.security.action.user.PutUserResponse;
import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore;
import org.hamcrest.Matchers;
+import org.junit.After;
import org.junit.Before;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_MAIN_ALIAS;
import static org.hamcrest.Matchers.arrayContaining;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
@@ -45,6 +51,14 @@
public class SecurityIndexManagerIntegTests extends SecurityIntegTestCase {
+ private final int concurrentCallsToOnAvailable = 6;
+ private final ExecutorService executor = Executors.newFixedThreadPool(concurrentCallsToOnAvailable);
+
+ @After
+ public void shutdownExecutor() {
+ executor.shutdown();
+ }
+
public void testConcurrentOperationsTryingToCreateSecurityIndexAndAlias() throws Exception {
final int processors = Runtime.getRuntime().availableProcessors();
final int numThreads = Math.min(50, scaledRandomIntBetween((processors + 1) / 2, 4 * processors)); // up to 50 threads
@@ -110,6 +124,12 @@ public void testOnIndexAvailableForSearchIndexCompletesWithinTimeout() throws Ex
// pick longer wait than in the assertBusy that waits for below to ensure index has had enough time to initialize
securityIndexManager.onIndexAvailableForSearch((ActionListener) future, TimeValue.timeValueSeconds(40));
+ // check listener added
+ assertThat(
+ securityIndexManager.getStateChangeListeners(),
+ hasItem(instanceOf(SecurityIndexManager.StateConsumerWithCancellable.class))
+ );
+
createSecurityIndexWithWaitForActiveShards();
assertBusy(
@@ -121,6 +141,12 @@ public void testOnIndexAvailableForSearchIndexCompletesWithinTimeout() throws Ex
// security index creation is complete and index is available for search; therefore whenIndexAvailableForSearch should report
// success in time
future.actionGet();
+
+ // check no remaining listeners
+ assertThat(
+ securityIndexManager.getStateChangeListeners(),
+ not(hasItem(instanceOf(SecurityIndexManager.StateConsumerWithCancellable.class)))
+ );
}
@SuppressWarnings("unchecked")
@@ -152,6 +178,69 @@ public void testOnIndexAvailableForSearchIndexAlreadyAvailable() throws Exceptio
securityIndexManager.onIndexAvailableForSearch((ActionListener) future, TimeValue.timeValueSeconds(10));
future.actionGet();
}
+
+ // check no remaining listeners
+ assertThat(
+ securityIndexManager.getStateChangeListeners(),
+ not(hasItem(instanceOf(SecurityIndexManager.StateConsumerWithCancellable.class)))
+ );
+ }
+
+ @SuppressWarnings("unchecked")
+ public void testOnIndexAvailableForSearchIndexUnderConcurrentLoad() throws Exception {
+ final SecurityIndexManager securityIndexManager = internalCluster().getInstances(NativePrivilegeStore.class)
+ .iterator()
+ .next()
+ .getSecurityIndexManager();
+ // Long time out calls should all succeed
+ final List> futures = new ArrayList<>();
+ for (int i = 0; i < concurrentCallsToOnAvailable / 2; i++) {
+ final Future future = executor.submit(() -> {
+ try {
+ final ActionFuture f = new PlainActionFuture<>();
+ securityIndexManager.onIndexAvailableForSearch((ActionListener) f, TimeValue.timeValueSeconds(40));
+ f.actionGet();
+ } catch (Exception ex) {
+ fail(ex, "should not have encountered exception");
+ }
+ return null;
+ });
+ futures.add(future);
+ }
+
+ // short time-out tasks should all time out
+ for (int i = 0; i < concurrentCallsToOnAvailable / 2; i++) {
+ final Future future = executor.submit(() -> {
+ expectThrows(ElasticsearchTimeoutException.class, () -> {
+ final ActionFuture f = new PlainActionFuture<>();
+ securityIndexManager.onIndexAvailableForSearch((ActionListener) f, TimeValue.timeValueMillis(10));
+ f.actionGet();
+ });
+ return null;
+ });
+ futures.add(future);
+ }
+
+ // Sleep a second for short-running calls to timeout
+ Thread.sleep(1000);
+
+ createSecurityIndexWithWaitForActiveShards();
+ // ensure security index manager state is fully in the expected precondition state for this test (ready for search)
+ assertBusy(
+ () -> assertThat(securityIndexManager.isAvailable(SecurityIndexManager.Availability.SEARCH_SHARDS), is(true)),
+ 30,
+ TimeUnit.SECONDS
+ );
+
+ for (var future : futures) {
+ future.get(10, TimeUnit.SECONDS);
+ }
+
+ // check no remaining listeners
+ assertThat(
+ securityIndexManager.getStateChangeListeners(),
+ not(hasItem(instanceOf(SecurityIndexManager.StateConsumerWithCancellable.class)))
+ );
}
@SuppressWarnings("unchecked")
@@ -163,9 +252,24 @@ public void testOnIndexAvailableForSearchIndexWaitTimeOut() {
.next()
.getSecurityIndexManager();
- final ActionFuture future = new PlainActionFuture<>();
- securityIndexManager.onIndexAvailableForSearch((ActionListener) future, TimeValue.timeValueMillis(100));
- expectThrows(ElasticsearchTimeoutException.class, future::actionGet);
+ {
+ final ActionFuture future = new PlainActionFuture<>();
+ securityIndexManager.onIndexAvailableForSearch((ActionListener) future, TimeValue.timeValueMillis(100));
+ expectThrows(ElasticsearchTimeoutException.class, future::actionGet);
+ }
+
+ // Also works with 0 timeout
+ {
+ final ActionFuture future = new PlainActionFuture<>();
+ securityIndexManager.onIndexAvailableForSearch((ActionListener) future, TimeValue.timeValueMillis(0));
+ expectThrows(ElasticsearchTimeoutException.class, future::actionGet);
+ }
+
+ // check no remaining listeners
+ assertThat(
+ securityIndexManager.getStateChangeListeners(),
+ not(hasItem(instanceOf(SecurityIndexManager.StateConsumerWithCancellable.class)))
+ );
}
public void testSecurityIndexSettingsCannotBeChanged() throws Exception {
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java
index a6c8de003c159..6d9b0ef6aeebe 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java
@@ -54,6 +54,7 @@
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -364,45 +365,80 @@ public void accept(State previousState, State nextState) {
* Notifies {@code listener} once the security index is available, or calls {@code onFailure} on {@code timeout}.
*/
public void onIndexAvailableForSearch(ActionListener listener, TimeValue timeout) {
- logger.info("Will wait for security index [{}] to become available for search", getConcreteIndexName());
+ logger.info("Will wait for security index [{}] for [{}] to become available for search", getConcreteIndexName(), timeout);
- final ActionListener notifyOnceListener = ActionListener.notifyOnce(listener);
+ if (state.indexAvailableForSearch) {
+ logger.debug("Security index [{}] is already available", getConcreteIndexName());
+ listener.onResponse(null);
+ return;
+ }
+ final AtomicBoolean isDone = new AtomicBoolean(false);
final var indexAvailableForSearchListener = new StateConsumerWithCancellable() {
@Override
public void accept(SecurityIndexManager.State previousState, SecurityIndexManager.State nextState) {
if (nextState.indexAvailableForSearch) {
- assert cancellable != null;
- // cancel and removeStateListener are idempotent
- cancellable.cancel();
- removeStateListener(this);
- notifyOnceListener.onResponse(null);
+ if (isDone.compareAndSet(false, true)) {
+ cancel();
+ removeStateListener(this);
+ listener.onResponse(null);
+ }
}
}
};
+ // add listener _before_ registering timeout -- this way we are guaranteed it gets removed (either by timeout below, or successful
+ // completion above)
+ addStateListener(indexAvailableForSearchListener);
+
// schedule failure handling on timeout -- keep reference to cancellable so a successful completion can cancel the timeout
- indexAvailableForSearchListener.cancellable = client.threadPool().schedule(() -> {
- removeStateListener(indexAvailableForSearchListener);
- notifyOnceListener.onFailure(
- new ElasticsearchTimeoutException(
- "timed out waiting for security index [" + getConcreteIndexName() + "] to become available for search"
- )
- );
- }, timeout, client.threadPool().generic());
+ indexAvailableForSearchListener.setCancellable(client.threadPool().schedule(() -> {
+ if (isDone.compareAndSet(false, true)) {
+ removeStateListener(indexAvailableForSearchListener);
+ listener.onFailure(
+ new ElasticsearchTimeoutException(
+ "timed out waiting for security index [" + getConcreteIndexName() + "] to become available for search"
+ )
+ );
+ }
+ }, timeout, client.threadPool().generic()));
+ }
- // in case the state has meanwhile changed to available, return immediately
- if (state.indexAvailableForSearch) {
- indexAvailableForSearchListener.cancellable.cancel();
- notifyOnceListener.onResponse(null);
- } else {
- addStateListener(indexAvailableForSearchListener);
- }
+ // pkg-private for testing
+ List> getStateChangeListeners() {
+ return stateChangeListeners;
}
- private abstract static class StateConsumerWithCancellable
+ /**
+ * This class ensures that if cancel() is called _before_ setCancellable(), the passed-in cancellable is still correctly cancelled on
+ * a subsequent setCancellable() call.
+ */
+ // pkg-private for testing
+ abstract static class StateConsumerWithCancellable
implements
- BiConsumer {
- volatile Scheduler.ScheduledCancellable cancellable;
+ BiConsumer,
+ Scheduler.Cancellable {
+ private volatile Scheduler.ScheduledCancellable cancellable;
+ private volatile boolean cancelled = false;
+
+ void setCancellable(Scheduler.ScheduledCancellable cancellable) {
+ this.cancellable = cancellable;
+ if (cancelled) {
+ cancel();
+ }
+ }
+
+ public boolean cancel() {
+ cancelled = true;
+ if (cancellable != null) {
+ // cancellable is idempotent, so it's fine to potentially call it multiple times
+ return cancellable.cancel();
+ }
+ return isCancelled();
+ }
+
+ public boolean isCancelled() {
+ return cancelled;
+ }
}
private Tuple checkIndexAvailable(ClusterState state) {
From 0d275c65dca4aeba90bd8ed632644a433c817408 Mon Sep 17 00:00:00 2001
From: Nikolaj Volgushev
Date: Thu, 26 Sep 2024 12:06:28 +0200
Subject: [PATCH 03/43] Log when clients are randomly reset in ITs (#113510)
This is useful for debugging tests and should not be too noisy since
it's a rare event.
---
.../main/java/org/elasticsearch/test/InternalTestCluster.java | 2 ++
1 file changed, 2 insertions(+)
diff --git a/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java b/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java
index ff66d59a21c5b..7a04384298933 100644
--- a/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java
+++ b/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java
@@ -1536,7 +1536,9 @@ private void randomlyResetClients() {
// only reset the clients on nightly tests, it causes heavy load...
if (RandomizedTest.isNightly() && rarely(random)) {
final Collection nodesAndClients = nodes.values();
+ logger.info("Resetting [{}] node clients on internal test cluster", nodesAndClients.size());
for (NodeAndClient nodeAndClient : nodesAndClients) {
+ logger.info("Resetting [{}] node client on internal test cluster", nodeAndClient.name);
nodeAndClient.resetClient();
}
}
From 98db01b271680732a0862db1a07f6677b49672c3 Mon Sep 17 00:00:00 2001
From: Luigi Dell'Aquila
Date: Thu, 26 Sep 2024 12:22:49 +0200
Subject: [PATCH 04/43] ES|QL: Fix warnings for date tests (#113586)
Fixes: https://github.com/elastic/elasticsearch/issues/113540 Fixes:
https://github.com/elastic/elasticsearch/issues/113539
More generic warning regex for Java 23 date patterns
(already fixed in 8.x, no need to backport)
---
muted-tests.yml | 6 ------
.../resources/rest-api-spec/test/esql/70_locale.yml | 4 ++--
2 files changed, 2 insertions(+), 8 deletions(-)
diff --git a/muted-tests.yml b/muted-tests.yml
index 728f3d0bd6c72..cf821355b1d3c 100644
--- a/muted-tests.yml
+++ b/muted-tests.yml
@@ -290,12 +290,6 @@ tests:
- class: org.elasticsearch.backwards.MixedClusterClientYamlTestSuiteIT
method: test {p0=search/180_locale_dependent_mapping/Test Index and Search locale dependent mappings / dates}
issue: https://github.com/elastic/elasticsearch/issues/113537
-- class: org.elasticsearch.xpack.esql.qa.mixed.EsqlClientYamlIT
- method: test {p0=esql/70_locale/Date format with default locale}
- issue: https://github.com/elastic/elasticsearch/issues/113539
-- class: org.elasticsearch.xpack.esql.qa.mixed.EsqlClientYamlIT
- method: test {p0=esql/70_locale/Date format with Italian locale}
- issue: https://github.com/elastic/elasticsearch/issues/113540
- class: org.elasticsearch.xpack.inference.TextEmbeddingCrudIT
method: testPutE5WithTrainedModelAndInference
issue: https://github.com/elastic/elasticsearch/issues/113565
diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/70_locale.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/70_locale.yml
index 05edf6cdfb5a8..5a9a2a21e21bc 100644
--- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/70_locale.yml
+++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/70_locale.yml
@@ -29,7 +29,7 @@ setup:
- do:
allowed_warnings_regex:
- "No limit defined, adding default limit of \\[.*\\]"
- - "Date format \\[MMMM\\] contains textual field specifiers that could change in JDK 23"
+ - "Date format \\[MMMM\\] contains textual field specifiers that could change in JDK 23.*"
esql.query:
body:
query: 'FROM events | eval fixed_format = date_format("MMMM", @timestamp), variable_format = date_format(format, @timestamp) | sort @timestamp | keep @timestamp, fixed_format, variable_format'
@@ -51,7 +51,7 @@ setup:
- do:
allowed_warnings_regex:
- "No limit defined, adding default limit of \\[.*\\]"
- - "Date format \\[MMMM\\] contains textual field specifiers that could change in JDK 23"
+ - "Date format \\[MMMM\\] contains textual field specifiers that could change in JDK 23.*"
esql.query:
body:
query: 'FROM events | eval fixed_format = date_format("MMMM", @timestamp), variable_format = date_format(format, @timestamp) | sort @timestamp | keep @timestamp, fixed_format, variable_format'
From b344493298a0db19f9e95d8252fe90e79102dd5a Mon Sep 17 00:00:00 2001
From: Kostas Krikellas <131142368+kkrik-es@users.noreply.github.com>
Date: Thu, 26 Sep 2024 14:57:31 +0300
Subject: [PATCH 05/43] Rest skipped tests after backporting (#113591)
The skip test entries were added in
https://github.com/elastic/elasticsearch/pull/113584, no longer needed
after backporting it.
---
modules/dot-prefix-validation/build.gradle | 10 ----------
rest-api-spec/build.gradle | 22 ----------------------
2 files changed, 32 deletions(-)
diff --git a/modules/dot-prefix-validation/build.gradle b/modules/dot-prefix-validation/build.gradle
index b300cae20d717..6e232570b4a22 100644
--- a/modules/dot-prefix-validation/build.gradle
+++ b/modules/dot-prefix-validation/build.gradle
@@ -27,13 +27,3 @@ tasks.named('yamlRestTest') {
tasks.named('yamlRestCompatTest') {
usesDefaultDistribution()
}
-
-tasks.named("yamlRestCompatTestTransform").configure(
- { task ->
- task.skipTest("tsdb/140_routing_path/multi-value routing path field", "Multi-value routing paths are allowed now. See #112645")
- task.skipTest(
- "dot_prefix/10_basic/Deprecated index template with a dot prefix index pattern",
- "Tentantively disabled until #112092 gets backported to 8.x"
- )
- }
-)
diff --git a/rest-api-spec/build.gradle b/rest-api-spec/build.gradle
index 1d69c170d7553..a742e83255bbb 100644
--- a/rest-api-spec/build.gradle
+++ b/rest-api-spec/build.gradle
@@ -55,28 +55,6 @@ tasks.named("precommit").configure {
}
tasks.named("yamlRestCompatTestTransform").configure({task ->
- task.skipTest("tsdb/140_routing_path/multi-value routing path field", "Multi-value routing paths are allowed now. See #112645")
task.skipTest("indices.sort/10_basic/Index Sort", "warning does not exist for compatibility")
task.skipTest("search/330_fetch_fields/Test search rewrite", "warning does not exist for compatibility")
- task.skipTest("search/540_ignore_above_synthetic_source/ignore_above mapping level setting on arrays", "Temporary mute while backporting to 8.x")
- task.skipTest("indices.create/20_synthetic_source/subobjects auto", "Tentantively disabled until #112092 gets backported to 8.x")
- task.skipTest(
- "index/92_metrics_auto_subobjects/Metrics object indexing with synthetic source",
- "Tentantively disabled until #112092 gets backported to 8.x"
- )
- task.skipTest(
- "index/92_metrics_auto_subobjects/Root without subobjects with synthetic source",
- "Tentantively disabled until #112092 gets backported to 8.x"
- )
- task.skipTest(
- "indices.put_index_template/15_composition/Composable index templates that include subobjects: auto at root",
- "Tentantively disabled until #112092 gets backported to 8.x"
- )
- task.skipTest(
- "indices.put_index_template/15_composition/Composable index templates that include subobjects: auto on arbitrary field",
- "Tentantively disabled until #112092 gets backported to 8.x"
- )
- task.skipTest("index/92_metrics_auto_subobjects/Metrics object indexing", "Tentantively disabled until #112092 gets backported to 8.x")
- task.skipTest("index/92_metrics_auto_subobjects/Root with metrics", "Tentantively disabled until #112092 gets backported to 8.x")
- task.skipTest("search/330_fetch_fields/Test with subobjects: auto", "Tentantively disabled until #112092 gets backported to 8.x")
})
From 5c6778cc8632950a50da4f34151bd0403c806dc6 Mon Sep 17 00:00:00 2001
From: elasticsearchmachine
<58790826+elasticsearchmachine@users.noreply.github.com>
Date: Thu, 26 Sep 2024 22:26:40 +1000
Subject: [PATCH 06/43] Mute
org.elasticsearch.integration.KibanaUserRoleIntegTests testFieldMappings
#113592
---
muted-tests.yml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/muted-tests.yml b/muted-tests.yml
index cf821355b1d3c..528f1e3be8d17 100644
--- a/muted-tests.yml
+++ b/muted-tests.yml
@@ -299,6 +299,9 @@ tests:
- class: org.elasticsearch.xpack.ml.integration.MlJobIT
method: testCantCreateJobWithSameID
issue: https://github.com/elastic/elasticsearch/issues/113581
+- class: org.elasticsearch.integration.KibanaUserRoleIntegTests
+ method: testFieldMappings
+ issue: https://github.com/elastic/elasticsearch/issues/113592
# Examples:
#
From fc9954e031e3f94fca58765755fbddae93115656 Mon Sep 17 00:00:00 2001
From: elasticsearchmachine
<58790826+elasticsearchmachine@users.noreply.github.com>
Date: Thu, 26 Sep 2024 22:26:58 +1000
Subject: [PATCH 07/43] Mute
org.elasticsearch.integration.KibanaUserRoleIntegTests testSearchAndMSearch
#113593
---
muted-tests.yml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/muted-tests.yml b/muted-tests.yml
index 528f1e3be8d17..5d7474af06d86 100644
--- a/muted-tests.yml
+++ b/muted-tests.yml
@@ -302,6 +302,9 @@ tests:
- class: org.elasticsearch.integration.KibanaUserRoleIntegTests
method: testFieldMappings
issue: https://github.com/elastic/elasticsearch/issues/113592
+- class: org.elasticsearch.integration.KibanaUserRoleIntegTests
+ method: testSearchAndMSearch
+ issue: https://github.com/elastic/elasticsearch/issues/113593
# Examples:
#
From e2281a1158976bd5fac73c8e77e9a67c769dd3b2 Mon Sep 17 00:00:00 2001
From: Salvatore Campagna
<93581129+salvatore-campagna@users.noreply.github.com>
Date: Thu, 26 Sep 2024 14:44:03 +0200
Subject: [PATCH 08/43] Introduce an `IndexSettingsProvider` to inject logsdb
index mode (#113505)
Here we introduce a new implementation of `IndexSettingProvider` whose goal is to "inject" the
`index.mode` setting with value `logsdb` when a cluster setting `cluster.logsdb.enabled` is `true`.
We also make sure that:
* the existing `index.mode` is not set
* the datastream name matches the `logs-*-*` pattern
* `logs@settings` component template is used
---
.../LogsIndexModeDisabledRestTestIT.java | 70 +++-
.../LogsIndexModeEnabledRestTestIT.java | 87 ++++-
.../logsdb/LogsIndexModeRestTestIT.java | 19 +-
.../core/src/main/java/module-info.java | 1 +
.../cluster/settings/ClusterSettings.java | 19 +
.../main/resources/logs@settings-logsdb.json | 26 ++
.../src/main/resources/logs@settings.json | 1 -
.../xpack/logsdb/LogsDBPlugin.java | 12 +-
.../LogsdbIndexModeSettingsProvider.java | 89 +++++
.../LogsdbIndexModeSettingsProviderTests.java | 326 ++++++++++++++++++
.../stack/LegacyStackTemplateRegistry.java | 7 +-
.../xpack/stack/StackPlugin.java | 2 +-
.../xpack/stack/StackTemplateRegistry.java | 21 +-
13 files changed, 648 insertions(+), 32 deletions(-)
create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/settings/ClusterSettings.java
create mode 100644 x-pack/plugin/core/template-resources/src/main/resources/logs@settings-logsdb.json
create mode 100644 x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/LogsdbIndexModeSettingsProvider.java
create mode 100644 x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/LogsdbIndexModeSettingsProviderTests.java
diff --git a/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/logsdb/LogsIndexModeDisabledRestTestIT.java b/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/logsdb/LogsIndexModeDisabledRestTestIT.java
index c9818a34169de..123ca3b806153 100644
--- a/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/logsdb/LogsIndexModeDisabledRestTestIT.java
+++ b/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/logsdb/LogsIndexModeDisabledRestTestIT.java
@@ -11,6 +11,7 @@
import org.elasticsearch.client.RestClient;
import org.elasticsearch.index.IndexMode;
+import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.test.cluster.ElasticsearchCluster;
import org.elasticsearch.test.cluster.local.distribution.DistributionType;
import org.hamcrest.Matchers;
@@ -23,6 +24,22 @@
public class LogsIndexModeDisabledRestTestIT extends LogsIndexModeRestTestIT {
+ private static final String MAPPINGS = """
+ {
+ "template": {
+ "mappings": {
+ "properties": {
+ "@timestamp": {
+ "type": "date"
+ },
+ "message": {
+ "type": "text"
+ }
+ }
+ }
+ }
+ }""";
+
@ClassRule()
public static ElasticsearchCluster cluster = ElasticsearchCluster.local()
.distribution(DistributionType.DEFAULT)
@@ -50,8 +67,59 @@ public void setup() throws Exception {
public void testLogsSettingsIndexModeDisabled() throws IOException {
assertOK(createDataStream(client, "logs-custom-dev"));
- final String indexMode = (String) getSetting(client, getDataStreamBackingIndex(client, "logs-custom-dev", 0), "index.mode");
+ final String indexMode = (String) getSetting(
+ client,
+ getDataStreamBackingIndex(client, "logs-custom-dev", 0),
+ IndexSettings.MODE.getKey()
+ );
assertThat(indexMode, Matchers.not(equalTo(IndexMode.LOGSDB.getName())));
}
+ public void testTogglingLogsdb() throws IOException {
+ putComponentTemplate(client, "logs@settings", MAPPINGS);
+ assertOK(createDataStream(client, "logs-custom-dev"));
+ final String indexModeBefore = (String) getSetting(
+ client,
+ getDataStreamBackingIndex(client, "logs-custom-dev", 0),
+ IndexSettings.MODE.getKey()
+ );
+ assertThat(indexModeBefore, Matchers.not(equalTo(IndexMode.LOGSDB.getName())));
+ assertOK(putClusterSetting(client, "cluster.logsdb.enabled", "true"));
+ final String indexModeAfter = (String) getSetting(
+ client,
+ getDataStreamBackingIndex(client, "logs-custom-dev", 0),
+ IndexSettings.MODE.getKey()
+ );
+ assertThat(indexModeAfter, Matchers.not(equalTo(IndexMode.LOGSDB.getName())));
+ assertOK(rolloverDataStream(client, "logs-custom-dev"));
+ final String indexModeLater = (String) getSetting(
+ client,
+ getDataStreamBackingIndex(client, "logs-custom-dev", 1),
+ IndexSettings.MODE.getKey()
+ );
+ assertThat(indexModeLater, equalTo(IndexMode.LOGSDB.getName()));
+ assertOK(putClusterSetting(client, "cluster.logsdb.enabled", "false"));
+ assertOK(rolloverDataStream(client, "logs-custom-dev"));
+ final String indexModeFinal = (String) getSetting(
+ client,
+ getDataStreamBackingIndex(client, "logs-custom-dev", 2),
+ IndexSettings.MODE.getKey()
+ );
+ assertThat(indexModeFinal, Matchers.not(equalTo(IndexMode.LOGSDB.getName())));
+
+ }
+
+ public void testEnablingLogsdb() throws IOException {
+ putComponentTemplate(client, "logs@settings", MAPPINGS);
+ assertOK(putClusterSetting(client, "cluster.logsdb.enabled", true));
+ assertOK(createDataStream(client, "logs-custom-dev"));
+ final String indexMode = (String) getSetting(
+ client,
+ getDataStreamBackingIndex(client, "logs-custom-dev", 0),
+ IndexSettings.MODE.getKey()
+ );
+ assertThat(indexMode, equalTo(IndexMode.LOGSDB.getName()));
+ assertOK(putClusterSetting(client, "cluster.logsdb.enabled", false));
+ }
+
}
diff --git a/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/logsdb/LogsIndexModeEnabledRestTestIT.java b/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/logsdb/LogsIndexModeEnabledRestTestIT.java
index d7bdf54007d69..a024a2c0f303c 100644
--- a/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/logsdb/LogsIndexModeEnabledRestTestIT.java
+++ b/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/logsdb/LogsIndexModeEnabledRestTestIT.java
@@ -10,8 +10,10 @@
package org.elasticsearch.datastreams.logsdb;
import org.elasticsearch.client.Response;
+import org.elasticsearch.client.ResponseException;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.index.IndexMode;
+import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.test.cluster.ElasticsearchCluster;
import org.elasticsearch.test.cluster.local.distribution.DistributionType;
import org.hamcrest.Matchers;
@@ -179,7 +181,11 @@ public void setup() throws Exception {
public void testCreateDataStream() throws IOException {
assertOK(putComponentTemplate(client, "logs@custom", MAPPINGS));
assertOK(createDataStream(client, "logs-custom-dev"));
- final String indexMode = (String) getSetting(client, getDataStreamBackingIndex(client, "logs-custom-dev", 0), "index.mode");
+ final String indexMode = (String) getSetting(
+ client,
+ getDataStreamBackingIndex(client, "logs-custom-dev", 0),
+ IndexSettings.MODE.getKey()
+ );
assertThat(indexMode, equalTo(IndexMode.LOGSDB.getName()));
}
@@ -224,4 +230,83 @@ public void testRolloverDataStream() throws IOException {
assertThat(firstBackingIndex, Matchers.not(equalTo(secondBackingIndex)));
assertThat(getDataStreamBackingIndices(client, "logs-custom-dev").size(), equalTo(2));
}
+
+ public void testLogsAtSettingWithStandardOverride() throws IOException {
+ assertOK(putComponentTemplate(client, "logs@custom", """
+ {
+ "template": {
+ "settings": {
+ "index": {
+ "mode": "standard"
+ }
+ }
+ }
+ }
+ """));
+ assertOK(createDataStream(client, "logs-custom-dev"));
+ final String indexMode = (String) getSetting(
+ client,
+ getDataStreamBackingIndex(client, "logs-custom-dev", 0),
+ IndexSettings.MODE.getKey()
+ );
+ assertThat(indexMode, equalTo(IndexMode.STANDARD.getName()));
+ }
+
+ public void testLogsAtSettingWithTimeSeriesOverride() throws IOException {
+ assertOK(putComponentTemplate(client, "logs@custom", """
+ {
+ "template": {
+ "settings": {
+ "index": {
+ "routing_path": [ "hostname" ],
+ "mode": "time_series",
+ "sort.field": [],
+ "sort.order": []
+ }
+ },
+ "mappings": {
+ "properties": {
+ "hostname": {
+ "type": "keyword",
+ "time_series_dimension": true
+ }
+ }
+ }
+ }
+ }
+ """));
+ assertOK(createDataStream(client, "logs-custom-dev"));
+ final String indexMode = (String) getSetting(
+ client,
+ getDataStreamBackingIndex(client, "logs-custom-dev", 0),
+ IndexSettings.MODE.getKey()
+ );
+ assertThat(indexMode, equalTo(IndexMode.TIME_SERIES.getName()));
+ }
+
+ public void testLogsAtSettingWithTimeSeriesOverrideFailure() {
+ // NOTE: apm@settings defines sorting on @timestamp and template composition results in index.mode "time_series"
+ // with a non-allowed index.sort.field '@timestamp'. This fails at template composition stage before the index is even created.
+ final ResponseException ex = assertThrows(ResponseException.class, () -> putComponentTemplate(client, "logs@custom", """
+ {
+ "template": {
+ "settings": {
+ "index": {
+ "routing_path": [ "hostname" ],
+ "mode": "time_series"
+ }
+ },
+ "mappings": {
+ "properties": {
+ "hostname": {
+ "type": "keyword",
+ "time_series_dimension": true
+ }
+ }
+ }
+ }
+ }
+ """));
+ assertTrue(ex.getMessage().contains("[index.mode=time_series] is incompatible with [index.sort.field]"));
+ }
}
diff --git a/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/logsdb/LogsIndexModeRestTestIT.java b/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/logsdb/LogsIndexModeRestTestIT.java
index 7d65207794598..22ac2b6d7d239 100644
--- a/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/logsdb/LogsIndexModeRestTestIT.java
+++ b/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/logsdb/LogsIndexModeRestTestIT.java
@@ -33,10 +33,16 @@ protected static void waitForLogs(RestClient client) throws Exception {
});
}
- protected static Response putComponentTemplate(final RestClient client, final String templateName, final String mappings)
+ protected static Response putComponentTemplate(final RestClient client, final String componentTemplate, final String contends)
throws IOException {
- final Request request = new Request("PUT", "/_component_template/" + templateName);
- request.setJsonEntity(mappings);
+ final Request request = new Request("PUT", "/_component_template/" + componentTemplate);
+ request.setJsonEntity(contends);
+ return client.performRequest(request);
+ }
+
+ protected static Response putTemplate(final RestClient client, final String template, final String contents) throws IOException {
+ final Request request = new Request("PUT", "/_index_template/" + template);
+ request.setJsonEntity(contents);
return client.performRequest(request);
}
@@ -87,4 +93,11 @@ protected static Response bulkIndex(final RestClient client, final String dataSt
bulkRequest.addParameter("refresh", "true");
return client.performRequest(bulkRequest);
}
+
+ protected static Response putClusterSetting(final RestClient client, final String settingName, final Object settingValue)
+ throws IOException {
+ final Request request = new Request("PUT", "/_cluster/settings");
+ request.setJsonEntity("{ \"transient\": { \"" + settingName + "\": " + settingValue + " } }");
+ return client.performRequest(request);
+ }
}
diff --git a/x-pack/plugin/core/src/main/java/module-info.java b/x-pack/plugin/core/src/main/java/module-info.java
index 72436bb9d5171..47848310fe781 100644
--- a/x-pack/plugin/core/src/main/java/module-info.java
+++ b/x-pack/plugin/core/src/main/java/module-info.java
@@ -228,6 +228,7 @@
exports org.elasticsearch.xpack.core.watcher.trigger;
exports org.elasticsearch.xpack.core.watcher.watch;
exports org.elasticsearch.xpack.core.watcher;
+ exports org.elasticsearch.xpack.cluster.settings;
provides org.elasticsearch.action.admin.cluster.node.info.ComponentVersionNumber
with
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/settings/ClusterSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/settings/ClusterSettings.java
new file mode 100644
index 0000000000000..1127889783f16
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/settings/ClusterSettings.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.cluster.settings;
+
+import org.elasticsearch.common.settings.Setting;
+
+public class ClusterSettings {
+ public static final Setting CLUSTER_LOGSDB_ENABLED = Setting.boolSetting(
+ "cluster.logsdb.enabled",
+ false,
+ Setting.Property.Dynamic,
+ Setting.Property.NodeScope
+ );
+}
diff --git a/x-pack/plugin/core/template-resources/src/main/resources/logs@settings-logsdb.json b/x-pack/plugin/core/template-resources/src/main/resources/logs@settings-logsdb.json
new file mode 100644
index 0000000000000..eabdd6fb9fad2
--- /dev/null
+++ b/x-pack/plugin/core/template-resources/src/main/resources/logs@settings-logsdb.json
@@ -0,0 +1,26 @@
+{
+ "template": {
+ "settings": {
+ "index": {
+ "lifecycle": {
+ "name": "logs"
+ },
+ "mode": "logsdb",
+ "codec": "best_compression",
+ "mapping": {
+ "ignore_malformed": true,
+ "total_fields": {
+ "ignore_dynamic_beyond_limit": true
+ }
+ },
+ "default_pipeline": "logs@default-pipeline"
+ }
+ }
+ },
+ "_meta": {
+ "description": "default settings for the logs index template installed by x-pack",
+ "managed": true
+ },
+ "version": ${xpack.stack.template.version},
+ "deprecated": ${xpack.stack.template.deprecated}
+}
diff --git a/x-pack/plugin/core/template-resources/src/main/resources/logs@settings.json b/x-pack/plugin/core/template-resources/src/main/resources/logs@settings.json
index e9a9f2611ad7b..ca2659b8d8dea 100644
--- a/x-pack/plugin/core/template-resources/src/main/resources/logs@settings.json
+++ b/x-pack/plugin/core/template-resources/src/main/resources/logs@settings.json
@@ -5,7 +5,6 @@
"lifecycle": {
"name": "logs"
},
- "mode": "${xpack.stack.template.logsdb.index.mode}",
"codec": "best_compression",
"mapping": {
"ignore_malformed": true,
diff --git a/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/LogsDBPlugin.java b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/LogsDBPlugin.java
index e38f953be96a3..833555a7884ea 100644
--- a/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/LogsDBPlugin.java
+++ b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/LogsDBPlugin.java
@@ -17,6 +17,7 @@
import java.util.Collection;
import java.util.List;
+import static org.elasticsearch.xpack.cluster.settings.ClusterSettings.CLUSTER_LOGSDB_ENABLED;
import static org.elasticsearch.xpack.logsdb.SyntheticSourceLicenseService.FALLBACK_SETTING;
public class LogsDBPlugin extends Plugin {
@@ -24,9 +25,12 @@ public class LogsDBPlugin extends Plugin {
private final Settings settings;
private final SyntheticSourceLicenseService licenseService;
+ private final LogsdbIndexModeSettingsProvider logsdbIndexModeSettingsProvider;
+
public LogsDBPlugin(Settings settings) {
this.settings = settings;
this.licenseService = new SyntheticSourceLicenseService(settings);
+ this.logsdbIndexModeSettingsProvider = new LogsdbIndexModeSettingsProvider(settings);
}
@Override
@@ -34,6 +38,10 @@ public Collection> createComponents(PluginServices services) {
licenseService.setLicenseState(XPackPlugin.getSharedLicenseState());
var clusterSettings = services.clusterService().getClusterSettings();
clusterSettings.addSettingsUpdateConsumer(FALLBACK_SETTING, licenseService::setSyntheticSourceFallback);
+ clusterSettings.addSettingsUpdateConsumer(
+ CLUSTER_LOGSDB_ENABLED,
+ logsdbIndexModeSettingsProvider::updateClusterIndexModeLogsdbEnabled
+ );
// Nothing to share here:
return super.createComponents(services);
}
@@ -43,11 +51,11 @@ public Collection getAdditionalIndexSettingProviders(Index
if (DiscoveryNode.isStateless(settings)) {
return List.of();
}
- return List.of(new SyntheticSourceIndexSettingsProvider(licenseService));
+ return List.of(new SyntheticSourceIndexSettingsProvider(licenseService), logsdbIndexModeSettingsProvider);
}
@Override
public List> getSettings() {
- return List.of(FALLBACK_SETTING);
+ return List.of(FALLBACK_SETTING, CLUSTER_LOGSDB_ENABLED);
}
}
diff --git a/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/LogsdbIndexModeSettingsProvider.java b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/LogsdbIndexModeSettingsProvider.java
new file mode 100644
index 0000000000000..3f6bb66dfa438
--- /dev/null
+++ b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/LogsdbIndexModeSettingsProvider.java
@@ -0,0 +1,89 @@
+/*
+ * 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.logsdb;
+
+import org.elasticsearch.cluster.metadata.ComposableIndexTemplate;
+import org.elasticsearch.cluster.metadata.Metadata;
+import org.elasticsearch.cluster.metadata.MetadataIndexTemplateService;
+import org.elasticsearch.common.compress.CompressedXContent;
+import org.elasticsearch.common.regex.Regex;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.index.IndexMode;
+import org.elasticsearch.index.IndexSettingProvider;
+import org.elasticsearch.index.IndexSettings;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Locale;
+
+import static org.elasticsearch.xpack.cluster.settings.ClusterSettings.CLUSTER_LOGSDB_ENABLED;
+
+final class LogsdbIndexModeSettingsProvider implements IndexSettingProvider {
+ private static final String LOGS_PATTERN = "logs-*-*";
+ private volatile boolean isLogsdbEnabled;
+
+ LogsdbIndexModeSettingsProvider(final Settings settings) {
+ this.isLogsdbEnabled = CLUSTER_LOGSDB_ENABLED.get(settings);
+ }
+
+ void updateClusterIndexModeLogsdbEnabled(boolean isLogsdbEnabled) {
+ this.isLogsdbEnabled = isLogsdbEnabled;
+ }
+
+ @Override
+ public Settings getAdditionalIndexSettings(
+ final String indexName,
+ final String dataStreamName,
+ boolean isTimeSeries,
+ final Metadata metadata,
+ final Instant resolvedAt,
+ final Settings settings,
+ final List combinedTemplateMappings
+ ) {
+ if (isLogsdbEnabled == false || dataStreamName == null) {
+ return Settings.EMPTY;
+ }
+
+ final IndexMode indexMode = resolveIndexMode(settings.get(IndexSettings.MODE.getKey()));
+ if (indexMode != null) {
+ return Settings.EMPTY;
+ }
+
+ if (usesLogsAtSettingsComponentTemplate(metadata, dataStreamName) && matchesLogsPattern(dataStreamName)) {
+ return Settings.builder().put("index.mode", IndexMode.LOGSDB.getName()).build();
+ }
+
+ return Settings.EMPTY;
+ }
+
+ private static boolean matchesLogsPattern(final String name) {
+ return Regex.simpleMatch(LOGS_PATTERN, name);
+ }
+
+ private IndexMode resolveIndexMode(final String mode) {
+ return mode != null ? Enum.valueOf(IndexMode.class, mode.toUpperCase(Locale.ROOT)) : null;
+ }
+
+ private boolean usesLogsAtSettingsComponentTemplate(final Metadata metadata, final String name) {
+ final String template = MetadataIndexTemplateService.findV2Template(metadata, name, false);
+ if (template == null) {
+ return false;
+ }
+ final ComposableIndexTemplate composableIndexTemplate = metadata.templatesV2().get(template);
+ if (composableIndexTemplate == null) {
+ return false;
+ }
+ for (final String componentTemplate : composableIndexTemplate.composedOf()) {
+ if ("logs@settings".equals(componentTemplate)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/LogsdbIndexModeSettingsProviderTests.java b/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/LogsdbIndexModeSettingsProviderTests.java
new file mode 100644
index 0000000000000..eeb5389644c02
--- /dev/null
+++ b/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/LogsdbIndexModeSettingsProviderTests.java
@@ -0,0 +1,326 @@
+/*
+ * 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.logsdb;
+
+import org.elasticsearch.cluster.metadata.ComposableIndexTemplate;
+import org.elasticsearch.cluster.metadata.ComposableIndexTemplateMetadata;
+import org.elasticsearch.cluster.metadata.Metadata;
+import org.elasticsearch.cluster.metadata.Template;
+import org.elasticsearch.common.compress.CompressedXContent;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.index.IndexMode;
+import org.elasticsearch.index.IndexSettings;
+import org.elasticsearch.test.ESTestCase;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import java.util.Map;
+
+public class LogsdbIndexModeSettingsProviderTests extends ESTestCase {
+
+ public static final String DEFAULT_MAPPING = """
+ {
+ "_doc": {
+ "properties": {
+ "@timestamp": {
+ "type": "date"
+ },
+ "message": {
+ "type": "keyword"
+ },
+ "host.name": {
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ """;
+
+ public void testLogsDbDisabled() throws IOException {
+ final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider(
+ Settings.builder().put("cluster.logsdb.enabled", false).build()
+ );
+
+ final Settings additionalIndexSettings = provider.getAdditionalIndexSettings(
+ null,
+ "logs-apache-production",
+ false,
+ Metadata.EMPTY_METADATA,
+ Instant.now().truncatedTo(ChronoUnit.SECONDS),
+ Settings.EMPTY,
+ List.of(new CompressedXContent(DEFAULT_MAPPING))
+ );
+
+ assertTrue(additionalIndexSettings.isEmpty());
+ }
+
+ public void testOnIndexCreation() throws IOException {
+ final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider(
+ Settings.builder().put("cluster.logsdb.enabled", true).build()
+ );
+
+ final Settings additionalIndexSettings = provider.getAdditionalIndexSettings(
+ "logs-apache-production",
+ null,
+ false,
+ Metadata.EMPTY_METADATA,
+ Instant.now().truncatedTo(ChronoUnit.SECONDS),
+ Settings.EMPTY,
+ List.of(new CompressedXContent(DEFAULT_MAPPING))
+ );
+
+ assertTrue(additionalIndexSettings.isEmpty());
+ }
+
+ public void testOnExplicitStandardIndex() throws IOException {
+ final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider(
+ Settings.builder().put("cluster.logsdb.enabled", true).build()
+ );
+
+ final Settings additionalIndexSettings = provider.getAdditionalIndexSettings(
+ null,
+ "logs-apache-production",
+ false,
+ Metadata.EMPTY_METADATA,
+ Instant.now().truncatedTo(ChronoUnit.SECONDS),
+ Settings.builder().put(IndexSettings.MODE.getKey(), IndexMode.STANDARD.getName()).build(),
+ List.of(new CompressedXContent(DEFAULT_MAPPING))
+ );
+
+ assertTrue(additionalIndexSettings.isEmpty());
+ }
+
+ public void testOnExplicitTimeSeriesIndex() throws IOException {
+ final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider(
+ Settings.builder().put("cluster.logsdb.enabled", true).build()
+ );
+
+ final Settings additionalIndexSettings = provider.getAdditionalIndexSettings(
+ null,
+ "logs-apache-production",
+ false,
+ Metadata.EMPTY_METADATA,
+ Instant.now().truncatedTo(ChronoUnit.SECONDS),
+ Settings.builder().put(IndexSettings.MODE.getKey(), IndexMode.TIME_SERIES.getName()).build(),
+ List.of(new CompressedXContent(DEFAULT_MAPPING))
+ );
+
+ assertTrue(additionalIndexSettings.isEmpty());
+ }
+
+ public void testNonLogsDataStream() throws IOException {
+ final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider(
+ Settings.builder().put("cluster.logsdb.enabled", true).build()
+ );
+
+ final Settings additionalIndexSettings = provider.getAdditionalIndexSettings(
+ null,
+ "logs",
+ false,
+ Metadata.EMPTY_METADATA,
+ Instant.now().truncatedTo(ChronoUnit.SECONDS),
+ Settings.EMPTY,
+ List.of(new CompressedXContent(DEFAULT_MAPPING))
+ );
+
+ assertTrue(additionalIndexSettings.isEmpty());
+ }
+
+ public void testWithoutLogsComponentTemplate() throws IOException {
+ final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider(
+ Settings.builder().put("cluster.logsdb.enabled", true).build()
+ );
+
+ final Settings additionalIndexSettings = provider.getAdditionalIndexSettings(
+ null,
+ "logs-apache-production",
+ false,
+ buildMetadata(List.of("*"), List.of()),
+ Instant.now().truncatedTo(ChronoUnit.SECONDS),
+ Settings.EMPTY,
+ List.of(new CompressedXContent(DEFAULT_MAPPING))
+ );
+
+ assertTrue(additionalIndexSettings.isEmpty());
+ }
+
+ public void testWithLogsComponentTemplate() throws IOException {
+ final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider(
+ Settings.builder().put("cluster.logsdb.enabled", true).build()
+ );
+
+ final Settings additionalIndexSettings = provider.getAdditionalIndexSettings(
+ null,
+ "logs-apache-production",
+ false,
+ buildMetadata(List.of("*"), List.of("logs@settings")),
+ Instant.now().truncatedTo(ChronoUnit.SECONDS),
+ Settings.EMPTY,
+ List.of(new CompressedXContent(DEFAULT_MAPPING))
+ );
+
+ assertIndexMode(additionalIndexSettings, IndexMode.LOGSDB.getName());
+ }
+
+ public void testWithMultipleComponentTemplates() throws IOException {
+ final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider(
+ Settings.builder().put("cluster.logsdb.enabled", true).build()
+ );
+
+ final Settings additionalIndexSettings = provider.getAdditionalIndexSettings(
+ null,
+ "logs-apache-production",
+ false,
+ buildMetadata(List.of("*"), List.of("logs@settings", "logs@custom")),
+ Instant.now().truncatedTo(ChronoUnit.SECONDS),
+ Settings.EMPTY,
+ List.of(new CompressedXContent(DEFAULT_MAPPING))
+ );
+
+ assertIndexMode(additionalIndexSettings, IndexMode.LOGSDB.getName());
+ }
+
+ public void testWithCustomComponentTemplatesOnly() throws IOException {
+ final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider(
+ Settings.builder().put("cluster.logsdb.enabled", true).build()
+ );
+
+ final Settings additionalIndexSettings = provider.getAdditionalIndexSettings(
+ null,
+ "logs-apache-production",
+ false,
+ buildMetadata(List.of("*"), List.of("logs@custom", "custom-component-template")),
+ Instant.now().truncatedTo(ChronoUnit.SECONDS),
+ Settings.EMPTY,
+ List.of(new CompressedXContent(DEFAULT_MAPPING))
+ );
+
+ assertTrue(additionalIndexSettings.isEmpty());
+ }
+
+ public void testNonMatchingTemplateIndexPattern() throws IOException {
+ final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider(
+ Settings.builder().put("cluster.logsdb.enabled", true).build()
+ );
+
+ final Settings additionalIndexSettings = provider.getAdditionalIndexSettings(
+ null,
+ "logs-apache-production",
+ false,
+ buildMetadata(List.of("standard-apache-production"), List.of("logs@settings")),
+ Instant.now().truncatedTo(ChronoUnit.SECONDS),
+ Settings.EMPTY,
+ List.of(new CompressedXContent(DEFAULT_MAPPING))
+ );
+
+ assertTrue(additionalIndexSettings.isEmpty());
+ }
+
+ public void testCaseSensitivity() throws IOException {
+ final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider(
+ Settings.builder().put("cluster.logsdb.enabled", true).build()
+ );
+
+ final Settings additionalIndexSettings = provider.getAdditionalIndexSettings(
+ null,
+ "LOGS-apache-production",
+ false,
+ Metadata.EMPTY_METADATA,
+ Instant.now().truncatedTo(ChronoUnit.SECONDS),
+ Settings.EMPTY,
+ List.of(new CompressedXContent(DEFAULT_MAPPING))
+ );
+
+ assertTrue(additionalIndexSettings.isEmpty());
+ }
+
+ public void testMultipleHyphensInDataStreamName() throws IOException {
+ final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider(
+ Settings.builder().put("cluster.logsdb.enabled", true).build()
+ );
+
+ final Settings additionalIndexSettings = provider.getAdditionalIndexSettings(
+ null,
+ "logs-apache-production-eu",
+ false,
+ Metadata.EMPTY_METADATA,
+ Instant.now().truncatedTo(ChronoUnit.SECONDS),
+ Settings.EMPTY,
+ List.of(new CompressedXContent(DEFAULT_MAPPING))
+ );
+
+ assertTrue(additionalIndexSettings.isEmpty());
+ }
+
+ public void testBeforeAndAFterSettingUpdate() throws IOException {
+ final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider(
+ Settings.builder().put("cluster.logsdb.enabled", false).build()
+ );
+
+ final Settings beforeSettings = provider.getAdditionalIndexSettings(
+ null,
+ "logs-apache-production",
+ false,
+ buildMetadata(List.of("*"), List.of("logs@settings")),
+ Instant.now().truncatedTo(ChronoUnit.SECONDS),
+ Settings.EMPTY,
+ List.of(new CompressedXContent(DEFAULT_MAPPING))
+ );
+
+ assertTrue(beforeSettings.isEmpty());
+
+ provider.updateClusterIndexModeLogsdbEnabled(true);
+
+ final Settings afterSettings = provider.getAdditionalIndexSettings(
+ null,
+ "logs-apache-production",
+ false,
+ buildMetadata(List.of("*"), List.of("logs@settings")),
+ Instant.now().truncatedTo(ChronoUnit.SECONDS),
+ Settings.EMPTY,
+ List.of(new CompressedXContent(DEFAULT_MAPPING))
+ );
+
+ assertIndexMode(afterSettings, IndexMode.LOGSDB.getName());
+
+ provider.updateClusterIndexModeLogsdbEnabled(false);
+
+ final Settings laterSettings = provider.getAdditionalIndexSettings(
+ null,
+ "logs-apache-production",
+ false,
+ buildMetadata(List.of("*"), List.of("logs@settings")),
+ Instant.now().truncatedTo(ChronoUnit.SECONDS),
+ Settings.EMPTY,
+ List.of(new CompressedXContent(DEFAULT_MAPPING))
+ );
+
+ assertTrue(laterSettings.isEmpty());
+ }
+
+ private static Metadata buildMetadata(final List indexPatterns, final List componentTemplates) throws IOException {
+ final Template template = new Template(Settings.EMPTY, new CompressedXContent(DEFAULT_MAPPING), null);
+ final ComposableIndexTemplate composableTemplate = ComposableIndexTemplate.builder()
+ .indexPatterns(indexPatterns)
+ .template(template)
+ .componentTemplates(componentTemplates)
+ .priority(1_000L)
+ .version(1L)
+ .build();
+ return Metadata.builder()
+ .putCustom(ComposableIndexTemplateMetadata.TYPE, new ComposableIndexTemplateMetadata(Map.of("composable", composableTemplate)))
+ .build();
+ }
+
+ private void assertIndexMode(final Settings settings, final String expectedIndexMode) {
+ assertEquals(expectedIndexMode, settings.get(IndexSettings.MODE.getKey()));
+ }
+
+}
diff --git a/x-pack/plugin/stack/src/main/java/org/elasticsearch/xpack/stack/LegacyStackTemplateRegistry.java b/x-pack/plugin/stack/src/main/java/org/elasticsearch/xpack/stack/LegacyStackTemplateRegistry.java
index 62d22c0c0a9cc..b2dc04c1178e4 100644
--- a/x-pack/plugin/stack/src/main/java/org/elasticsearch/xpack/stack/LegacyStackTemplateRegistry.java
+++ b/x-pack/plugin/stack/src/main/java/org/elasticsearch/xpack/stack/LegacyStackTemplateRegistry.java
@@ -51,12 +51,7 @@ public class LegacyStackTemplateRegistry extends IndexTemplateRegistry {
private final FeatureService featureService;
private volatile boolean stackTemplateEnabled;
- private static final Map ADDITIONAL_TEMPLATE_VARIABLES = Map.of(
- "xpack.stack.template.deprecated",
- "true",
- "xpack.stack.template.logsdb.index.mode",
- "standard"
- );
+ private static final Map ADDITIONAL_TEMPLATE_VARIABLES = Map.of("xpack.stack.template.deprecated", "true");
// General mappings conventions for any data that ends up in a data stream
public static final String DATA_STREAMS_MAPPINGS_COMPONENT_TEMPLATE_NAME = "data-streams-mappings";
diff --git a/x-pack/plugin/stack/src/main/java/org/elasticsearch/xpack/stack/StackPlugin.java b/x-pack/plugin/stack/src/main/java/org/elasticsearch/xpack/stack/StackPlugin.java
index cc127883652af..71d01798323d3 100644
--- a/x-pack/plugin/stack/src/main/java/org/elasticsearch/xpack/stack/StackPlugin.java
+++ b/x-pack/plugin/stack/src/main/java/org/elasticsearch/xpack/stack/StackPlugin.java
@@ -23,7 +23,7 @@ public StackPlugin(Settings settings) {
@Override
public List> getSettings() {
- return List.of(StackTemplateRegistry.STACK_TEMPLATES_ENABLED, StackTemplateRegistry.CLUSTER_LOGSDB_ENABLED);
+ return List.of(StackTemplateRegistry.STACK_TEMPLATES_ENABLED);
}
@Override
diff --git a/x-pack/plugin/stack/src/main/java/org/elasticsearch/xpack/stack/StackTemplateRegistry.java b/x-pack/plugin/stack/src/main/java/org/elasticsearch/xpack/stack/StackTemplateRegistry.java
index 592842f61eee8..b45f17e434388 100644
--- a/x-pack/plugin/stack/src/main/java/org/elasticsearch/xpack/stack/StackTemplateRegistry.java
+++ b/x-pack/plugin/stack/src/main/java/org/elasticsearch/xpack/stack/StackTemplateRegistry.java
@@ -18,7 +18,6 @@
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.features.FeatureService;
import org.elasticsearch.features.NodeFeature;
-import org.elasticsearch.index.IndexMode;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.xcontent.NamedXContentRegistry;
import org.elasticsearch.xcontent.XContentParserConfiguration;
@@ -36,6 +35,8 @@
import java.util.List;
import java.util.Map;
+import static org.elasticsearch.xpack.cluster.settings.ClusterSettings.CLUSTER_LOGSDB_ENABLED;
+
public class StackTemplateRegistry extends IndexTemplateRegistry {
private static final Logger logger = LogManager.getLogger(StackTemplateRegistry.class);
@@ -58,15 +59,6 @@ public class StackTemplateRegistry extends IndexTemplateRegistry {
Setting.Property.Dynamic
);
- /**
- * if index.mode "logsdb" is applied by default in logs@settings for 'logs-*-*'
- */
- public static final Setting CLUSTER_LOGSDB_ENABLED = Setting.boolSetting(
- "cluster.logsdb.enabled",
- false,
- Setting.Property.NodeScope
- );
-
private final ClusterService clusterService;
private final FeatureService featureService;
private final Map componentTemplateConfigs;
@@ -167,15 +159,10 @@ private Map loadComponentTemplateConfigs(boolean logs
),
new IndexTemplateConfig(
LOGS_SETTINGS_COMPONENT_TEMPLATE_NAME,
- "/logs@settings.json",
+ logsDbEnabled ? "/logs@settings-logsdb.json" : "/logs@settings.json",
REGISTRY_VERSION,
TEMPLATE_VERSION_VARIABLE,
- Map.of(
- "xpack.stack.template.deprecated",
- "false",
- "xpack.stack.template.logsdb.index.mode",
- logsDbEnabled ? IndexMode.LOGSDB.getName() : IndexMode.STANDARD.getName()
- )
+ Map.of("xpack.stack.template.deprecated", "false")
),
new IndexTemplateConfig(
METRICS_MAPPINGS_COMPONENT_TEMPLATE_NAME,
From 1b67dabadb35b4c79d67ff30ccdfce1cd5929ce8 Mon Sep 17 00:00:00 2001
From: Benjamin Trent
Date: Thu, 26 Sep 2024 08:51:02 -0400
Subject: [PATCH 09/43] Fix collapse interaction with stored fields (#112761)
Collapse dynamically will add values to the DocumentField values array.
There are a few scenarios where this is immutable and most of these are
OK. However, we get in trouble when we create an immutable set for
StoredValues which collapse later tries to update.
The other option for this fix was to make an array copy for `values` in
every `DocumentField` ctor, this seemed very expensive and could get out
of hand. So, I decided to fix this one bug instead.
closes https://github.com/elastic/elasticsearch/issues/112646
---
docs/changelog/112761.yaml | 6 ++++
.../search/CollapseSearchResultsIT.java | 30 +++++++++++++++++++
.../fetch/subphase/StoredFieldsPhase.java | 4 ++-
3 files changed, 39 insertions(+), 1 deletion(-)
create mode 100644 docs/changelog/112761.yaml
diff --git a/docs/changelog/112761.yaml b/docs/changelog/112761.yaml
new file mode 100644
index 0000000000000..fe63f38f365a4
--- /dev/null
+++ b/docs/changelog/112761.yaml
@@ -0,0 +1,6 @@
+pr: 112761
+summary: Fix collapse interaction with stored fields
+area: Search
+type: bug
+issues:
+ - 112646
diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/CollapseSearchResultsIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/CollapseSearchResultsIT.java
index 48dda7fd30068..89474a0181597 100644
--- a/server/src/internalClusterTest/java/org/elasticsearch/search/CollapseSearchResultsIT.java
+++ b/server/src/internalClusterTest/java/org/elasticsearch/search/CollapseSearchResultsIT.java
@@ -14,6 +14,7 @@
import org.elasticsearch.index.query.MatchAllQueryBuilder;
import org.elasticsearch.search.collapse.CollapseBuilder;
import org.elasticsearch.test.ESIntegTestCase;
+import org.elasticsearch.xcontent.XContentType;
import java.util.Map;
import java.util.Set;
@@ -85,4 +86,33 @@ public void testCollapseWithFields() {
}
);
}
+
+ public void testCollapseWithStoredFields() {
+ final String indexName = "test_collapse";
+ createIndex(indexName);
+ final String collapseField = "collapse_field";
+ assertAcked(indicesAdmin().preparePutMapping(indexName).setSource("""
+ {
+ "dynamic": "strict",
+ "properties": {
+ "collapse_field": { "type": "keyword", "store": true },
+ "ts": { "type": "date", "store": true }
+ }
+ }
+ """, XContentType.JSON));
+ index(indexName, "id_1_0", Map.of(collapseField, "value1", "ts", 0));
+ index(indexName, "id_1_1", Map.of(collapseField, "value1", "ts", 1));
+ index(indexName, "id_2_0", Map.of(collapseField, "value2", "ts", 2));
+ refresh(indexName);
+
+ assertNoFailuresAndResponse(
+ prepareSearch(indexName).setQuery(new MatchAllQueryBuilder())
+ .setFetchSource(false)
+ .storedFields("*")
+ .setCollapse(new CollapseBuilder(collapseField)),
+ searchResponse -> {
+ assertEquals(collapseField, searchResponse.getHits().getCollapseField());
+ }
+ );
+ }
}
diff --git a/server/src/main/java/org/elasticsearch/search/fetch/subphase/StoredFieldsPhase.java b/server/src/main/java/org/elasticsearch/search/fetch/subphase/StoredFieldsPhase.java
index b3211f0b1e31c..17b57645d7d5f 100644
--- a/server/src/main/java/org/elasticsearch/search/fetch/subphase/StoredFieldsPhase.java
+++ b/server/src/main/java/org/elasticsearch/search/fetch/subphase/StoredFieldsPhase.java
@@ -27,6 +27,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.stream.Collectors;
/**
* Process stored fields loaded from a HitContext into DocumentFields
@@ -42,7 +43,8 @@ List
*/
public static List dateCases() {
- return List.of(
- new TypedDataSupplier("<1970-01-01T00:00:00Z>", () -> 0L, DataType.DATETIME),
- new TypedDataSupplier(
- "",
- () -> ESTestCase.randomLongBetween(0, 10 * (long) 10e11), // 1970-01-01T00:00:00Z - 2286-11-20T17:46:40Z
- DataType.DATETIME
- ),
- new TypedDataSupplier(
- "",
- // 2286-11-20T17:46:40Z - +292278994-08-17T07:12:55.807Z
- () -> ESTestCase.randomLongBetween(10 * (long) 10e11, Long.MAX_VALUE),
- DataType.DATETIME
- ),
- new TypedDataSupplier(
- "",
- // very close to +292278994-08-17T07:12:55.807Z, the maximum supported millis since epoch
- () -> ESTestCase.randomLongBetween(Long.MAX_VALUE / 100 * 99, Long.MAX_VALUE),
- DataType.DATETIME
- )
- );
+ return dateCases(Long.MIN_VALUE, Long.MAX_VALUE);
+ }
+
+ /**
+ * Generate cases for {@link DataType#DATETIME}.
+ *
+ * For multi-row parameters, see {@link MultiRowTestCaseSupplier#dateCases}.
+ *
+ */
+ public static List dateCases(long min, long max) {
+ List cases = new ArrayList<>();
+ if (min <= 0 && max >= 0) {
+ cases.add(new TypedDataSupplier("<1970-01-01T00:00:00Z>", () -> 0L, DataType.DATETIME));
+ }
+
+ // 1970-01-01T00:00:00Z - 2286-11-20T17:46:40Z
+ long lower1 = Math.max(min, 0);
+ long upper1 = Math.min(max, 10 * (long) 10e11);
+ if (lower1 < upper1) {
+ cases.add(new TypedDataSupplier("", () -> ESTestCase.randomLongBetween(lower1, upper1), DataType.DATETIME));
+ }
+
+ // 2286-11-20T17:46:40Z - +292278994-08-17T07:12:55.807Z
+ long lower2 = Math.max(min, 10 * (long) 10e11);
+ long upper2 = Math.min(max, Long.MAX_VALUE);
+ if (lower2 < upper2) {
+ cases.add(new TypedDataSupplier("", () -> ESTestCase.randomLongBetween(lower2, upper2), DataType.DATETIME));
+ }
+
+ // very close to +292278994-08-17T07:12:55.807Z, the maximum supported millis since epoch
+ long lower3 = Math.max(min, Long.MAX_VALUE / 100 * 99);
+ long upper3 = Math.min(max, Long.MAX_VALUE);
+ if (lower3 < upper3) {
+ cases.add(
+ new TypedDataSupplier("", () -> ESTestCase.randomLongBetween(lower3, upper3), DataType.DATETIME)
+ );
+ }
+
+ return cases;
}
/**
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToDateNanosTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToDateNanosTests.java
new file mode 100644
index 0000000000000..e91a5cc1ebca4
--- /dev/null
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToDateNanosTests.java
@@ -0,0 +1,135 @@
+/*
+ * 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.esql.expression.function.scalar.convert;
+
+import com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+
+import org.elasticsearch.common.time.DateUtils;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.core.type.DataType;
+import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+public class ToDateNanosTests extends AbstractScalarFunctionTestCase {
+ public ToDateNanosTests(@Name("TestCase") Supplier testCaseSupplier) {
+ this.testCase = testCaseSupplier.get();
+ }
+
+ @ParametersFactory
+ public static Iterable