From b8d655d36d25d987df80315e20c17bf722904c5d Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 1 Aug 2025 09:58:55 -0400 Subject: [PATCH 1/3] Create WaitForClusterSetup gradle plugin to abstract common logic across multiple plugins Signed-off-by: Craig Perkins --- .../gradle/WaitForClusterSetupPlugin.groovy | 29 ++++ .../gradle/WaitForClusterSetupTask.groovy | 160 ++++++++++++++++++ ...ensearch.wait-for-cluster-setup.properties | 12 ++ 3 files changed, 201 insertions(+) create mode 100644 buildSrc/src/main/groovy/org/opensearch/gradle/WaitForClusterSetupPlugin.groovy create mode 100644 buildSrc/src/main/groovy/org/opensearch/gradle/WaitForClusterSetupTask.groovy create mode 100644 buildSrc/src/main/resources/META-INF/gradle-plugins/opensearch.wait-for-cluster-setup.properties diff --git a/buildSrc/src/main/groovy/org/opensearch/gradle/WaitForClusterSetupPlugin.groovy b/buildSrc/src/main/groovy/org/opensearch/gradle/WaitForClusterSetupPlugin.groovy new file mode 100644 index 0000000000000..b0fee4c7d71f3 --- /dev/null +++ b/buildSrc/src/main/groovy/org/opensearch/gradle/WaitForClusterSetupPlugin.groovy @@ -0,0 +1,29 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.gradle + +import org.gradle.api.Plugin +import org.gradle.api.Project + +class WaitForClusterSetupPlugin implements Plugin { + @Override + void apply(Project project) { + project.extensions.create('clusterSetup', ClusterSetupExtension) + + project.task('waitForClusterSetup', type: WaitForClusterSetupTask) + } +} + +class ClusterSetupExtension { + boolean securityEnabled = false + String protocol = 'http' + String username = 'admin' + String password = 'admin' + int timeoutMs = 180000 +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/org/opensearch/gradle/WaitForClusterSetupTask.groovy b/buildSrc/src/main/groovy/org/opensearch/gradle/WaitForClusterSetupTask.groovy new file mode 100644 index 0000000000000..18a4448681077 --- /dev/null +++ b/buildSrc/src/main/groovy/org/opensearch/gradle/WaitForClusterSetupTask.groovy @@ -0,0 +1,160 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction +import org.opensearch.gradle.testclusters.OpenSearchCluster + +import javax.net.ssl.* +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.security.GeneralSecurityException +import java.security.cert.X509Certificate +import java.util.concurrent.TimeUnit +import java.util.function.Predicate +import java.util.stream.Collectors + +class WaitForClusterSetupTask extends DefaultTask { + + @Input + OpenSearchCluster cluster + + @Input + boolean securityEnabled = false + + @Input + String username = System.getProperty("user", "admin") + + @Input + String password = System.getProperty("password", "admin") + + @Input + int timeoutMs = 180000 + + @TaskAction + void setupCluster() { + cluster.@waitConditions.clear() + + // Write unicast hosts + String unicastUris = cluster.nodes.stream().flatMap { node -> + node.getAllTransportPortURI().stream() + }.collect(Collectors.joining("\n")) + + cluster.nodes.forEach { node -> + try { + Files.write(node.getConfigDir().resolve("unicast_hosts.txt"), + unicastUris.getBytes(StandardCharsets.UTF_8)) + } catch (IOException e) { + throw new java.io.UncheckedIOException("Failed to write configuration files", e) + } + } + + // Add wait condition + Predicate pred = { + String protocol = securityEnabled ? "https" : "http" + String host = System.getProperty("tests.cluster", cluster.getFirstNode().getHttpSocketURI()) + WaitForClusterYellow wait = new WaitForClusterYellow(protocol, host, cluster.nodes.size()) + wait.setUsername(username) + wait.setPassword(password) + return wait.wait(timeoutMs) + } + + cluster.@waitConditions.put("cluster health yellow", pred) + cluster.waitForAllConditions() + } +} + +class WaitForClusterYellow { + private URL url + private String username + private String password + Set validResponseCodes = Collections.singleton(200) + + WaitForClusterYellow(String protocol, String host, int numberOfNodes) throws MalformedURLException { + this(new URL(protocol + "://" + host + "/_cluster/health?wait_for_nodes=>=" + numberOfNodes + "&wait_for_status=yellow")) + } + + WaitForClusterYellow(URL url) { + this.url = url + } + + boolean wait(int durationInMs) throws GeneralSecurityException, InterruptedException, IOException { + final long waitUntil = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(durationInMs) + final long sleep = 100 + + IOException failure = null + while (true) { + + try { + checkResource() + return true + } catch (IOException e) { + failure = e + } + if (System.nanoTime() < waitUntil) { + Thread.sleep(sleep) + } else { + throw failure + } + } + } + + void setUsername(String username) { + this.username = username + } + + void setPassword(String password) { + this.password = password + } + + void checkResource() throws IOException { + final HttpURLConnection connection = buildConnection() + connection.connect() + final Integer response = connection.getResponseCode() + if (validResponseCodes.contains(response)) { + return + } else { + throw new IOException(response + " " + connection.getResponseMessage()) + } + } + + HttpURLConnection buildConnection() throws IOException { + final HttpURLConnection connection = (HttpURLConnection) this.@url.openConnection() + + if (connection instanceof HttpsURLConnection) { + TrustManager[] trustAllCerts = [new X509TrustManager() { + X509Certificate[] getAcceptedIssuers() { return null } + void checkClientTrusted(X509Certificate[] certs, String authType) {} + void checkServerTrusted(X509Certificate[] certs, String authType) {} + }] as TrustManager[] + + SSLContext sc = SSLContext.getInstance("SSL") + sc.init(null, trustAllCerts, new java.security.SecureRandom()) + connection.setSSLSocketFactory(sc.getSocketFactory()) + connection.setHostnameVerifier({ hostname, session -> true } as HostnameVerifier) + } + + configureBasicAuth(connection) + connection.setRequestMethod("GET") + return connection + } + + void configureBasicAuth(HttpURLConnection connection) { + // only configure security if https is enabled + if ("https".equals(url.getProtocol())) { + 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)) + ) + } + } + } +} diff --git a/buildSrc/src/main/resources/META-INF/gradle-plugins/opensearch.wait-for-cluster-setup.properties b/buildSrc/src/main/resources/META-INF/gradle-plugins/opensearch.wait-for-cluster-setup.properties new file mode 100644 index 0000000000000..24f3dcb2bb7b3 --- /dev/null +++ b/buildSrc/src/main/resources/META-INF/gradle-plugins/opensearch.wait-for-cluster-setup.properties @@ -0,0 +1,12 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +# The OpenSearch Contributors require contributions made to +# this file be licensed under the Apache-2.0 license or a +# compatible open source license. +# +# Modifications Copyright OpenSearch Contributors. See +# GitHub history for details. +# + +implementation-class=org.opensearch.gradle.WaitForClusterSetupPlugin From cc0f74e6a47ca82aeffbd80b5fd004101b78c498 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 1 Aug 2025 10:09:34 -0400 Subject: [PATCH 2/3] Add to CHANGELOG Signed-off-by: Craig Perkins --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7c15a4899f74..1fccfe90c0e31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,5 +89,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Fix leafSorter optimization for ReadOnlyEngine and NRTReplicationEngine ([#18639](https://github.com/opensearch-project/OpenSearch/pull/18639)) ### Security +- Create WaitForClusterSetup gradle plugin to abstract common logic across multiple plugins for setting up testing with security ([#18892](https://github.com/opensearch-project/OpenSearch/pull/18892)) [Unreleased 3.x]: https://github.com/opensearch-project/OpenSearch/compare/3.1...main From ce6d933b5557d17b172b6fb6c31d17cc76592551 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Fri, 1 Aug 2025 10:14:10 -0400 Subject: [PATCH 3/3] Remove extension Signed-off-by: Craig Perkins --- .../opensearch/gradle/WaitForClusterSetupPlugin.groovy | 10 ---------- .../opensearch/gradle/WaitForClusterSetupTask.groovy | 4 ++++ 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/buildSrc/src/main/groovy/org/opensearch/gradle/WaitForClusterSetupPlugin.groovy b/buildSrc/src/main/groovy/org/opensearch/gradle/WaitForClusterSetupPlugin.groovy index b0fee4c7d71f3..051cb26769c63 100644 --- a/buildSrc/src/main/groovy/org/opensearch/gradle/WaitForClusterSetupPlugin.groovy +++ b/buildSrc/src/main/groovy/org/opensearch/gradle/WaitForClusterSetupPlugin.groovy @@ -14,16 +14,6 @@ import org.gradle.api.Project class WaitForClusterSetupPlugin implements Plugin { @Override void apply(Project project) { - project.extensions.create('clusterSetup', ClusterSetupExtension) - project.task('waitForClusterSetup', type: WaitForClusterSetupTask) } } - -class ClusterSetupExtension { - boolean securityEnabled = false - String protocol = 'http' - String username = 'admin' - String password = 'admin' - int timeoutMs = 180000 -} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/org/opensearch/gradle/WaitForClusterSetupTask.groovy b/buildSrc/src/main/groovy/org/opensearch/gradle/WaitForClusterSetupTask.groovy index 18a4448681077..893e1dc45856c 100644 --- a/buildSrc/src/main/groovy/org/opensearch/gradle/WaitForClusterSetupTask.groovy +++ b/buildSrc/src/main/groovy/org/opensearch/gradle/WaitForClusterSetupTask.groovy @@ -1,5 +1,9 @@ /* * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. */ package org.opensearch.gradle