Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions integ-test/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,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(","))

Expand Down Expand Up @@ -754,6 +761,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 ->
Expand Down Expand Up @@ -902,6 +915,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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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();
}
Expand Down
184 changes: 184 additions & 0 deletions integ-test/src/test/java/org/opensearch/sql/util/ClusterPlugins.java
Original file line number Diff line number Diff line change
@@ -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).
*
* <p>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}.
*
* <p>Gating uses two modes decided by the running Gradle task:
*
* <ul>
* <li><b>Optional</b> (default, e.g. {@code integTestRemote}): a missing plugin is reported as a
* skipped assumption via {@link org.junit.Assume#assumeTrue}. Plain external clusters are the
* intended skip environment.
* <li><b>Required</b> (dedicated tasks that provision the plugin, e.g. {@code
* integTestWithSecurity}, {@code tracingIntegTest}, or the local {@code integTest} that
* bundles geospatial): a missing plugin is a hard {@link org.junit.Assert#assertTrue}
* failure, so a broken plugin stack never silently vanishes into a green run.
* </ul>
*
* The required set is declared by the task via the {@link #REQUIRED_PLUGINS_PROPERTY} system
* property; tasks leave it unset to keep the optional (skip-on-absence) behaviour.
*/
public final class ClusterPlugins {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thinking is it possible to make this like our capability annotation, like `@RequiresEnv("xxx-plugin") I see currently CapabilityRule is initialized in base class and may access to the client?


/** Component name of the OpenSearch security plugin as reported by {@code _cat/plugins}. */
public static final String SECURITY_PLUGIN = "opensearch-security";

/** Component name of the OpenSearch geospatial plugin (provides ip2geo / geoip enrichment). */
public static final String GEOSPATIAL_PLUGIN = "opensearch-geospatial";

/** Component name of the OpenTelemetry exporter plugin that backs PPL query tracing. */
public static final String TELEMETRY_OTEL_PLUGIN = "telemetry-otel";

/**
* Comma-separated list of plugin component names the current Gradle test task declares mandatory.
* When a plugin appears here, {@link #requirePluginOrAssume} turns a missing plugin into a hard
* assertion failure instead of a skipped assumption. Tasks that run against arbitrary external
* clusters (for example {@code integTestRemote}) leave this unset so absence remains a skip.
*/
public static final String REQUIRED_PLUGINS_PROPERTY = "tests.required.plugins";

/**
* Boolean system property set by dedicated tasks that provision a remote (cross-cluster) cluster.
* When {@code true}, an absent remote cluster is a hard failure rather than a skipped assumption.
*/
public static final String REQUIRE_REMOTE_CLUSTER_PROPERTY = "tests.required.remote.cluster";

/**
* Name used for {@code REMOTE_CLUSTER} when no remote cluster is configured. It keeps the derived
* {@code REMOTE_CLUSTER} constant (and the {@code <cluster>:<index>} names built from it)
* non-null so cross-cluster test classes still load; the {@code @BeforeClass} gate skips or fails
* the tests before any body that would actually query this non-existent cluster runs.
*/
public static final String DEFAULT_REMOTE_CLUSTER = "remoteCluster";

/**
* Selects the remote cluster name from a comma-separated {@code cluster.names} property value.
* Returns the first token (leading/trailing whitespace trimmed) that starts with {@code
* "remote"}, or {@code null} when the value is {@code null}, blank, or contains no such token.
*
* <p>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() {}
}
Loading
Loading