diff --git a/integ-test/build.gradle b/integ-test/build.gradle index 21eeb767d57..5f622a11968 100644 --- a/integ-test/build.gradle +++ b/integ-test/build.gradle @@ -691,6 +691,13 @@ task integTestWithSecurity(type: RestIntegTestTask) { useCluster testClusters.integTestWithSecurity useCluster testClusters.remoteIntegTestWithSecurity + // This task provisions the security plugin and a remote cluster, so their absence is a real + // breakage: mark both required so the security and cross-cluster ITs fail loudly instead of + // skipping. These properties are scoped to this task's test JVM and are intentionally NOT set + // on integTestRemote. + systemProperty 'tests.required.plugins', 'opensearch-security' + systemProperty 'tests.required.remote.cluster', 'true' + systemProperty "cluster.names", getClusters().stream().map(cluster -> cluster.getName()).collect(Collectors.joining(",")) @@ -756,6 +763,12 @@ yamlRestTest { integTest { useCluster testClusters.remoteCluster + // This task's cluster bundles the geospatial plugin (see testClusters.integTest above), so a + // missing geospatial plugin here is a real breakage, not an environment gap: mark it required + // so GeoIpFunctionsIT fails loudly instead of silently skipping. NOT set on integTestRemote, + // whose external cluster legitimately may not bundle geospatial. + systemProperty 'tests.required.plugins', 'opensearch-geospatial' + // Set properties for connection to clusters and between clusters doFirst { getClusters().forEach { cluster -> @@ -904,6 +917,11 @@ task tracingIntegTest(type: RestIntegTestTask) { dependsOn ':opensearch-sql-plugin:bundlePlugin' dependsOn downloadTelemetryOtel + // This task's cluster bundles telemetry-otel (see testClusters.tracingIntegTest above), so a + // missing plugin is a real breakage: mark it required so PPLTracingIT fails loudly instead of + // skipping. NOT set on integTestRemote. + systemProperty 'tests.required.plugins', 'telemetry-otel' + systemProperty 'tests.security.manager', 'false' systemProperty 'project.root', project.projectDir.absolutePath systemProperty 'tests.tracing.otlp.port', '4318' diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/tracing/PPLTracingIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/tracing/PPLTracingIT.java index 8e20b3fd1a5..e7d983cbc47 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/tracing/PPLTracingIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/tracing/PPLTracingIT.java @@ -28,6 +28,7 @@ import org.opensearch.client.ResponseException; import org.opensearch.sql.calcite.tracing.OtlpHttpTraceReceiver.Span; import org.opensearch.sql.ppl.PPLIntegTestCase; +import org.opensearch.sql.util.ClusterPlugins; public class PPLTracingIT extends PPLIntegTestCase { @@ -55,6 +56,10 @@ public static void stopReceiver() { @Override public void init() throws Exception { super.init(); + ClusterPlugins.requirePluginOrAssume( + client(), + ClusterPlugins.TELEMETRY_OTEL_PLUGIN, + "telemetry-otel plugin not installed on test cluster; skipping PPL tracing tests"); enableCalcite(); loadIndex(Index.BANK); receiver.clear(); diff --git a/integ-test/src/test/java/org/opensearch/sql/ppl/GeoIpFunctionsIT.java b/integ-test/src/test/java/org/opensearch/sql/ppl/GeoIpFunctionsIT.java index 14c6e2fb8f8..7ba4636e4c6 100644 --- a/integ-test/src/test/java/org/opensearch/sql/ppl/GeoIpFunctionsIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/ppl/GeoIpFunctionsIT.java @@ -29,6 +29,7 @@ import org.opensearch.common.xcontent.XContentFactory; import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.sql.util.ClusterPlugins; /** IP enrichment PPL request with OpenSearch Geo-sptial plugin */ public class GeoIpFunctionsIT extends PPLIntegTestCase { @@ -52,6 +53,11 @@ public class GeoIpFunctionsIT extends PPLIntegTestCase { @Override public void init() throws Exception { super.init(); + ClusterPlugins.requirePluginOrAssume( + client(), + ClusterPlugins.GEOSPATIAL_PLUGIN, + "opensearch-geospatial plugin not installed on test cluster; skipping geoip enrichment" + + " tests"); loadIndex(Index.GEOIP); if (!initialized) { // Create a new dataSource diff --git a/integ-test/src/test/java/org/opensearch/sql/security/CrossClusterTestBase.java b/integ-test/src/test/java/org/opensearch/sql/security/CrossClusterTestBase.java index dc4d7d0dafd..c0686d9e365 100644 --- a/integ-test/src/test/java/org/opensearch/sql/security/CrossClusterTestBase.java +++ b/integ-test/src/test/java/org/opensearch/sql/security/CrossClusterTestBase.java @@ -11,24 +11,42 @@ import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_MVEXPAND_EDGE_CASES; import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_TIME_DATA; +import org.junit.BeforeClass; import org.opensearch.sql.ppl.PPLIntegTestCase; +import org.opensearch.sql.util.ClusterPlugins; public class CrossClusterTestBase extends PPLIntegTestCase { static { - // find a remote cluster - String[] clusterNames = System.getProperty("cluster.names").split(","); - var remote = "remoteCluster"; - for (var cluster : clusterNames) { - if (cluster.startsWith("remote")) { - remote = cluster; - break; - } - } - REMOTE_CLUSTER = remote; + // find a remote cluster; the "cluster.names" property is only set by tasks that stand up a + // second (remote) cluster, so it may be absent on a plain single-cluster integTestRemote run. + // Selection (including whitespace trimming of comma-separated tokens) is delegated to the pure, + // unit-tested ClusterPlugins.selectRemoteCluster helper. + String remote = ClusterPlugins.selectRemoteCluster(System.getProperty("cluster.names")); + HAS_REMOTE_CLUSTER = remote != null; + REMOTE_CLUSTER = remote != null ? remote : ClusterPlugins.DEFAULT_REMOTE_CLUSTER; } + /** True only when a remote cluster is configured; cross-cluster tests are skipped otherwise. */ + public static final boolean HAS_REMOTE_CLUSTER; + public static final String REMOTE_CLUSTER; + @BeforeClass + public static void requireRemoteCluster() { + // On plain external-cluster runs a missing remote cluster is a skipped assumption. On the + // dedicated task that provisions the remote cluster (integTestWithSecurity sets + // -Dtests.required.remote.cluster=true) its absence is instead a hard failure, so a broken + // cross-cluster setup cannot masquerade as an all-green run. + ClusterPlugins.requireOrAssume( + HAS_REMOTE_CLUSTER, + Boolean.getBoolean(ClusterPlugins.REQUIRE_REMOTE_CLUSTER_PROPERTY), + "Cross-cluster search requires a configured remote cluster (-Dcluster.names must include a" + + " 'remote*' cluster); skipping", + "Cross-cluster search requires a configured remote cluster (-Dcluster.names must include a" + + " 'remote*' cluster) but none was found, and this task marks it required via -D" + + ClusterPlugins.REQUIRE_REMOTE_CLUSTER_PROPERTY); + } + protected static final String TEST_INDEX_BANK_REMOTE = REMOTE_CLUSTER + ":" + TEST_INDEX_BANK; protected static final String TEST_INDEX_DOG_REMOTE = REMOTE_CLUSTER + ":" + TEST_INDEX_DOG; protected static final String TEST_INDEX_DOG_MATCH_ALL_REMOTE = diff --git a/integ-test/src/test/java/org/opensearch/sql/security/PPLPermissionsIT.java b/integ-test/src/test/java/org/opensearch/sql/security/PPLPermissionsIT.java index dbe07b9c6a5..b61c4f0a4a1 100644 --- a/integ-test/src/test/java/org/opensearch/sql/security/PPLPermissionsIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/security/PPLPermissionsIT.java @@ -16,6 +16,7 @@ import org.json.JSONObject; import org.junit.Test; import org.opensearch.client.ResponseException; +import org.opensearch.sql.util.ClusterPlugins; /** * Integration tests for PPL permissions issue fix. Tests that PPL queries work correctly when users @@ -55,6 +56,10 @@ public class PPLPermissionsIT extends SecurityTestBase { @Override protected void init() throws Exception { + ClusterPlugins.requirePluginOrAssume( + client(), + ClusterPlugins.SECURITY_PLUGIN, + "opensearch-security plugin not installed on test cluster; skipping FGAC tests"); super.init(); createSecurityRolesAndUsers(); loadIndex(Index.BANK); diff --git a/integ-test/src/test/java/org/opensearch/sql/security/RestCommandSecurityIT.java b/integ-test/src/test/java/org/opensearch/sql/security/RestCommandSecurityIT.java index c4b5e726a54..fe126d43ba7 100644 --- a/integ-test/src/test/java/org/opensearch/sql/security/RestCommandSecurityIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/security/RestCommandSecurityIT.java @@ -13,6 +13,7 @@ import org.junit.Test; import org.opensearch.client.ResponseException; import org.opensearch.sql.legacy.TestUtils; +import org.opensearch.sql.util.ClusterPlugins; /** * Integration tests that the rest command is subject to the security plugin fine grained access @@ -32,6 +33,10 @@ public class RestCommandSecurityIT extends SecurityTestBase { @Override protected void init() throws Exception { + ClusterPlugins.requirePluginOrAssume( + client(), + ClusterPlugins.SECURITY_PLUGIN, + "opensearch-security plugin not installed on test cluster; skipping FGAC tests"); super.init(); setupRolesAndUsers(); enableCalcite(); diff --git a/integ-test/src/test/java/org/opensearch/sql/security/SQLCursorPermissionsIT.java b/integ-test/src/test/java/org/opensearch/sql/security/SQLCursorPermissionsIT.java index cacec3b2623..3e7ff640220 100644 --- a/integ-test/src/test/java/org/opensearch/sql/security/SQLCursorPermissionsIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/security/SQLCursorPermissionsIT.java @@ -17,6 +17,7 @@ import org.opensearch.client.Response; import org.opensearch.sql.legacy.SQLIntegTestCase; import org.opensearch.sql.legacy.TestUtils; +import org.opensearch.sql.util.ClusterPlugins; /** * Regression test for SQL cursor pagination under Fine-Grained Access Control (FGAC). @@ -35,6 +36,10 @@ public class SQLCursorPermissionsIT extends SQLIntegTestCase { @Override protected void init() throws Exception { + ClusterPlugins.requirePluginOrAssume( + client(), + ClusterPlugins.SECURITY_PLUGIN, + "opensearch-security plugin not installed on test cluster; skipping FGAC tests"); loadIndex(Index.ACCOUNT); createSecurityRolesAndUsers(); } diff --git a/integ-test/src/test/java/org/opensearch/sql/util/ClusterPlugins.java b/integ-test/src/test/java/org/opensearch/sql/util/ClusterPlugins.java new file mode 100644 index 00000000000..bd5a2057ea3 --- /dev/null +++ b/integ-test/src/test/java/org/opensearch/sql/util/ClusterPlugins.java @@ -0,0 +1,184 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.util; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.junit.Assert; +import org.junit.Assume; +import org.opensearch.client.Request; +import org.opensearch.client.Response; +import org.opensearch.client.RestClient; + +/** + * Runtime detection of optional cluster plugins, used to environment-gate integration tests that + * depend on plugins which are not always installed on the target cluster (for example the plain + * external {@code integTestRemote} cluster does not bundle security, and a multi-shard external + * cluster may lack geospatial or telemetry). + * + *
The probe reads {@code /_cat/plugins?h=component} and matches an installed component by exact + * line equality (never substring), so {@code opensearch-security} is not falsely reported present + * on a cluster that only has {@code opensearch-security-analytics}. + * + *
Gating uses two modes decided by the running Gradle task: + * + *
Extracted as a pure, side-effect-free function so the selection logic can be unit-tested + * directly, without mutating the {@code cluster.names} system property, reloading a static + * initializer, or loading the integration base class (whose {@code OpenSearchTestCase} ancestry + * runs randomized-runner bootstrap in its static initializer). Trimming ensures a property such + * as {@code "clusterA, remoteCluster"} still matches the {@code remote*} token despite the space + * that {@code split(",")} leaves attached. + * + * @param clusterNamesProperty raw value of the {@code cluster.names} system property (may be + * null) + * @return the selected remote cluster name, or {@code null} if none is present + */ + public static String selectRemoteCluster(String clusterNamesProperty) { + if (clusterNamesProperty == null || clusterNamesProperty.isBlank()) { + return null; + } + for (String cluster : clusterNamesProperty.split(",")) { + String trimmed = cluster.trim(); + if (trimmed.startsWith("remote")) { + return trimmed; + } + } + return null; + } + + /** + * Returns {@code true} when the given plugin component is installed on the cluster the client is + * pointed at, matching the {@code _cat/plugins?h=component} output line by line with exact + * equality. Any I/O or HTTP error (including a non-2xx {@link + * org.opensearch.client.ResponseException}, which extends {@link IOException}) propagates to the + * caller so a broken probe fails loudly rather than being misreported as "not installed". + * + * @param client REST client connected to the cluster under test + * @param pluginComponentName component name as it appears in {@code _cat/plugins?h=component} + * @throws IOException if the probe request fails or returns a non-2xx response + */ + public static boolean isPluginInstalled(RestClient client, String pluginComponentName) + throws IOException { + Response response = client.performRequest(new Request("GET", "/_cat/plugins?h=component")); + String body; + try (InputStream content = response.getEntity().getContent()) { + body = new String(content.readAllBytes(), StandardCharsets.UTF_8); + } + for (String line : body.split("\n")) { + if (line.trim().equals(pluginComponentName)) { + return true; + } + } + return false; + } + + /** + * Returns {@code true} when {@code pluginComponentName} is listed in {@link + * #REQUIRED_PLUGINS_PROPERTY}. Matching is exact per comma-separated token (whitespace trimmed). + */ + public static boolean isPluginRequired(String pluginComponentName) { + String required = System.getProperty(REQUIRED_PLUGINS_PROPERTY, ""); + for (String token : required.split(",")) { + if (token.trim().equals(pluginComponentName)) { + return true; + } + } + return false; + } + + /** + * Gates a test on the presence of {@code pluginComponentName}. If the current task marks the + * plugin required (see {@link #REQUIRED_PLUGINS_PROPERTY}) an absent plugin fails the test; + * otherwise it is skipped with {@code skipMessage}. Probe I/O errors propagate. + * + * @throws IOException if the underlying probe request fails + */ + public static void requirePluginOrAssume( + RestClient client, String pluginComponentName, String skipMessage) throws IOException { + requireOrAssume( + isPluginInstalled(client, pluginComponentName), + isPluginRequired(pluginComponentName), + skipMessage, + "Plugin '" + + pluginComponentName + + "' is marked required for this task via -D" + + REQUIRED_PLUGINS_PROPERTY + + " but is not installed on the target cluster"); + } + + /** + * Core gate primitive. When {@code required} is {@code true} an absent capability ({@code present + * == false}) fails via {@link Assert#assertTrue}; otherwise it is skipped via {@link + * Assume#assumeTrue}. When the capability is present the caller proceeds unchanged in both modes. + * + * @param present whether the capability (plugin, remote cluster, ...) is available + * @param required whether the current task declares the capability mandatory + * @param skipMessage assumption message used when optional and absent + * @param failMessage assertion message used when required and absent + */ + public static void requireOrAssume( + boolean present, boolean required, String skipMessage, String failMessage) { + if (required) { + Assert.assertTrue(failMessage, present); + } else { + Assume.assumeTrue(skipMessage, present); + } + } + + private ClusterPlugins() {} +} diff --git a/integ-test/src/test/java/org/opensearch/sql/util/ClusterPluginsTests.java b/integ-test/src/test/java/org/opensearch/sql/util/ClusterPluginsTests.java new file mode 100644 index 00000000000..e6ccd04feba --- /dev/null +++ b/integ-test/src/test/java/org/opensearch/sql/util/ClusterPluginsTests.java @@ -0,0 +1,293 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.sun.net.httpserver.HttpServer; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import org.apache.hc.core5.http.HttpHost; +import org.junit.After; +import org.junit.AssumptionViolatedException; +import org.junit.Test; +import org.opensearch.client.RestClient; + +/** + * Unit tests for {@link ClusterPlugins}. These exercise the real helper against an in-process + * {@link HttpServer} that emulates {@code _cat/plugins?h=component}, so nothing is re-implemented + * in the test. The server follows the same pattern as {@code OtlpHttpTraceReceiver}. + * + *
Coverage: + * + *