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
2 changes: 1 addition & 1 deletion .github/workflows/test-integration-unreleased.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ jobs:
- name: Run Integration Test
run: |
cd opensearch-java
./gradlew clean integrationTest -Dhttps=false
./gradlew clean integrationTest -Dhttps=false -Dtests.opensearch.testcontainers.enabled=false

- name: Upload Reports
if: failure()
Expand Down
25 changes: 1 addition & 24 deletions .github/workflows/test-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,27 +40,8 @@ jobs:
java-version: ${{ matrix.entry.java }}
distribution: 'temurin'
cache: 'gradle'
- name: Run Docker
run: |
echo "PASSWORD=admin" >> $GITHUB_ENV
docker info
docker compose --project-directory .ci/opensearch build --build-arg OPENSEARCH_VERSION=${{ matrix.entry.opensearch_version }}
docker compose --project-directory .ci/opensearch up -d
sleep 60

- name: Sets password (new versions)
run: |
VERSION_COMPONENTS=(${OPENSEARCH_VERSION//./ })
MAJOR_VERSION=${VERSION_COMPONENTS[0]}
MINOR_VERSION=${VERSION_COMPONENTS[1]}
if (( $MAJOR_VERSION > 2 || ( $MAJOR_VERSION == 2 && $MINOR_VERSION >= 12 ) )); then
echo "PASSWORD=0_aD^min_0" >> $GITHUB_ENV
fi
env:
OPENSEARCH_VERSION: ${{ matrix.entry.opensearch_version }}

- name: Run Integration Test
run: ./gradlew clean integrationTest -Dpassword=${{ env.PASSWORD }}
run: ./gradlew clean integrationTest -Dtests.opensearch.version=${{ matrix.entry.opensearch_version }}

- name: Upload Reports
if: failure()
Expand All @@ -69,7 +50,3 @@ jobs:
name: test-reports-os${{ matrix.entry.opensearch_version }}-java${{ matrix.entry.java }}
path: java-client/build/reports/
retention-days: 7

- name: Stop Docker
run: |
docker compose --project-directory .ci/opensearch down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- Bump `org.apache.httpcomponents.client5:httpclient5` from 5.6 to 5.6.1 ([#1967](https://github.com/opensearch-project/opensearch-java/pull/1967))

### Added
- Run Java client integration tests with a Testcontainers-managed OpenSearch instance by default ([#2033](https://github.com/opensearch-project/opensearch-java/pull/2033))
- Detect AWS SDK `Apache5HttpClient` in `AwsSdk2Transport` body-method guardrail ([#1903](https://github.com/opensearch-project/opensearch-java/pull/1970))
- Support Jackson 3.x release line ([#1810](https://github.com/opensearch-project/opensearch-java/pull/1810))
- Added `equals()` and `hashCode()` implementations to `FieldValue` ([#1998](https://github.com/opensearch-project/opensearch-java/pull/1998))
Expand Down
21 changes: 16 additions & 5 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,28 @@ To run unit tests for the java-client:

#### Integration Tests

To run integration tests for the java-client, start an OpenSearch cluster using docker and pass the OpenSearch version:
To run integration tests for the java-client:

```
docker-compose --project-directory .ci/opensearch build --build-arg OPENSEARCH_VERSION=1.3.0
docker-compose --project-directory .ci/opensearch up -d
./gradlew clean integrationTest
```

Run integration tests after starting OpenSearch cluster:
By default, the integration test task starts a single OpenSearch test container for the test JVM. To test against a specific OpenSearch image version, pass the OpenSearch version:

```
./gradlew clean integrationTest
./gradlew clean integrationTest -Dtests.opensearch.version=3.2.0
```

To pass the full official OpenSearch image name, use:

```
./gradlew clean integrationTest -Dtests.opensearch.image=opensearchproject/opensearch:3.2.0
```

To run against an already running cluster, disable the test container and pass the cluster endpoint if it is not `localhost:9200`:

```
./gradlew clean integrationTest -Dtests.opensearch.testcontainers.enabled=false -Dtests.rest.cluster=localhost:9200
```

#### AWS Transport Integration Tests
Expand Down
23 changes: 20 additions & 3 deletions java-client/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ tasks.build {
dependsOn("spotlessJavaCheck")
}

val opensearchVersion = "3.5.0-SNAPSHOT"
val opensearchDockerVersion = opensearchVersion.removeSuffix("-SNAPSHOT")

tasks.test {
systemProperty("tests.security.manager", "false")

Expand All @@ -168,6 +171,16 @@ val integrationTest = task<Test>("integrationTest") {
systemProperty("https", System.getProperty("https", "true"))
systemProperty("user", System.getProperty("user", "admin"))
systemProperty("password", System.getProperty("password", "admin"))
systemProperty(
"tests.opensearch.testcontainers.enabled",
System.getProperty("tests.opensearch.testcontainers.enabled", "true")
)
systemProperty(
"tests.opensearch.version",
System.getProperty("tests.opensearch.version", opensearchDockerVersion)
)
System.getProperty("tests.rest.cluster")?.let { systemProperty("tests.rest.cluster", it) }
System.getProperty("tests.opensearch.image")?.let { systemProperty("tests.opensearch.image", it) }
systemProperty("tests.awsSdk2support.domainHost",
System.getProperty("tests.awsSdk2support.domainHost", null))
systemProperty("tests.awsSdk2support.serviceName",
Expand All @@ -176,8 +189,6 @@ val integrationTest = task<Test>("integrationTest") {
System.getProperty("tests.awsSdk2support.domainRegion", "us-east-1"))
}

val opensearchVersion = "3.5.0-SNAPSHOT"

dependencies {
val jacksonVersion = "2.21.2"
val jacksonDatabindVersion = "2.21.2"
Expand Down Expand Up @@ -387,6 +398,12 @@ if (runtimeJavaVersion >= JavaVersion.VERSION_21) {
testImplementation("org.opensearch.test", "framework", opensearchVersion) {
exclude(group = "org.hamcrest")
}
// opensearch-testcontainers exposes OpenSearchContainer as a GenericContainer subclass, but publishes

@reta reta Jul 2, 2026

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.

Please just use:

Suggested change
// opensearch-testcontainers exposes OpenSearchContainer as a GenericContainer subclass, but publishes
testImplementation("org.opensearch:opensearch-testcontainers:4.1.0")
testImplementation"("org.testcontainers:testcontainers:2.0.4")
testImplementation"("org.testcontainers:testcontainers-junit-jupiter:2.0.4")

// Testcontainers as a runtime dependency. javac needs the superclass API for inherited methods.
"java21CompileOnly"("org.testcontainers:testcontainers:2.0.3") {
isTransitive = false
}
"java21Implementation"("org.opensearch:opensearch-testcontainers:4.1.0")
}

tasks.named<JavaCompile>("compileJava21Java") {
Expand All @@ -408,4 +425,4 @@ if (runtimeJavaVersion >= JavaVersion.VERSION_21) {
testClassesDirs += java21.output.classesDirs
classpath = sourceSets["java21"].runtimeClasspath
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

package org.opensearch.client.opensearch.integTest;

import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
Expand All @@ -30,6 +31,7 @@
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.opensearch.Version;
import org.opensearch.client.RestClient;
import org.opensearch.client.RestClientBuilder;
Expand All @@ -45,6 +47,7 @@
import org.opensearch.common.settings.Settings;
import org.opensearch.test.rest.OpenSearchRestTestCase;

@ThreadLeakFilters(filters = TestcontainersThreadFilter.class)
public abstract class OpenSearchJavaClientTestCase extends OpenSearchRestTestCase implements OpenSearchTransportSupport {
private static final List<String> systemIndices = List.of(
".opensearch-observability",
Expand All @@ -59,6 +62,12 @@ public abstract class OpenSearchJavaClientTestCase extends OpenSearchRestTestCas
private static TreeSet<Version> nodeVersions;
private static List<HttpHost> clusterHosts;

// Superclass setup reads tests.rest.cluster before subclass @Before methods run.
@BeforeClass
public static void startOpenSearchTestContainer() {
OpenSearchTestContainer.startIfNeeded();
}

@Before
public void initJavaClient() throws IOException {
if (javaClient == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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.client.opensearch.integTest;

import org.opensearch.testcontainers.OpenSearchContainer;

final class OpenSearchTestContainer {

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.

Please move under java-client/src/test/java21. Also, we should be using JUnit Jupiter extentions mechanism with proper pre / post lifycycle (see please examples here https://www.baeldung.com/junit-5-registerextension-annotation)

static final String ENABLED_PROPERTY = "tests.opensearch.testcontainers.enabled";
static final String VERSION_PROPERTY = "tests.opensearch.version";
static final String IMAGE_PROPERTY = "tests.opensearch.image";
static final String CLUSTER_PROPERTY = "tests.rest.cluster";
static final String HTTPS_PROPERTY = "https";
static final String USER_PROPERTY = "user";
static final String PASSWORD_PROPERTY = "password";

private static final String DEFAULT_IMAGE = "opensearchproject/opensearch";
private static final String DEFAULT_ADMIN_PASSWORD = "admin";
private static final int HTTP_PORT = 9200;

private static OpenSearchContainer<?> container;

private OpenSearchTestContainer() {}

static synchronized void startIfNeeded() {
if (hasText(System.getProperty(CLUSTER_PROPERTY)) || !testcontainersEnabled()) {
return;
}

if (container == null) {
String version = System.getProperty(VERSION_PROPERTY);
String imageName = resolveImageName(version, System.getProperty(IMAGE_PROPERTY));
OpenSearchContainer<?> openSearch = createContainer(imageName);
openSearch.start();
container = openSearch;
}

System.setProperty(CLUSTER_PROPERTY, container.getHost() + ":" + container.getMappedPort(HTTP_PORT));
System.setProperty(HTTPS_PROPERTY, Boolean.toString(container.isSecurityEnabled()));
System.setProperty(USER_PROPERTY, container.getUsername());
System.setProperty(PASSWORD_PROPERTY, container.getPassword());
}

private static OpenSearchContainer<?> createContainer(String imageName) {
OpenSearchContainer<?> openSearch = new OpenSearchContainer<>(imageName).withSecurityEnabled()
.withEnv("bootstrap.memory_lock", "true")
.withEnv("cluster.routing.allocation.disk.threshold_enabled", "false");

String configuredPassword = System.getProperty(PASSWORD_PROPERTY);
if (hasText(configuredPassword) && !DEFAULT_ADMIN_PASSWORD.equals(configuredPassword)) {
openSearch.withEnv("OPENSEARCH_INITIAL_ADMIN_PASSWORD", configuredPassword);
}

return openSearch;
}

static String resolveImageName(String version, String image) {
if (hasText(image)) {
return image;
}
if (!hasText(version)) {
throw new IllegalStateException("Missing " + VERSION_PROPERTY + " for OpenSearch Testcontainers image");
}
return DEFAULT_IMAGE + ":" + version;
}

private static boolean testcontainersEnabled() {
return Boolean.parseBoolean(System.getProperty(ENABLED_PROPERTY, "true"));
}

private static boolean hasText(String value) {
return value != null && !value.isBlank();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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.client.opensearch.integTest;

import static org.junit.Assert.assertEquals;

import org.junit.Test;

public class OpenSearchTestContainerTests {

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.

I don't think we need these tests - opensearch-testcontainers has bunch of them


@Test
public void defaultImageUsesOfficialOpenSearchImage() {
assertEquals("opensearchproject/opensearch:3.2.0", OpenSearchTestContainer.resolveImageName("3.2.0", null));
}

@Test
public void configuredImageTakesPrecedenceOverVersion() {
assertEquals(
"opensearchproject/opensearch:2.19.2",
OpenSearchTestContainer.resolveImageName("3.2.0", "opensearchproject/opensearch:2.19.2")
);
}

@Test
public void missingVersionWithoutCustomImageFailsFast() {
try {
OpenSearchTestContainer.resolveImageName(null, null);
} catch (IllegalStateException e) {
assertEquals("Missing tests.opensearch.version for OpenSearch Testcontainers image", e.getMessage());
return;
}

throw new AssertionError("Expected missing OpenSearch version to fail");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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.client.opensearch.integTest;

import com.carrotsearch.randomizedtesting.ThreadFilter;

public final class TestcontainersThreadFilter implements ThreadFilter {

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.

Please remove, we should not see any leaking threads after https://github.com/opensearch-project/opensearch-java/pull/2033/changes#r3514648791 (the extension part)

@Override
public boolean reject(Thread thread) {
String name = thread.getName();
// Testcontainers owns these helper threads and Ryuk cleans them up after the JVM exits.
return "testcontainers-ryuk".equals(name) || name.startsWith("testcontainers-pull-watchdog-") || name.startsWith("ducttape-");
}
}
Loading