diff --git a/.github/workflows/sql-test-workflow.yml b/.github/workflows/sql-test-workflow.yml index cdc08c74803..abadaf2ace5 100644 --- a/.github/workflows/sql-test-workflow.yml +++ b/.github/workflows/sql-test-workflow.yml @@ -64,8 +64,9 @@ jobs: ./gradlew :core:jacocoTestCoverageVerification || echo "* Jacoco failed for core" >> report.log ./gradlew :protocol:jacocoTestCoverageVerification || echo "* Jacoco failed for protocol" >> report.log ./gradlew :opensearch-sql-plugin:jacocoTestCoverageVerification || echo "* Jacoco failed for plugin" >> report.log - # Misc tests + # Misc/Additional Integration tests ./gradlew :integ-test:integTest || echo "* Integration test failed" >> report.log + ./gradlew :integ-test:multiClusterSearch || echo "* Multi-Cluster Search tests failed" >> report.log ./gradlew :doctest:doctest || echo "* Doctest failed" >> report.log ./scripts/bwctest.sh || echo "* Backward compatibility test failed" >> report.log diff --git a/integ-test/build.gradle b/integ-test/build.gradle index 4a5f2015e0d..6587f500acf 100644 --- a/integ-test/build.gradle +++ b/integ-test/build.gradle @@ -285,6 +285,9 @@ integTest { exclude 'org/opensearch/sql/doctest/**/*IT.class' exclude 'org/opensearch/sql/correctness/**' + // Skip to run these IT tests on a different cluster + exclude 'org/opensearch/sql/multiClusterSearch/**' + // Explain IT is dependent on internal implementation of old engine so it's not necessary // to run these with new engine and not necessary to make this consistent with old engine. exclude 'org/opensearch/sql/legacy/ExplainIT.class' @@ -301,7 +304,7 @@ integTest { exclude 'org/opensearch/sql/jdbc/**' // Exclude this IT until running IT with security plugin enabled is ready - exclude 'org/opensearch/sql/ppl/CrossClusterSearchIT.class' + // exclude 'org/opensearch/sql/ppl/CrossClusterSearchIT.class' } @@ -481,6 +484,76 @@ task bwcTestSuite(type: StandaloneRestIntegTestTask) { dependsOn tasks.named("${baseName}#fullRestartClusterTask") } +testClusters { + multiClusterSearch { + testDistribution = "ARCHIVE" + numberOfNodes = 3 + plugin ":opensearch-sql-plugin" + } +} + +testClusters { + multiClusterSearchRemote { + testDistribution = "ARCHIVE" + plugin ":opensearch-sql-plugin" + } +} + +task multiClusterSearch(type: RestIntegTestTask) { + + useCluster testClusters.multiClusterSearch + useCluster testClusters.multiClusterSearchRemote + + testLogging { + events "passed", "skipped", "failed" + } + + // Set properties for connection to clusters and between clusters + doFirst { + getClusters().forEach { cluster -> + String allTransportSocketURI = cluster.nodes.stream().flatMap { node -> + node.getAllTransportPortURI().stream() + }.collect(Collectors.joining(",")) + String allHttpSocketURI = cluster.nodes.stream().flatMap { node -> + node.getAllHttpSocketURI().stream() + }.collect(Collectors.joining(",")) + + systemProperty "tests.rest.${cluster.name}.http_hosts", "${-> allHttpSocketURI}" + systemProperty "tests.rest.${cluster.name}.transport_hosts", "${-> allTransportSocketURI}" + } + } + + dependsOn ':opensearch-sql-plugin:bundlePlugin' + + systemProperty 'tests.security.manager', 'false' + systemProperty('project.root', project.projectDir.absolutePath) + + systemProperty "https", System.getProperty("https") + systemProperty "user", System.getProperty("user") + systemProperty "password", System.getProperty("password") + + // Set default query size limit + systemProperty 'defaultQuerySizeLimit', '10000' + + // Tell the test JVM if the cluster JVM is running under a debugger so that tests can use longer timeouts for + // requests. The 'doFirst' delays reading the debug setting on the cluster till execution time. + doFirst { + if (System.getProperty("debug-jvm") != null) { + setDebug(true); + } + systemProperty 'cluster.debug', getDebug() + } + + + if (System.getProperty("test.debug") != null) { + jvmArgs '-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5006' + } + + filter { + includeTestsMatching "org.opensearch.sql.multiClusterSearch.*IT" + } +} + def opensearch_tmp_dir = rootProject.file('build/private/es_tmp').absoluteFile opensearch_tmp_dir.mkdirs() diff --git a/integ-test/src/test/java/org/opensearch/sql/legacy/OpenSearchSQLRestTestCase.java b/integ-test/src/test/java/org/opensearch/sql/legacy/OpenSearchSQLRestTestCase.java index e057c589690..f59731456b0 100644 --- a/integ-test/src/test/java/org/opensearch/sql/legacy/OpenSearchSQLRestTestCase.java +++ b/integ-test/src/test/java/org/opensearch/sql/legacy/OpenSearchSQLRestTestCase.java @@ -105,10 +105,10 @@ protected RestClient buildClient(Settings settings, HttpHost[] hosts) throws IOE } // Modified from initClient in OpenSearchRestTestCase - public void initRemoteClient() throws IOException { + public void initRemoteClient(String clusterName) throws IOException { if (remoteClient == null) { assert remoteAdminClient == null; - String cluster = getTestRestCluster(REMOTE_CLUSTER); + String cluster = getTestRestCluster(clusterName); String[] stringUrls = cluster.split(","); List hosts = new ArrayList<>(stringUrls.length); for (String stringUrl : stringUrls) { @@ -252,14 +252,14 @@ protected static void configureHttpsClient(RestClientBuilder builder, Settings s * Initialize rest client to remote cluster, * and create a connection to it from the coordinating cluster. */ - public void configureMultiClusters() throws IOException { - initRemoteClient(); + public void configureMultiClusters(String clusterName) throws IOException { + initRemoteClient(clusterName); Request connectionRequest = new Request("PUT", "_cluster/settings"); String connectionSetting = "{\"persistent\": {\"cluster\": {\"remote\": {\"" - + REMOTE_CLUSTER + + clusterName + "\": {\"seeds\": [\"" - + getTestTransportCluster(REMOTE_CLUSTER).split(",")[0] + + getTestTransportCluster(clusterName).split(",")[0] + "\"]}}}}}"; connectionRequest.setJsonEntity(connectionSetting); adminClient().performRequest(connectionRequest); diff --git a/integ-test/src/test/java/org/opensearch/sql/multiClusterSearch/CrossClusterSearchIT.java b/integ-test/src/test/java/org/opensearch/sql/multiClusterSearch/CrossClusterSearchIT.java new file mode 100644 index 00000000000..b7b22ee958e --- /dev/null +++ b/integ-test/src/test/java/org/opensearch/sql/multiClusterSearch/CrossClusterSearchIT.java @@ -0,0 +1,147 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + + +package org.opensearch.sql.multiClusterSearch; + +import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_ACCOUNT; +import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_BANK; +import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_DOG; +import static org.opensearch.sql.util.MatcherUtils.columnName; +import static org.opensearch.sql.util.MatcherUtils.rows; +import static org.opensearch.sql.util.MatcherUtils.verifyColumn; +import static org.opensearch.sql.util.MatcherUtils.verifyDataRows; + +import java.io.IOException; +import org.json.JSONObject; +import org.junit.Rule; +import org.junit.jupiter.api.Test; +import org.junit.rules.ExpectedException; +import org.opensearch.client.ResponseException; +import org.opensearch.sql.ppl.PPLIntegTestCase; + +public class CrossClusterSearchIT extends PPLIntegTestCase { + + @Rule + public ExpectedException exceptionRule = ExpectedException.none(); + + public static final String REMOTE_CLUSTER = "multiClusterSearchRemote"; + + private final static String TEST_INDEX_BANK_REMOTE = REMOTE_CLUSTER + ":" + TEST_INDEX_BANK; + private final static String TEST_INDEX_DOG_REMOTE = REMOTE_CLUSTER + ":" + TEST_INDEX_DOG; + private final static String TEST_INDEX_DOG_MATCH_ALL_REMOTE = MATCH_ALL_REMOTE_CLUSTER + ":" + TEST_INDEX_DOG; + private final static String TEST_INDEX_ACCOUNT_REMOTE = REMOTE_CLUSTER + ":" + TEST_INDEX_ACCOUNT; + + @Override + public void init() throws IOException { + configureMultiClusters(REMOTE_CLUSTER); + loadIndex(Index.BANK); + loadIndex(Index.BANK, remoteClient()); + loadIndex(Index.DOG); + loadIndex(Index.DOG, remoteClient()); + loadIndex(Index.ACCOUNT, remoteClient()); + } + + @Test + public void testCrossClusterSearchAllFields() throws IOException { + JSONObject result = executeQuery(String.format("search source=%s", TEST_INDEX_DOG_REMOTE)); + verifyColumn(result, columnName("dog_name"), columnName("holdersName"), columnName("age")); + } + + @Test + public void testMatchAllCrossClusterSearchAllFields() throws IOException { + JSONObject result = executeQuery(String.format("search source=%s", TEST_INDEX_DOG_MATCH_ALL_REMOTE)); + verifyColumn(result, columnName("dog_name"), columnName("holdersName"), columnName("age")); + } + + @Test + public void testCrossClusterSearchWithoutLocalFieldMappingShouldFail() throws IOException { + exceptionRule.expect(ResponseException.class); + exceptionRule.expectMessage("400 Bad Request"); + exceptionRule.expectMessage("IndexNotFoundException"); + + executeQuery(String.format("search source=%s", TEST_INDEX_ACCOUNT_REMOTE)); + } + + @Test + public void testCrossClusterSearchCommandWithLogicalExpression() throws IOException { + JSONObject result = executeQuery(String.format( + "search source=%s firstname='Hattie' | fields firstname", TEST_INDEX_BANK_REMOTE)); + verifyDataRows(result, rows("Hattie")); + } + + @Test + public void testCrossClusterSearchMultiClusters() throws IOException { + JSONObject result = executeQuery(String.format( + "search source=%s,%s firstname='Hattie' | fields firstname", TEST_INDEX_BANK_REMOTE, TEST_INDEX_BANK)); + verifyDataRows(result, + rows("Hattie"), + rows("Hattie")); + } + + @Test + public void testCrossClusterDescribeAllFields() throws IOException { + JSONObject result = executeQuery(String.format("describe %s", TEST_INDEX_DOG_REMOTE)); + verifyColumn( + result, + columnName("TABLE_CAT"), + columnName("TABLE_SCHEM"), + columnName("TABLE_NAME"), + columnName("COLUMN_NAME"), + columnName("DATA_TYPE"), + columnName("TYPE_NAME"), + columnName("COLUMN_SIZE"), + columnName("BUFFER_LENGTH"), + columnName("DECIMAL_DIGITS"), + columnName("NUM_PREC_RADIX"), + columnName("NULLABLE"), + columnName("REMARKS"), + columnName("COLUMN_DEF"), + columnName("SQL_DATA_TYPE"), + columnName("SQL_DATETIME_SUB"), + columnName("CHAR_OCTET_LENGTH"), + columnName("ORDINAL_POSITION"), + columnName("IS_NULLABLE"), + columnName("SCOPE_CATALOG"), + columnName("SCOPE_SCHEMA"), + columnName("SCOPE_TABLE"), + columnName("SOURCE_DATA_TYPE"), + columnName("IS_AUTOINCREMENT"), + columnName("IS_GENERATEDCOLUMN") + ); + } + + @Test + public void testMatchAllCrossClusterDescribeAllFields() throws IOException { + JSONObject result = executeQuery(String.format("describe %s", TEST_INDEX_DOG_MATCH_ALL_REMOTE)); + verifyColumn( + result, + columnName("TABLE_CAT"), + columnName("TABLE_SCHEM"), + columnName("TABLE_NAME"), + columnName("COLUMN_NAME"), + columnName("DATA_TYPE"), + columnName("TYPE_NAME"), + columnName("COLUMN_SIZE"), + columnName("BUFFER_LENGTH"), + columnName("DECIMAL_DIGITS"), + columnName("NUM_PREC_RADIX"), + columnName("NULLABLE"), + columnName("REMARKS"), + columnName("COLUMN_DEF"), + columnName("SQL_DATA_TYPE"), + columnName("SQL_DATETIME_SUB"), + columnName("CHAR_OCTET_LENGTH"), + columnName("ORDINAL_POSITION"), + columnName("IS_NULLABLE"), + columnName("SCOPE_CATALOG"), + columnName("SCOPE_SCHEMA"), + columnName("SCOPE_TABLE"), + columnName("SOURCE_DATA_TYPE"), + columnName("IS_AUTOINCREMENT"), + columnName("IS_GENERATEDCOLUMN") + ); + } +} diff --git a/integ-test/src/test/java/org/opensearch/sql/ppl/CrossClusterSearchIT.java b/integ-test/src/test/java/org/opensearch/sql/ppl/CrossClusterSearchIT.java index a8e686a8930..8f5a464819f 100644 --- a/integ-test/src/test/java/org/opensearch/sql/ppl/CrossClusterSearchIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/ppl/CrossClusterSearchIT.java @@ -33,7 +33,7 @@ public class CrossClusterSearchIT extends PPLIntegTestCase { @Override public void init() throws IOException { - configureMultiClusters(); + configureMultiClusters(REMOTE_CLUSTER); loadIndex(Index.BANK); loadIndex(Index.BANK, remoteClient()); loadIndex(Index.DOG);