diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3a7ea347cb3..9131ce06da3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,23 +1,13 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: // https://github.com/microsoft/vscode-dev-containers/tree/v0.241.1/containers/java-8 { - "name": "Java 8", - "image": "mcr.microsoft.com/devcontainers/java:1-8-bullseye", + "name": "Java 17", + "image": "mcr.microsoft.com/devcontainers/java:0-17", // Configure tool-specific properties. "customizations": { // Configure properties specific to VS Code. "vscode": { - // Set *default* container specific settings.json values on container create. - "settings": { - "java.import.gradle.java.home": "/usr/local/sdkman/candidates/java/current", - "java.configuration.runtimes": [{ - "default": true, - "name": "JavaSE-1.8", - "path": "/usr/local/sdkman/candidates/java/current" - }] - }, - // Add the IDs of extensions you want installed when the container is created. "extensions": [ "vscjava.vscode-java-pack" diff --git a/.github/actions/setup-build/action.yml b/.github/actions/setup-build/action.yml index 23204529408..0ab491cbb20 100644 --- a/.github/actions/setup-build/action.yml +++ b/.github/actions/setup-build/action.yml @@ -1,9 +1,16 @@ name: Set up Build description: Sets up Build +inputs: + java-version: + description: 'The Java version to set up' + required: true + default: '17' runs: using: "composite" steps: - uses: ./.github/actions/setup-java + with: + java-version: ${{ inputs.java-version }} - name: Clear existing docker image cache shell: bash run: docker image prune -af diff --git a/.github/actions/setup-gradle/action.yml b/.github/actions/setup-gradle/action.yml index 3cb582c0cf8..c97d776e04f 100644 --- a/.github/actions/setup-gradle/action.yml +++ b/.github/actions/setup-gradle/action.yml @@ -4,10 +4,9 @@ runs: using: "composite" steps: - name: Setup Gradle Build Action - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 with: gradle-home-cache-includes: | caches notifications jdks - gradle-home-cache-cleanup: true diff --git a/.github/actions/setup-java/action.yml b/.github/actions/setup-java/action.yml index 053b7ebe23b..ee8c3323fc1 100644 --- a/.github/actions/setup-java/action.yml +++ b/.github/actions/setup-java/action.yml @@ -1,9 +1,14 @@ name: Set up Java description: Sets up Java version +inputs: + java-version: + description: 'The Java version to set up' + required: true + default: '17' runs: using: "composite" steps: - uses: actions/setup-java@v4 with: - java-version: '8' + java-version: ${{ inputs.java-version }} distribution: temurin diff --git a/.github/settings.yml b/.github/settings.yml index b10394a894a..cc9f477b19d 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -400,7 +400,7 @@ branches: # Required. Require branches to be up to date before merging. strict: true # Required. The list of status checks to require in order to merge into this branch - contexts: ["core", "check_docs_examples (:docs:examples:check)", "in-docker_test", "ci/circleci: minimal_core", "test"] + contexts: ["core (17)", "core (21)", "check_docs_examples (:docs:examples:check)", "in-docker_test", "ci/circleci: minimal_core", "test"] # Required. Enforce all configured restrictions for administrators. Set to true to enforce required status checks for repository administrators. Set to null to disable. enforce_admins: false # Prevent merge commits from being pushed to matching branches diff --git a/.github/workflows/ci-docker-wormhole.yml b/.github/workflows/ci-docker-wormhole.yml index 23a491ba512..8204bd0454b 100644 --- a/.github/workflows/ci-docker-wormhole.yml +++ b/.github/workflows/ci-docker-wormhole.yml @@ -53,5 +53,5 @@ jobs: -v "$PWD:$PWD" \ -w "$PWD" \ -e AUTO_APPLY_GIT_HOOKS=false \ - openjdk:8-jdk-alpine \ + eclipse-temurin:17-jdk-alpine \ ./gradlew --no-daemon --continue --scan testcontainers:test --tests '*GenericContainerRuleTest' diff --git a/.github/workflows/ci-rootless.yml b/.github/workflows/ci-rootless.yml index 21ff4f793c6..e5c9afd1dbc 100644 --- a/.github/workflows/ci-rootless.yml +++ b/.github/workflows/ci-rootless.yml @@ -48,11 +48,11 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup rootless Docker - uses: ScribeMD/rootless-docker@0.2.2 - - name: Remove Docket root socket - run: sudo rm -rf /var/run/docker.sock + uses: docker/setup-docker-action@v4 + with: + rootless: true - name: Setup Gradle Build Action - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 - name: Build with Gradle run: ./gradlew --no-daemon --scan testcontainers:test --tests '*GenericContainerRuleTest' - uses: ./.github/actions/setup-junit-report diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 45b47f364ad..3dd919bc919 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -53,6 +53,7 @@ jobs: checks: write steps: - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-build - name: Build with Gradle run: ./gradlew.bat cleanTest testcontainers:test --no-daemon --continue --scan --no-build-cache - uses: ./.github/actions/setup-junit-report @@ -82,6 +83,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} repository: ${{ github.event.client_payload.pull_request.head.repo.full_name }} ref: ${{ github.event.client_payload.pull_request.head.ref }} + - uses: ./.github/actions/setup-build - name: Build with Gradle run: ./gradlew.bat cleanTest testcontainers:test --no-daemon --continue --scan --no-build-cache - uses: ./.github/actions/setup-junit-report diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fcdebaee5cc..ed96bfdcfc7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,9 +50,14 @@ jobs: runs-on: ubuntu-22.04 permissions: checks: write + strategy: + matrix: + java: [ '17', '21' ] steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup-build + with: + java-version: ${{ matrix.java }} - name: Build and test with Gradle run: | ./gradlew :testcontainers:check --no-daemon --continue --scan @@ -86,7 +91,7 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/setup-java - name: Setup Gradle Build Action - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 - id: set-matrix env: # Since we override the tests executor, @@ -121,7 +126,7 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/setup-java - name: Setup Gradle Build Action - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 - id: set-matrix working-directory: ./examples/ env: @@ -158,7 +163,7 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/setup-java - name: Setup Gradle Build Action - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 - id: set-matrix env: # Since we override the tests executor, diff --git a/.github/workflows/moby-latest.yml b/.github/workflows/moby-latest.yml index 3ebfce168c6..28529f882c6 100644 --- a/.github/workflows/moby-latest.yml +++ b/.github/workflows/moby-latest.yml @@ -13,8 +13,11 @@ jobs: test_docker: strategy: matrix: - install-docker-type: ["STABLE", "ROOTLESS", "ROOTFUL"] - name: "Core tests using Docker ${{ matrix.install-docker-type }}" + include: + - { install-docker-type: "STABLE", channel: stable, rootless: false } + - { install-docker-type: "ROOTLESS", channel: stable, rootless: true } + - { install-docker-type: "ROOTFUL", channel: test, rootless: false } + name: "Core tests using Docker ${{ matrix.install-docker-type }} (channel ${{ matrix.channel }})" runs-on: ubuntu-22.04 continue-on-error: true steps: @@ -22,26 +25,19 @@ jobs: - uses: ./.github/actions/setup-build - name: Install Stable Docker - if: ${{ matrix.install-docker-type == 'STABLE' }} - run: curl https://get.docker.com - - - name: Install Docker from the TEST channel - if: ${{ matrix.install-docker-type == 'ROOTFUL' }} - run: curl https://get.docker.com | CHANNEL=test sh - - - name: Setup rootless Docker - if: ${{ matrix.install-docker-type == 'ROOTLESS' }} - uses: ScribeMD/rootless-docker@6bd157a512c2fafa4e0243a8aa87d964eb890886 # v0.2.2 - - - name: Remove Docker root socket - if: ${{ matrix.install-docker-type == 'ROOTLESS' }} - run: sudo rm -rf /var/run/docker.sock + id: setup_docker + uses: docker/setup-docker-action@v4 + with: + channel: ${{ matrix.channel }} + rootless: ${{ matrix.rootless }} - name: Check Docker version run: docker version - name: Build with Gradle run: ./gradlew cleanTest --no-daemon --continue --scan -Dscan.tag.DOCKER_${{ matrix.install-docker-type }} testcontainers:test -Dorg.gradle.caching=false + env: + DOCKER_HOST: ${{steps.setup_docker.outputs.sock}} - uses: ./.github/actions/setup-junit-report - name: Notify to Slack on failures diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 79a0d6856b1..4586838d0d9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: run: docker image prune -af - name: Setup Gradle Build Action - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 - name: Run Gradle Build run: ./gradlew build --scan --no-daemon -i -x test diff --git a/.sdkmanrc b/.sdkmanrc index c09dba9cac1..b9fe931c084 100644 --- a/.sdkmanrc +++ b/.sdkmanrc @@ -1,3 +1,3 @@ # Enable auto-env through the sdkman_auto_env config # Add key=value pairs of SDKs to use below -java=8.0.372-tem +java=17.0.12-tem diff --git a/README.md b/README.md index ab001d6537b..22c800f03be 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.testcontainers/testcontainers/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.testcontainers/testcontainers) +[![Netlify Status](https://api.netlify.com/api/v1/badges/189f28a2-7faa-42ff-b03c-738142079cc9/deploy-status)](https://app.netlify.com/sites/testcontainers/deploys) + [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=33816473&machine=standardLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=EastUs) [![Revved up by Develocity](https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.testcontainers.org/scans) diff --git a/build.gradle b/build.gradle index af1f408b35e..7f4bad6e835 100644 --- a/build.gradle +++ b/build.gradle @@ -12,9 +12,9 @@ buildscript { plugins { id 'io.franzbecker.gradle-lombok' version '5.0.0' - id 'com.github.johnrengelman.shadow' version '8.1.1' + id 'com.gradleup.shadow' version '8.3.0' id 'me.champeau.gradle.japicmp' version '0.4.3' apply false - id 'com.diffplug.spotless' version '6.13.0' apply false + id 'com.diffplug.spotless' version '6.22.0' apply false } apply from: "$rootDir/gradle/ci-support.gradle" @@ -26,6 +26,7 @@ captainHook { } subprojects { + apply plugin: 'java' apply plugin: 'java-library' apply plugin: 'idea' apply plugin: 'io.franzbecker.gradle-lombok' @@ -35,9 +36,17 @@ subprojects { group = "org.testcontainers" - sourceCompatibility = 1.8 - targetCompatibility = 1.8 - compileJava.options.encoding = 'UTF-8' + java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } + } + + tasks.withType(JavaCompile) { + options.release.set(8) + options.encoding = 'UTF-8' + } + compileTestJava.options.encoding = 'UTF-8' javadoc.options.encoding = 'UTF-8' @@ -126,7 +135,7 @@ subprojects { } checkstyle { - toolVersion = "9.3" + toolVersion = "10.12.4" configFile = rootProject.file('config/checkstyle/checkstyle.xml') } } diff --git a/core/build.gradle b/core/build.gradle index a653c7923ba..835dcd8a868 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'com.github.johnrengelman.shadow' +apply plugin: 'com.gradleup.shadow' description = "Testcontainers Core" @@ -63,8 +63,8 @@ tasks.japicmp { configurations.all { resolutionStrategy { // use lower Jackson version - force 'com.fasterxml.jackson.core:jackson-databind:2.8.8' - force 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.8.8' + force 'com.fasterxml.jackson.core:jackson-databind:2.18.2' + force 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2' } } @@ -88,8 +88,8 @@ dependencies { shaded 'org.awaitility:awaitility:4.2.0' - api platform('com.github.docker-java:docker-java-bom:3.4.0') - shaded platform('com.github.docker-java:docker-java-bom:3.4.0') + api platform('com.github.docker-java:docker-java-bom:3.4.1') + shaded platform('com.github.docker-java:docker-java-bom:3.4.1') api "com.github.docker-java:docker-java-api" @@ -100,7 +100,7 @@ dependencies { api 'com.github.docker-java:docker-java-transport-zerodep' shaded 'com.google.guava:guava:33.3.1-jre' - shaded "org.yaml:snakeyaml:2.2" + shaded "org.yaml:snakeyaml:2.3" shaded 'org.glassfish.main.external:trilead-ssh2-repackaged:4.1.2' diff --git a/core/src/main/java/org/testcontainers/containers/DockerCompose.java b/core/src/main/java/org/testcontainers/containers/DockerCompose.java index e1d2b3ead64..82b8a6d5142 100644 --- a/core/src/main/java/org/testcontainers/containers/DockerCompose.java +++ b/core/src/main/java/org/testcontainers/containers/DockerCompose.java @@ -4,6 +4,7 @@ interface DockerCompose { String ENV_PROJECT_NAME = "COMPOSE_PROJECT_NAME"; + String ENV_COMPOSE_FILE = "COMPOSE_FILE"; DockerCompose withCommand(String cmd); diff --git a/core/src/main/java/org/testcontainers/images/builder/Transferable.java b/core/src/main/java/org/testcontainers/images/builder/Transferable.java index 292a70d6ac7..32bf199b8a8 100644 --- a/core/src/main/java/org/testcontainers/images/builder/Transferable.java +++ b/core/src/main/java/org/testcontainers/images/builder/Transferable.java @@ -10,6 +10,7 @@ public interface Transferable { int DEFAULT_FILE_MODE = 0100644; + int DEFAULT_DIR_MODE = 040755; static Transferable of(String string) { diff --git a/core/src/main/java/org/testcontainers/utility/RegistryAuthLocator.java b/core/src/main/java/org/testcontainers/utility/RegistryAuthLocator.java index 00dd8184f51..3caabb6f0d4 100644 --- a/core/src/main/java/org/testcontainers/utility/RegistryAuthLocator.java +++ b/core/src/main/java/org/testcontainers/utility/RegistryAuthLocator.java @@ -12,6 +12,7 @@ import org.testcontainers.DockerClientFactory; import org.zeroturnaround.exec.InvalidResultException; import org.zeroturnaround.exec.ProcessExecutor; +import org.zeroturnaround.exec.ProcessResult; import org.zeroturnaround.exec.stream.LogOutputStream; import java.io.ByteArrayInputStream; @@ -284,11 +285,14 @@ private AuthConfig runCredentialProvider(String hostName, String helperOrStoreNa try { data = runCredentialProgram(hostName, credentialProgramName); - if (data.getStderr() != null && !data.getStderr().isEmpty()) { - final String responseErrorMsg = data.getStderr(); + if (data.getExitValue() == 1) { + final String responseErrorMsg = data.getStdout(); if (!StringUtils.isBlank(responseErrorMsg)) { - String credentialsNotFoundMsg = getGenericCredentialsNotFoundMsg(credentialProgramName); + String credentialsNotFoundMsg = getGenericCredentialsNotFoundMsg( + responseErrorMsg, + credentialProgramName + ); if (credentialsNotFoundMsg != null && credentialsNotFoundMsg.equals(responseErrorMsg)) { log.info( "Credential helper/store ({}) does not have credentials for {}", @@ -300,15 +304,16 @@ private AuthConfig runCredentialProvider(String hostName, String helperOrStoreNa } log.debug( - "Failure running docker credential helper/store ({}) with output '{}'", + "Failure running docker credential helper/store ({}) with output '{}' and error '{}'", credentialProgramName, - responseErrorMsg + responseErrorMsg, + data.getStderr() ); } else { log.debug("Failure running docker credential helper/store ({})", credentialProgramName); } - throw new InvalidResultException(data.getStderr(), null); + throw new InvalidResultException(data.getStdout(), null); } } catch (Exception e) { log.debug("Failure running docker credential helper/store ({})", credentialProgramName); @@ -344,48 +349,13 @@ private String effectiveRegistryName(DockerImageName dockerImageName) { ); } - private String getGenericCredentialsNotFoundMsg(String credentialHelperName) { + private String getGenericCredentialsNotFoundMsg(String credentialsNotFoundMsg, String credentialHelperName) { if (!CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE.containsKey(credentialHelperName)) { - String credentialsNotFoundMsg = discoverCredentialsHelperNotFoundMessage(credentialHelperName); - if (!StringUtils.isBlank(credentialsNotFoundMsg)) { - CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE.put(credentialHelperName, credentialsNotFoundMsg); - } + CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE.put(credentialHelperName, credentialsNotFoundMsg); } - return CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE.get(credentialHelperName); } - private String discoverCredentialsHelperNotFoundMessage(String credentialHelperName) { - // will do fake call to given credential helper to find out with which message - // it response when there are no credentials for given hostName - - // hostName should be valid, but most probably not existing - // IF its not enough, then should probably run 'list' command first to be sure... - final String notExistentFakeHostName = "https://not.a.real.registry/url"; - - String credentialsNotFoundMsg = null; - try { - CredentialOutput data = runCredentialProgram(notExistentFakeHostName, credentialHelperName); - - if (data.getStderr() != null && !data.getStderr().isEmpty()) { - credentialsNotFoundMsg = data.getStderr(); - - log.debug( - "Got credentials not found error message from docker credential helper - {}", - credentialsNotFoundMsg - ); - } - } catch (Exception e) { - log.warn( - "Failure running docker credential helper ({}) with fake call, expected 'credentials not found' response. Exception message: {}", - credentialHelperName, - e.getMessage() - ); - } - - return credentialsNotFoundMsg; - } - private CredentialOutput runCredentialProgram(String hostName, String credentialHelperName) throws InterruptedException, TimeoutException, IOException { String[] command = SystemUtils.IS_OS_WINDOWS @@ -395,50 +365,55 @@ private CredentialOutput runCredentialProgram(String hostName, String credential StringBuffer stdout = new StringBuffer(); StringBuffer stderr = new StringBuffer(); - try { - new ProcessExecutor() - .command(command) - .redirectInput(new ByteArrayInputStream(hostName.getBytes())) - .redirectOutput( - new LogOutputStream() { - @Override - protected void processLine(String line) { - stdout.append(line).append(System.lineSeparator()); - } + ProcessResult processResult = new ProcessExecutor() + .command(command) + .redirectInput(new ByteArrayInputStream(hostName.getBytes())) + .redirectOutput( + new LogOutputStream() { + @Override + protected void processLine(String line) { + stdout.append(line).append(System.lineSeparator()); } - ) - .redirectError( - new LogOutputStream() { - @Override - protected void processLine(String line) { - stderr.append(line).append(System.lineSeparator()); - } + } + ) + .redirectError( + new LogOutputStream() { + @Override + protected void processLine(String line) { + stderr.append(line).append(System.lineSeparator()); } - ) - .exitValueNormal() - .timeout(30, TimeUnit.SECONDS) - .execute(); - } catch (InvalidResultException e) {} + } + ) + .timeout(30, TimeUnit.SECONDS) + .execute(); + int exitValue = processResult.getExitValue(); - return new CredentialOutput(stdout.toString(), stderr.toString()); + return new CredentialOutput(exitValue, stdout.toString(), stderr.toString()); } static class CredentialOutput { + private final int exitValue; + private final String stdout; private final String stderr; - public CredentialOutput(String stdout, String stderr) { + public CredentialOutput(int exitValue, String stdout, String stderr) { + this.exitValue = exitValue; this.stdout = stdout.trim(); this.stderr = stderr.trim(); } - public String getStdout() { + int getExitValue() { + return this.exitValue; + } + + String getStdout() { return this.stdout; } - public String getStderr() { + String getStderr() { return this.stderr; } } diff --git a/core/src/test/java/org/testcontainers/TestImages.java b/core/src/test/java/org/testcontainers/TestImages.java index d8209e29507..0d052f8e963 100644 --- a/core/src/test/java/org/testcontainers/TestImages.java +++ b/core/src/test/java/org/testcontainers/TestImages.java @@ -4,9 +4,14 @@ public interface TestImages { DockerImageName REDIS_IMAGE = DockerImageName.parse("redis:6-alpine"); + DockerImageName RABBITMQ_IMAGE = DockerImageName.parse("rabbitmq:3.7.25"); + DockerImageName MONGODB_IMAGE = DockerImageName.parse("mongo:4.4"); + DockerImageName ALPINE_IMAGE = DockerImageName.parse("alpine:3.17"); + DockerImageName DOCKER_REGISTRY_IMAGE = DockerImageName.parse("registry:2.7.0"); + DockerImageName TINY_IMAGE = DockerImageName.parse("alpine:3.17"); } diff --git a/core/src/test/java/org/testcontainers/containers/ComposeProfilesOptionTest.java b/core/src/test/java/org/testcontainers/containers/ComposeProfilesOptionTest.java index 71aba726dda..8fc662a118e 100644 --- a/core/src/test/java/org/testcontainers/containers/ComposeProfilesOptionTest.java +++ b/core/src/test/java/org/testcontainers/containers/ComposeProfilesOptionTest.java @@ -37,9 +37,11 @@ public void setUp() { @Test public void testProfileOption() { try ( + // composeContainerWithLocalCompose { ComposeContainer compose = new ComposeContainer(COMPOSE_FILE) - .withOptions("--profile=cache") .withLocalCompose(true) + // } + .withOptions("--profile=cache") ) { compose.start(); assertThat(compose.listChildContainers()).hasSize(1); diff --git a/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java b/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java index acb24cf0d68..c1c4ff3818e 100644 --- a/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java +++ b/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java @@ -52,7 +52,7 @@ public class GenericContainerTest { public void shouldReportOOMAfterWait() { Info info = DockerClientFactory.instance().client().infoCmd().exec(); // Poor man's rootless Docker detection :D - Assumptions.assumeThat(info.getSecurityOptions()).doesNotContain("rootless"); + Assumptions.assumeThat(info.getSecurityOptions()).doesNotContain("name=rootless"); try ( GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) .withStartupCheckStrategy(new NoopStartupCheckStrategy()) diff --git a/core/src/test/java/org/testcontainers/dockerclient/EnvironmentAndSystemPropertyClientProviderStrategyTest.java b/core/src/test/java/org/testcontainers/dockerclient/EnvironmentAndSystemPropertyClientProviderStrategyTest.java index 062c3d793a5..58bd6984401 100644 --- a/core/src/test/java/org/testcontainers/dockerclient/EnvironmentAndSystemPropertyClientProviderStrategyTest.java +++ b/core/src/test/java/org/testcontainers/dockerclient/EnvironmentAndSystemPropertyClientProviderStrategyTest.java @@ -18,6 +18,7 @@ import java.util.Properties; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; @@ -147,6 +148,8 @@ public void applicableWhenIgnoringUserPropertiesAndConfigured() { @Test public void notApplicableWhenIgnoringUserPropertiesAndNotConfigured() { + assumeThat(System.getenv("DOCKER_HOST")).isNull(); + Mockito .doReturn("autoIgnoringUserProperties") .when(TestcontainersConfiguration.getInstance()) diff --git a/core/src/test/java/org/testcontainers/junit/ComposeContainerTest.java b/core/src/test/java/org/testcontainers/junit/ComposeContainerTest.java index c101da6b6b3..39c7ff2baf5 100644 --- a/core/src/test/java/org/testcontainers/junit/ComposeContainerTest.java +++ b/core/src/test/java/org/testcontainers/junit/ComposeContainerTest.java @@ -18,21 +18,30 @@ public class ComposeContainerTest extends BaseComposeTest { @Rule + // composeContainerConstructor { public ComposeContainer environment = new ComposeContainer( new File("src/test/resources/composev2/compose-test.yml") ) .withExposedService("redis-1", REDIS_PORT) .withExposedService("db-1", 3306); + // } + @Override protected ComposeContainer getEnvironment() { return environment; } @Test - public void testGetServicePort() { + public void testGetServiceHostAndPort() { + // getServiceHostAndPort { + String serviceHost = environment.getServiceHost("redis-1", REDIS_PORT); int serviceWithInstancePort = environment.getServicePort("redis-1", REDIS_PORT); + // } + + assertThat(serviceHost).as("Service host is not blank").isNotBlank(); assertThat(serviceWithInstancePort).as("Port is set for service with instance number").isNotNull(); + int serviceWithoutInstancePort = environment.getServicePort("redis", REDIS_PORT); assertThat(serviceWithoutInstancePort).as("Port is set for service with instance number").isNotNull(); assertThat(serviceWithoutInstancePort).as("Service ports are the same").isEqualTo(serviceWithInstancePort); diff --git a/core/src/test/java/org/testcontainers/junit/ComposeContainerWithCopyFilesTest.java b/core/src/test/java/org/testcontainers/junit/ComposeContainerWithCopyFilesTest.java index 390ddd505fb..2b1e2cd4081 100644 --- a/core/src/test/java/org/testcontainers/junit/ComposeContainerWithCopyFilesTest.java +++ b/core/src/test/java/org/testcontainers/junit/ComposeContainerWithCopyFilesTest.java @@ -49,11 +49,13 @@ public void testWithFileCopyInclusionUsingFilePath() throws IOException { @Test public void testWithFileCopyInclusionUsingDirectoryPath() throws IOException { try ( + // composeContainerWithCopyFiles { ComposeContainer environment = new ComposeContainer( new File("src/test/resources/compose-file-copy-inclusions/compose-test-only.yml") ) .withExposedService("app", 8080) .withCopyFilesInContainer("Dockerfile", "EnvVariableRestEndpoint.java", "test") + // } ) { environment.start(); diff --git a/core/src/test/java/org/testcontainers/junit/ComposeContainerWithWaitStrategies.java b/core/src/test/java/org/testcontainers/junit/ComposeContainerWithWaitStrategies.java new file mode 100644 index 00000000000..1304413dce5 --- /dev/null +++ b/core/src/test/java/org/testcontainers/junit/ComposeContainerWithWaitStrategies.java @@ -0,0 +1,54 @@ +package org.testcontainers.junit; + +import org.junit.Test; +import org.testcontainers.containers.ComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.io.File; +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ComposeContainerWithWaitStrategies { + + private static final int REDIS_PORT = 6379; + + @Test + public void testComposeContainerConstructor() { + try ( + // composeContainerWithCombinedWaitStrategies { + ComposeContainer compose = new ComposeContainer(new File("src/test/resources/composev2/compose-test.yml")) + .withExposedService("redis-1", REDIS_PORT, Wait.forSuccessfulCommand("redis-cli ping")) + .withExposedService("db-1", 3306, Wait.forLogMessage(".*ready for connections.*\\n", 1)) + // } + ) { + compose.start(); + containsStartedServices(compose, "redis-1", "db-1"); + } + } + + @Test + public void testComposeContainerWaitForPortWithTimeout() { + try ( + // composeContainerWaitForPortWithTimeout { + ComposeContainer compose = new ComposeContainer(new File("src/test/resources/composev2/compose-test.yml")) + .withExposedService( + "redis-1", + REDIS_PORT, + Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)) + ) + // } + ) { + compose.start(); + containsStartedServices(compose, "redis-1"); + } + } + + private void containsStartedServices(ComposeContainer compose, String... expectedServices) { + for (String serviceName : expectedServices) { + assertThat(compose.getContainerByServiceName(serviceName)) + .as("Container should be found by service name %s", serviceName) + .isPresent(); + } + } +} diff --git a/core/src/test/java/org/testcontainers/utility/RegistryAuthLocatorTest.java b/core/src/test/java/org/testcontainers/utility/RegistryAuthLocatorTest.java index 2e6f87d08b7..ed986b05015 100644 --- a/core/src/test/java/org/testcontainers/utility/RegistryAuthLocatorTest.java +++ b/core/src/test/java/org/testcontainers/utility/RegistryAuthLocatorTest.java @@ -241,7 +241,7 @@ public void lookupAuthConfigWithCredentialsNotFound() throws URISyntaxException, assertThat(discoveredMessage) .as("Not correct message discovered") - .isEqualTo("Fake credentials not found on credentials store 'https://not.a.real.registry/url'"); + .isEqualTo("Fake credentials not found on credentials store 'registry2.example.com'"); } @Test diff --git a/core/src/test/resources/auth-config/docker-credential-fake b/core/src/test/resources/auth-config/docker-credential-fake index 4a851d93322..3eb54b35a7e 100755 --- a/core/src/test/resources/auth-config/docker-credential-fake +++ b/core/src/test/resources/auth-config/docker-credential-fake @@ -7,11 +7,7 @@ fi read inputLine if [ "$inputLine" = "registry2.example.com" ]; then - echo Fake credentials not found on credentials store \'$inputLine\' 1>&2 - exit 1 -fi -if [ "$inputLine" = "https://not.a.real.registry/url" ]; then - echo Fake credentials not found on credentials store \'$inputLine\' 1>&2 + echo Fake credentials not found on credentials store \'$inputLine\' 0>&2 exit 1 fi diff --git a/core/src/test/resources/auth-config/win/docker-credential-fake.bat b/core/src/test/resources/auth-config/win/docker-credential-fake.bat index c1095f0908c..afb4c66f1c6 100644 --- a/core/src/test/resources/auth-config/win/docker-credential-fake.bat +++ b/core/src/test/resources/auth-config/win/docker-credential-fake.bat @@ -6,11 +6,7 @@ if not "%1" == "get" ( set /p inputLine="" if "%inputLine%" == "registry2.example.com" ( - echo Fake credentials not found on credentials store '%inputLine%' 1>&2 - exit 1 -) -if "%inputLine%" == "https://not.a.real.registry/url" ( - echo Fake credentials not found on credentials store '%inputLine%' 1>&2 + echo Fake credentials not found on credentials store '%inputLine%' 0>&2 exit 1 ) diff --git a/core/src/test/resources/compose-file-copy-inclusions/compose-root-only.yml b/core/src/test/resources/compose-file-copy-inclusions/compose-root-only.yml index fa17d5f472d..31ad1216532 100644 --- a/core/src/test/resources/compose-file-copy-inclusions/compose-root-only.yml +++ b/core/src/test/resources/compose-file-copy-inclusions/compose-root-only.yml @@ -2,6 +2,6 @@ services: app: build: . ports: - - "8080:8080" + - "8080" env_file: - '.env' diff --git a/core/src/test/resources/compose-file-copy-inclusions/compose-test-only.yml b/core/src/test/resources/compose-file-copy-inclusions/compose-test-only.yml index 943f908ce20..ddc069664ab 100644 --- a/core/src/test/resources/compose-file-copy-inclusions/compose-test-only.yml +++ b/core/src/test/resources/compose-file-copy-inclusions/compose-test-only.yml @@ -2,6 +2,6 @@ services: app: build: . ports: - - "8080:8080" + - "8080" env_file: - './test/.env' diff --git a/core/src/test/resources/compose-file-copy-inclusions/compose.yml b/core/src/test/resources/compose-file-copy-inclusions/compose.yml index 2a9334d7980..4ea26f671f7 100644 --- a/core/src/test/resources/compose-file-copy-inclusions/compose.yml +++ b/core/src/test/resources/compose-file-copy-inclusions/compose.yml @@ -2,7 +2,7 @@ services: app: build: . ports: - - "8080:8080" + - "8080" env_file: - '.env' - './test/.env' diff --git a/docs/examples/junit4/generic/build.gradle b/docs/examples/junit4/generic/build.gradle index 296e936953f..5a276cf6a7f 100644 --- a/docs/examples/junit4/generic/build.gradle +++ b/docs/examples/junit4/generic/build.gradle @@ -10,16 +10,3 @@ dependencies { testImplementation "org.seleniumhq.selenium:selenium-api:4.25.0" testImplementation 'org.assertj:assertj-core:3.26.3' } - -test { - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(11) - } -} - -compileTestJava { - javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(11) - } - options.release.set(11) -} diff --git a/docs/index.md b/docs/index.md index 90cb81bf3af..a3b499dfa0c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -221,6 +221,8 @@ A huge thank you to our sponsors: * [Spark ClickHouse Connector](https://github.com/housepower/spark-clickhouse-connector) - Integration tests for Apache Spark with both single node ClickHouse instance and multi-node ClickHouse cluster. * [Quarkus](https://github.com/quarkusio/quarkus) - Testcontainers is used extensively for Quarkus' [DevServices](https://quarkus.io/guides/dev-services) feature. * [Apache Kyuubi](https://kyuubi.apache.org) - Integration testing with Trino as data source engine, Kafka, etc. +* [Dash0](https://www.dash0.com) - Integration testing for OpenTelemetry Observability product. + ## License diff --git a/docs/modules/azure.md b/docs/modules/azure.md index 5e80270e90c..e09634b09c4 100644 --- a/docs/modules/azure.md +++ b/docs/modules/azure.md @@ -5,14 +5,102 @@ This module is INCUBATING. While it is ready for use and operational in the curr Testcontainers module for the Microsoft Azure's [SDK](https://github.com/Azure/azure-sdk-for-java). -Currently, the module supports `CosmosDB` emulator. In order to use it, you should use the following class: +Currently, the module supports `Azurite`, `Azure Event Hubs` and `CosmosDB` emulators. In order to use them, you should use the following classes: Class | Container Image -|- +AzuriteContainer | [mcr.microsoft.com/azure-storage/azurite](https://github.com/microsoft/containerregistry) +AzureEventHubsContainer | [mcr.microsoft.com/azure-messaging/eventhubs-emulator](https://github.com/microsoft/containerregistry) CosmosDBEmulatorContainer | [mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator](https://github.com/microsoft/containerregistry) ## Usage example +### Azurite Storage Emulator + +Start Azurite Emulator during a test: + + +[Starting a Azurite container](../../modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java) inside_block:emulatorContainer + + +!!! note + SSL configuration is possible using the `withSsl(MountableFile, String)` and `withSsl(MountableFile, MountableFile)` methods. + +If the tested application needs to use more than one set of credentials, the container can be configured to use custom credentials. +Please see some examples below. + + +[Starting a Azurite Blob container with one account and two keys](../../modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java) inside_block:withTwoAccountKeys + + + +[Starting a Azurite Blob container with more accounts and keys](../../modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java) inside_block:withMoreAccounts + + +#### Using with Blob + +Build Azure Blob client: + + +[Build Azure Blob Service client](../../modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java) inside_block:createBlobClient + + +In case the application needs to use custom credentials, we can obtain them with a different method: + + +[Obtain connection string with non-default credentials](../../modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java) inside_block:useNonDefaultCredentials + + +#### Using with Queue + +Build Azure Queue client: + + +[Build Azure Queue Service client](../../modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java) inside_block:createQueueClient + + +!!! note + We can use custom credentials the same way as defined in the Blob section. + +#### Using with Table + +Build Azure Table client: + + +[Build Azure Table Service client](../../modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java) inside_block:createTableClient + + +!!! note + We can use custom credentials the same way as defined in the Blob section. + +### Azure Event Hubs Emulator + + +[Configuring the Azure Event Hubs Emulator container](../../modules/azure/src/test/resources/eventhubs_config.json) + + +Start Azure Event Hubs Emulator during a test: + + +[Setting up a network](../../modules/azure/src/test/java/org/testcontainers/azure/AzureEventHubsContainerTest.java) inside_block:network + + + +[Starting an Azurite container as dependency](../../modules/azure/src/test/java/org/testcontainers/azure/AzureEventHubsContainerTest.java) inside_block:azuriteContainer + + + +[Starting an Azure Event Hubs Emulator container](../../modules/azure/src/test/java/org/testcontainers/azure/AzureEventHubsContainerTest.java) inside_block:emulatorContainer + + +#### Using Azure Event Hubs clients + +Configure the consumer and the producer clients: + + +[Configuring the clients](../../modules/azure/src/test/java/org/testcontainers/azure/AzureEventHubsContainerTest.java) inside_block:createProducerAndConsumer + + ### CosmosDB Start Azure CosmosDB Emulator during a test: diff --git a/docs/modules/docker_compose.md b/docs/modules/docker_compose.md index 9032e7c6ee2..2ac45912479 100644 --- a/docs/modules/docker_compose.md +++ b/docs/modules/docker_compose.md @@ -2,161 +2,121 @@ ## Benefits -Similar to generic containers support, it's also possible to run a bespoke set of services -specified in a `docker-compose.yml` file. +Similar to generic container support, it's also possible to run a bespoke set of services specified in a +`docker-compose.yml` file. -This is intended to be useful on projects where Docker Compose is already used in dev or other environments to define -services that an application may be dependent upon. +This is especially useful for projects where Docker Compose is already used in development +or other environments to define services that an application may be dependent upon. -Behind the scenes, Testcontainers actually launches a temporary Docker Compose client - in a container, of course, so -it's not necessary to have it installed on all developer/test machines. +The `ComposeContainer` leverages [Compose V2](https://www.docker.com/blog/announcing-compose-v2-general-availability/), +making it easy to use the same dependencies from the development environment within tests. ## Example -A single class rule, pointing to a `docker-compose.yml` file, should be sufficient to launch any number of services -required by your tests: -```java -@ClassRule -public static DockerComposeContainer environment = - new DockerComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("redis_1", REDIS_PORT) - .withExposedService("elasticsearch_1", ELASTICSEARCH_PORT); -``` +A single class `ComposeContainer`, defined based on a `docker-compose.yml` file, +should be sufficient to launch any number of services required by our tests: + + +[Create a ComposeContainer](../../core/src/test/java/org/testcontainers/junit/ComposeContainerTest.java) inside_block:composeContainerConstructor + -In this example, `compose-test.yml` should have content such as: +!!! note + Make sure the service names use a `-` rather than `_` as separator. + +In this example, Docker Compose file should have content such as: ```yaml -redis: - image: redis -elasticsearch: - image: elasticsearch +services: + redis: + image: redis + db: + image: mysql:8.0.36 ``` -Note that it is not necessary to define ports to be exposed in the YAML file; this would inhibit reuse/inclusion of the -file in other contexts. +Note that it is not necessary to define ports to be exposed in the YAML file, +as this would inhibit the reuse/inclusion of the file in other contexts. + +Instead, Testcontainers will spin up a small `ambassador` container, +which will proxy between the Compose-managed containers and ports that are accessible to our tests. -Instead, Testcontainers will spin up a small 'ambassador' container, which will proxy -between the Compose-managed containers and ports that are accessible to your tests. This is done using a separate, minimal -container that runs socat as a TCP proxy. +## ComposeContainer vs DockerComposeContainer -## Accessing a container from tests +So far, we discussed `ComposeContainer`, which supports docker compose [version 2](https://www.docker.com/blog/announcing-compose-v2-general-availability/). -The rule provides methods for discovering how your tests can interact with the containers: +On the other hand, `DockerComposeContainer` utilizes Compose V1, which has been marked deprecated by Docker. + +The two APIs are quite similar, and most examples provided on this page can be applied to both of them. + +## Accessing a Container + +`ComposeContainer` provides methods for discovering how your tests can interact with the containers: * `getServiceHost(serviceName, servicePort)` returns the IP address where the container is listening (via an ambassador container) * `getServicePort(serviceName, servicePort)` returns the Docker mapped port for a port that has been exposed (via an ambassador container) -For example, with the Redis example above, the following will allow your tests to access the Redis service: -```java -String redisUrl = environment.getServiceHost("redis_1", REDIS_PORT) - + ":" + - environment.getServicePort("redis_1", REDIS_PORT); -``` +Let's use this API to create the URL that will enable our tests to access the Redis service: + +[Access a Service's host and port](../../core/src/test/java/org/testcontainers/junit/ComposeContainerTest.java) inside_block:getServiceHostAndPort + -## Startup timeout +## Wait Strategies and Startup Timeouts Ordinarily Testcontainers will wait for up to 60 seconds for each exposed container's first mapped network port to start listening. - This simple measure provides a basic check whether a container is ready for use. -There are overloaded `withExposedService` methods that take a `WaitStrategy` so you can specify a timeout strategy per container. +There are overloaded `withExposedService` methods that take a `WaitStrategy` +where we can specify a timeout strategy per container. -### Waiting for startup examples +We can either use the fluent API to crate a [custom strategy](../features/startup_and_waits.md) or use one of the already existing ones, +accessible via the static factory methods from of the `Wait` class. -Waiting for exposed port to start listening: -```java -@ClassRule -public static DockerComposeContainer environment = - new DockerComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("redis_1", REDIS_PORT, - Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30))); -``` +For instance, we can wait for exposed port and set a custom timeout: + +[Wait for the exposed port and use a custom timeout](../../core/src/test/java/org/testcontainers/junit/ComposeContainerWithWaitStrategies.java) inside_block:composeContainerWaitForPortWithTimeout + -Wait for arbitrary status codes on an HTTPS endpoint: -```java -@ClassRule -public static DockerComposeContainer environment = - new DockerComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("elasticsearch_1", ELASTICSEARCH_PORT, - Wait.forHttp("/all") - .forStatusCode(200) - .forStatusCode(401) - .usingTls()); -``` +Needless to say, we can define different strategies for each service in our Docker Compose setup. -Separate wait strategies for each container: -```java -@ClassRule -public static DockerComposeContainer environment = - new DockerComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort()) - .withExposedService("elasticsearch_1", ELASTICSEARCH_PORT, - Wait.forHttp("/all") - .forStatusCode(200) - .forStatusCode(401) - .usingTls()); -``` +For example, our Redis container can wait for a successful redis-cli command, +while our db service waits for a specific log message: -Alternatively, you can use `waitingFor(serviceName, waitStrategy)`, -for example if you need to wait on a log message from a service, but don't need to expose a port. + +[Wait for a custom command and a log message](../../core/src/test/java/org/testcontainers/junit/ComposeContainerWithWaitStrategies.java) inside_block:composeContainerWithCombinedWaitStrategies + -```java -@ClassRule -public static DockerComposeContainer environment = - new DockerComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort()) - .waitingFor("db_1", Wait.forLogMessage("started", 1)); -``` -## 'Local compose' mode -You can override Testcontainers' default behaviour and make it use a `docker-compose` binary installed on the local machine. -This will generally yield an experience that is closer to running docker-compose locally, with the caveat that Docker Compose needs to be present on dev and CI machines. -```java -public static DockerComposeContainer environment = - new DockerComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort()) - .waitingFor("db_1", Wait.forLogMessage("started", 1)) - .withLocalCompose(true); -``` +## The 'Local Compose' Mode -## Compose V2 +We can override Testcontainers' default behaviour and make it use a `docker-compose` binary installed on the local machine. -[Compose V2 is GA](https://www.docker.com/blog/announcing-compose-v2-general-availability/) and it relies on the `docker` command itself instead of `docker-compose`. -Testcontainers provides `ComposeContainer` if you want to use Compose V2. +This will generally yield an experience that is closer to running _docker compose_ locally, +with the caveat that Docker Compose needs to be present on dev and CI machines. -```java -public static ComposeContainer environment = - new ComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("redis-1", REDIS_PORT, Wait.forListeningPort()) - .waitingFor("db-1", Wait.forLogMessage("started", 1)); -``` - -!!! note - Make sure the service name use a `-` instead of `_` as separator using `ComposeContainer`. + +[Use ComposeContainer in 'Local Compose' mode](../../core/src/test/java/org/testcontainers/containers/ComposeProfilesOptionTest.java) inside_block:composeContainerWithLocalCompose + -## Build working directory +## Build Working Directory -You can select what files should be copied only via `withCopyFilesInContainer`: +We can select what files should be copied only via `withCopyFilesInContainer`: -```java -public static ComposeContainer environment = - new ComposeContainer(new File("compose.yml")) - .withCopyFilesInContainer(".env"); -``` + +[Use ComposeContainer in 'Local Compose' mode](../../core/src/test/java/org/testcontainers/junit/ComposeContainerWithCopyFilesTest.java) inside_block:composeContainerWithCopyFiles + -In this example, only `compose.yml` and `.env` are copied over into the container that will run the Docker Compose file. +In this example, only docker compose and env files are copied over into the container that will run the Docker Compose file. By default, all files in the same directory as the compose file are copied over. -This can be used with `DockerComposeContainer` and `ComposeContainer`. -You can use file and directory references. +We can use file and directory references. They are always resolved relative to the directory where the compose file resides. !!! note - This only work with containarized Compose, not with `Local Compose` mode. + This can be used with `DockerComposeContainer` and `ComposeContainer`, but **only in the containerized Compose (not with `Local Compose` mode)**. ## Using private repositories in Docker compose -When Docker Compose is used in container mode (not local), it's needs to be made aware of Docker settings for private repositories. +When Docker Compose is used in container mode (not local), it needs to be made aware of Docker +settings for private repositories. By default, those setting are located in `$HOME/.docker/config.json`. There are 3 ways to specify location of the `config.json` for Docker Compose: diff --git a/examples/build.gradle b/examples/build.gradle index 6eaf2f73e49..821f2b4a1fc 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -1,6 +1,6 @@ // empty build.gradle for dependabot plugins { - id 'com.diffplug.spotless' version '6.13.0' apply false + id 'com.diffplug.spotless' version '6.22.0' apply false } apply from: "$rootDir/../gradle/ci-support.gradle" @@ -25,7 +25,7 @@ subprojects { } checkstyle { - toolVersion = "9.3" + toolVersion = "10.12.4" configFile = rootProject.file('../config/checkstyle/checkstyle.xml') } } diff --git a/examples/cucumber/build.gradle b/examples/cucumber/build.gradle index 3ee4054ce71..2710e259e9b 100644 --- a/examples/cucumber/build.gradle +++ b/examples/cucumber/build.gradle @@ -18,16 +18,3 @@ dependencies { testImplementation 'org.testcontainers:selenium' testImplementation 'org.assertj:assertj-core:3.26.3' } - -test { - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(11) - } -} - -compileTestJava { - javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(11) - } - options.release.set(11) -} diff --git a/examples/gradle/wrapper/gradle-wrapper.jar b/examples/gradle/wrapper/gradle-wrapper.jar index d64cd491770..2c3521197d7 100644 Binary files a/examples/gradle/wrapper/gradle-wrapper.jar and b/examples/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/gradle/wrapper/gradle-wrapper.properties b/examples/gradle/wrapper/gradle-wrapper.properties index db8c3baafe3..68e8816d71c 100644 --- a/examples/gradle/wrapper/gradle-wrapper.properties +++ b/examples/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=9d926787066a081739e8200858338b4a69e837c3a821a33aca9db09dd4a41026 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/examples/gradlew b/examples/gradlew index 1aa94a42690..f5feea6d6b1 100755 --- a/examples/gradlew +++ b/examples/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/examples/gradlew.bat b/examples/gradlew.bat index 6689b85beec..9b42019c791 100644 --- a/examples/gradlew.bat +++ b/examples/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/examples/linked-container/src/test/java/com/example/linkedcontainer/LinkedContainerTestImages.java b/examples/linked-container/src/test/java/com/example/linkedcontainer/LinkedContainerTestImages.java index 8bbbafd189f..af8a823c1de 100644 --- a/examples/linked-container/src/test/java/com/example/linkedcontainer/LinkedContainerTestImages.java +++ b/examples/linked-container/src/test/java/com/example/linkedcontainer/LinkedContainerTestImages.java @@ -4,5 +4,6 @@ public interface LinkedContainerTestImages { DockerImageName POSTGRES_TEST_IMAGE = DockerImageName.parse("postgres:9.6.12"); + DockerImageName REDMINE_TEST_IMAGE = DockerImageName.parse("redmine:3.3.2"); } diff --git a/gradle.properties b/gradle.properties index ea875d07cf2..be57d9abd98 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,4 @@ org.gradle.caching=true org.gradle.configureondemand=true org.gradle.jvmargs=-Xmx2g -testcontainers.version=1.20.3 +testcontainers.version=1.20.4 diff --git a/gradle/shading.gradle b/gradle/shading.gradle index fc7f6587b3e..10ceb5086a4 100644 --- a/gradle/shading.gradle +++ b/gradle/shading.gradle @@ -1,6 +1,6 @@ import java.util.jar.JarFile -apply plugin: 'com.github.johnrengelman.shadow' +apply plugin: 'com.gradleup.shadow' configurations { shaded @@ -30,7 +30,7 @@ project.afterEvaluate { return it.dependencyProject.tasks.findByName("shadowJar")?.relocators ?: [] } - // See https://github.com/johnrengelman/shadow/blob/5.0.0/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ConfigureShadowRelocation.groovy + // See https://github.com/GradleUp/shadow/blob/5.0.0/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ConfigureShadowRelocation.groovy Set packages = [] for (artifact in project.configurations.shaded.resolvedConfiguration.resolvedArtifacts) { diff --git a/gradle/spotless.gradle b/gradle/spotless.gradle index ce71829331e..59b834dbad7 100644 --- a/gradle/spotless.gradle +++ b/gradle/spotless.gradle @@ -18,6 +18,6 @@ spotless { } groovyGradle { target '**/*.groovy' - greclipse('4.19.0') + greclipse('4.19') } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd491770..2c3521197d7 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a7a990ab2a8..efe2ff34492 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=c16d517b50dd28b3f5838f0e844b7520b8f1eb610f2f29de7e4e04a1b7c9c79b -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip +distributionSha256Sum=258e722ec21e955201e31447b0aed14201765a3bfbae296a46cf60b70e66db70 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a42690..f5feea6d6b1 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 6689b85beec..9b42019c791 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/mkdocs.yml b/mkdocs.yml index 5c051c5565c..c308128969b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -136,4 +136,4 @@ nav: - bounty.md edit_uri: edit/main/docs/ extra: - latest_version: 1.20.3 + latest_version: 1.20.4 diff --git a/modules/activemq/build.gradle b/modules/activemq/build.gradle index 7ee0971b251..dc20113a84b 100644 --- a/modules/activemq/build.gradle +++ b/modules/activemq/build.gradle @@ -7,16 +7,3 @@ dependencies { testImplementation "org.apache.activemq:activemq-client:6.1.2" testImplementation "org.apache.activemq:artemis-jakarta-client:2.37.0" } - -test { - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(17) - } -} - -compileTestJava { - javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(17) - } - options.release.set(11) -} diff --git a/modules/activemq/src/test/java/org/testcontainers/activemq/ActiveMQContainerTest.java b/modules/activemq/src/test/java/org/testcontainers/activemq/ActiveMQContainerTest.java index 3ccd7acf2b1..842701fa376 100644 --- a/modules/activemq/src/test/java/org/testcontainers/activemq/ActiveMQContainerTest.java +++ b/modules/activemq/src/test/java/org/testcontainers/activemq/ActiveMQContainerTest.java @@ -3,7 +3,6 @@ import jakarta.jms.Connection; import jakarta.jms.ConnectionFactory; import jakarta.jms.Destination; -import jakarta.jms.JMSException; import jakarta.jms.MessageConsumer; import jakarta.jms.MessageProducer; import jakarta.jms.Session; @@ -17,7 +16,7 @@ public class ActiveMQContainerTest { @Test - public void test() throws JMSException { + public void test() { try ( // container { ActiveMQContainer activemq = new ActiveMQContainer("apache/activemq-classic:5.18.3") // } diff --git a/modules/azure/build.gradle b/modules/azure/build.gradle index d31a549abe5..3dc97d03fce 100644 --- a/modules/azure/build.gradle +++ b/modules/azure/build.gradle @@ -7,4 +7,8 @@ dependencies { testImplementation 'org.assertj:assertj-core:3.26.3' testImplementation 'com.azure:azure-cosmos:4.63.3' + testImplementation 'com.azure:azure-storage-blob:12.29.0' + testImplementation 'com.azure:azure-storage-queue:12.24.0' + testImplementation 'com.azure:azure-data-tables:12.5.0' + testImplementation 'com.azure:azure-messaging-eventhubs:5.19.2' } diff --git a/modules/azure/src/main/java/org/testcontainers/azure/AzureEventHubsContainer.java b/modules/azure/src/main/java/org/testcontainers/azure/AzureEventHubsContainer.java new file mode 100644 index 00000000000..d611a2442cc --- /dev/null +++ b/modules/azure/src/main/java/org/testcontainers/azure/AzureEventHubsContainer.java @@ -0,0 +1,109 @@ +package org.testcontainers.azure; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.Transferable; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.LicenseAcceptance; + +/** + * Testcontainers implementation for Azure Eventhubs Emulator. + *

+ * Supported image: {@code "mcr.microsoft.com/azure-messaging/eventhubs-emulator"} + *

+ * Exposed ports: + *

+ */ +public class AzureEventHubsContainer extends GenericContainer { + + private static final int DEFAULT_AMQP_PORT = 5672; + + private static final String CONNECTION_STRING_FORMAT = + "Endpoint=sb://%s:%d;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"; + + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse( + "mcr.microsoft.com/azure-messaging/eventhubs-emulator" + ); + + private AzuriteContainer azuriteContainer; + + /** + * @param dockerImageName specified docker image name to run + */ + public AzureEventHubsContainer(final String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + /** + * @param dockerImageName specified docker image name to run + */ + public AzureEventHubsContainer(final DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + waitingFor(Wait.forLogMessage(".*Emulator Service is Successfully Up!.*", 1)); + withExposedPorts(DEFAULT_AMQP_PORT); + } + + /** + * * Sets the Azurite dependency needed by the Event Hubs Container, + * + * @param azuriteContainer The Azurite container used by Event HUbs as a dependency + * @return this + */ + public AzureEventHubsContainer withAzuriteContainer(final AzuriteContainer azuriteContainer) { + this.azuriteContainer = azuriteContainer; + dependsOn(this.azuriteContainer); + return this; + } + + /** + * Provide the broker configuration to the container. + * + * @param config The file containing the broker configuration + * @return this + */ + public AzureEventHubsContainer withConfig(final Transferable config) { + withCopyToContainer(config, "/Eventhubs_Emulator/ConfigFiles/Config.json"); + return this; + } + + /** + * Accepts the EULA of the container. + * + * @return this + */ + public AzureEventHubsContainer acceptLicense() { + withEnv("ACCEPT_EULA", "Y"); + return this; + } + + @Override + protected void configure() { + if (azuriteContainer == null) { + throw new IllegalStateException( + "The image " + + getDockerImageName() + + " requires an Azurite container. Please provide one with the withAzuriteContainer method!" + ); + } + final String azuriteHost = azuriteContainer.getNetworkAliases().get(0); + withEnv("BLOB_SERVER", azuriteHost); + withEnv("METADATA_SERVER", azuriteHost); + // If license was not accepted programmatically, check if it was accepted via resource file + if (!getEnvMap().containsKey("ACCEPT_EULA")) { + LicenseAcceptance.assertLicenseAccepted(this.getDockerImageName()); + acceptLicense(); + } + } + + /** + * Returns the connection string. + * + * @return connection string + */ + public String getConnectionString() { + return String.format(CONNECTION_STRING_FORMAT, getHost(), getMappedPort(DEFAULT_AMQP_PORT)); + } +} diff --git a/modules/azure/src/main/java/org/testcontainers/azure/AzuriteContainer.java b/modules/azure/src/main/java/org/testcontainers/azure/AzuriteContainer.java new file mode 100644 index 00000000000..56c58df1f1d --- /dev/null +++ b/modules/azure/src/main/java/org/testcontainers/azure/AzuriteContainer.java @@ -0,0 +1,167 @@ +package org.testcontainers.azure; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +/** + * Testcontainers implementation for Azurite Emulator. + *

+ * Supported image: {@code mcr.microsoft.com/azure-storage/azurite} + *

+ * Exposed ports: + *

+ */ +public class AzuriteContainer extends GenericContainer { + + private static final String ALLOW_ALL_CONNECTIONS = "0.0.0.0"; + + private static final int DEFAULT_BLOB_PORT = 10000; + + private static final int DEFAULT_QUEUE_PORT = 10001; + + private static final int DEFAULT_TABLE_PORT = 10002; + + private static final String CONNECTION_STRING_FORMAT = + "DefaultEndpointsProtocol=%s;AccountName=%s;AccountKey=%s;BlobEndpoint=%s://%s:%d/%s;QueueEndpoint=%s://%s:%d/%s;TableEndpoint=%s://%s:%d/%s;"; + + /** + * The account name of the default credentials. + */ + private static final String WELL_KNOWN_ACCOUNT_NAME = "devstoreaccount1"; + + /** + * The account key of the default credentials. + */ + private static final String WELL_KNOWN_ACCOUNT_KEY = + "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="; + + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse( + "mcr.microsoft.com/azure-storage/azurite" + ); + + private MountableFile cert = null; + + private String certExtension = null; + + private MountableFile key = null; + + private String pwd = null; + + /** + * @param dockerImageName specified docker image name to run + */ + public AzuriteContainer(String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + /** + * @param dockerImageName specified docker image name to run + */ + public AzuriteContainer(DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + withExposedPorts(DEFAULT_BLOB_PORT, DEFAULT_QUEUE_PORT, DEFAULT_TABLE_PORT); + } + + /** + * Configure SSL with a custom certificate and password. + * + * @param pfxCert The PFX certificate file + * @param password The password securing the certificate + * @return this + */ + public AzuriteContainer withSsl(final MountableFile pfxCert, final String password) { + this.cert = pfxCert; + this.pwd = password; + this.certExtension = ".pfx"; + return this; + } + + /** + * Configure SSL with a custom certificate and private key. + * + * @param pemCert The PEM certificate file + * @param pemKey The PEM key file + * @return this + */ + public AzuriteContainer withSsl(final MountableFile pemCert, final MountableFile pemKey) { + this.cert = pemCert; + this.key = pemKey; + this.certExtension = ".pem"; + return this; + } + + @Override + protected void configure() { + withCommand(getCommandLine()); + if (this.cert != null) { + logger().info("Using path for cert file: '{}'", this.cert); + withCopyFileToContainer(this.cert, "/cert" + this.certExtension); + if (this.key != null) { + logger().info("Using path for key file: '{}'", this.key); + withCopyFileToContainer(this.key, "/key.pem"); + } + } + } + + /** + * Returns the connection string for the default credentials. + * + * @return connection string + */ + public String getConnectionString() { + return getConnectionString(WELL_KNOWN_ACCOUNT_NAME, WELL_KNOWN_ACCOUNT_KEY); + } + + /** + * Returns the connection string for the account name and key specified. + * + * @param accountName The name of the account + * @param accountKey The account key + * @return connection string + */ + public String getConnectionString(final String accountName, final String accountKey) { + final String protocol = cert != null ? "https" : "http"; + return String.format( + CONNECTION_STRING_FORMAT, + protocol, + accountName, + accountKey, + protocol, + getHost(), + getMappedPort(DEFAULT_BLOB_PORT), + accountName, + protocol, + getHost(), + getMappedPort(DEFAULT_QUEUE_PORT), + accountName, + protocol, + getHost(), + getMappedPort(DEFAULT_TABLE_PORT), + accountName + ); + } + + String getCommandLine() { + final StringBuilder args = new StringBuilder("azurite"); + args.append(" --blobHost ").append(ALLOW_ALL_CONNECTIONS); + args.append(" --queueHost ").append(ALLOW_ALL_CONNECTIONS); + args.append(" --tableHost ").append(ALLOW_ALL_CONNECTIONS); + if (this.cert != null) { + args.append(" --cert ").append("/cert").append(this.certExtension); + if (this.pwd != null) { + args.append(" --pwd ").append(this.pwd); + } else { + args.append(" --key ").append("/key.pem"); + } + } + final String cmd = args.toString(); + logger().debug("Using command line: '{}'", cmd); + return cmd; + } +} diff --git a/modules/azure/src/test/java/org/testcontainers/azure/AzureEventHubsContainerTest.java b/modules/azure/src/test/java/org/testcontainers/azure/AzureEventHubsContainerTest.java new file mode 100644 index 00000000000..c0febc2ed86 --- /dev/null +++ b/modules/azure/src/test/java/org/testcontainers/azure/AzureEventHubsContainerTest.java @@ -0,0 +1,83 @@ +package org.testcontainers.azure; + +import com.azure.core.util.IterableStream; +import com.azure.messaging.eventhubs.EventData; +import com.azure.messaging.eventhubs.EventHubClientBuilder; +import com.azure.messaging.eventhubs.EventHubConsumerClient; +import com.azure.messaging.eventhubs.EventHubProducerClient; +import com.azure.messaging.eventhubs.models.EventPosition; +import com.azure.messaging.eventhubs.models.PartitionEvent; +import org.junit.Rule; +import org.junit.Test; +import org.testcontainers.containers.Network; +import org.testcontainers.utility.MountableFile; + +import java.time.Duration; +import java.util.Collections; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.waitAtMost; + +public class AzureEventHubsContainerTest { + + @Rule + // network { + public Network network = Network.newNetwork(); + + // } + + @Rule + // azuriteContainer { + public AzuriteContainer azuriteContainer = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") + .withNetwork(network); + + // } + + @Rule + // emulatorContainer { + public AzureEventHubsContainer emulator = new AzureEventHubsContainer( + "mcr.microsoft.com/azure-messaging/eventhubs-emulator:2.0.1" + ) + .acceptLicense() + .withNetwork(network) + .withConfig(MountableFile.forClasspathResource("/eventhubs_config.json")) + .withAzuriteContainer(azuriteContainer); + + // } + + @Test + public void testWithEventHubsClient() { + try ( + // createProducerAndConsumer { + EventHubProducerClient producer = new EventHubClientBuilder() + .connectionString(emulator.getConnectionString()) + .fullyQualifiedNamespace("emulatorNs1") + .eventHubName("eh1") + .buildProducerClient(); + EventHubConsumerClient consumer = new EventHubClientBuilder() + .connectionString(emulator.getConnectionString()) + .fullyQualifiedNamespace("emulatorNs1") + .eventHubName("eh1") + .consumerGroup("cg1") + .buildConsumerClient() + // } + ) { + producer.send(Collections.singletonList(new EventData("test"))); + + waitAtMost(Duration.ofSeconds(30)) + .pollDelay(Duration.ofSeconds(5)) + .untilAsserted(() -> { + IterableStream events = consumer.receiveFromPartition( + "0", + 1, + EventPosition.earliest(), + Duration.ofSeconds(2) + ); + Optional event = events.stream().findFirst(); + assertThat(event).isPresent(); + assertThat(event.get().getData().getBodyAsString()).isEqualTo("test"); + }); + } + } +} diff --git a/modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java b/modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java new file mode 100644 index 00000000000..2bf0ad856d3 --- /dev/null +++ b/modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java @@ -0,0 +1,266 @@ +package org.testcontainers.azure; + +import com.azure.core.util.BinaryData; +import com.azure.data.tables.TableClient; +import com.azure.data.tables.TableServiceClient; +import com.azure.data.tables.TableServiceClientBuilder; +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.BlobServiceClientBuilder; +import com.azure.storage.queue.QueueClient; +import com.azure.storage.queue.QueueServiceClient; +import com.azure.storage.queue.QueueServiceClientBuilder; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.testcontainers.utility.MountableFile; + +import java.util.Properties; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AzuriteContainerTest { + + private static final String PASSWORD = "changeit"; + + private static Properties originalSystemProperties; + + @BeforeClass + public static void captureOriginalSystemProperties() { + originalSystemProperties = (Properties) System.getProperties().clone(); + System.setProperty( + "javax.net.ssl.trustStore", + MountableFile.forClasspathResource("/keystore.pfx").getFilesystemPath() + ); + System.setProperty("javax.net.ssl.trustStorePassword", PASSWORD); + System.setProperty("javax.net.ssl.trustStoreType", "PKCS12"); + } + + @AfterClass + public static void restoreOriginalSystemProperties() { + System.setProperties(originalSystemProperties); + } + + @Test + public void testWithBlobServiceClient() { + try ( + // emulatorContainer { + AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") + // } + ) { + emulator.start(); + assertThat(emulator.getConnectionString()).contains("BlobEndpoint=http://"); + testBlob(emulator); + } + } + + @Test + public void testWithQueueServiceClient() { + try (AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0")) { + emulator.start(); + assertThat(emulator.getConnectionString()).contains("QueueEndpoint=http://"); + testQueue(emulator); + } + } + + @Test + public void testWithTableServiceClient() { + try (AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0")) { + emulator.start(); + assertThat(emulator.getConnectionString()).contains("TableEndpoint=http://"); + testTable(emulator); + } + } + + @Test + public void testWithBlobServiceClientWithSslUsingPfx() { + try ( + AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") + .withSsl(MountableFile.forClasspathResource("/keystore.pfx"), PASSWORD) + ) { + emulator.start(); + assertThat(emulator.getConnectionString()).contains("BlobEndpoint=https://"); + testBlob(emulator); + } + } + + @Test + public void testWithQueueServiceClientWithSslUsingPfx() { + try ( + AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") + .withSsl(MountableFile.forClasspathResource("/keystore.pfx"), PASSWORD) + ) { + emulator.start(); + assertThat(emulator.getConnectionString()).contains("QueueEndpoint=https://"); + testQueue(emulator); + } + } + + @Test + public void testWithTableServiceClientWithSslUsingPfx() { + try ( + AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") + .withSsl(MountableFile.forClasspathResource("/keystore.pfx"), PASSWORD) + ) { + emulator.start(); + assertThat(emulator.getConnectionString()).contains("TableEndpoint=https://"); + testTable(emulator); + } + } + + @Test + public void testWithBlobServiceClientWithSslUsingPem() { + try ( + AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") + .withSsl( + MountableFile.forClasspathResource("/certificate.pem"), + MountableFile.forClasspathResource("/key.pem") + ) + ) { + emulator.start(); + assertThat(emulator.getConnectionString()).contains("BlobEndpoint=https://"); + testBlob(emulator); + } + } + + @Test + public void testWithQueueServiceClientWithSslUsingPem() { + try ( + AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") + .withSsl( + MountableFile.forClasspathResource("/certificate.pem"), + MountableFile.forClasspathResource("/key.pem") + ) + ) { + emulator.start(); + assertThat(emulator.getConnectionString()).contains("QueueEndpoint=https://"); + testQueue(emulator); + } + } + + @Test + public void testWithTableServiceClientWithSslUsingPem() { + try ( + AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") + .withSsl( + MountableFile.forClasspathResource("/certificate.pem"), + MountableFile.forClasspathResource("/key.pem") + ) + ) { + emulator.start(); + assertThat(emulator.getConnectionString()).contains("TableEndpoint=https://"); + testTable(emulator); + } + } + + @Test + public void testTwoAccountKeysWithBlobServiceClient() { + try ( + // withTwoAccountKeys { + AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") + .withEnv("AZURITE_ACCOUNTS", "account1:key1:key2") + // } + ) { + emulator.start(); + + String connectionString1 = emulator.getConnectionString("account1", "key1"); + // the second account will have access to the same container using a different key + String connectionString2 = emulator.getConnectionString("account1", "key2"); + + BlobServiceClient blobServiceClient1 = new BlobServiceClientBuilder() + .connectionString(connectionString1) + .buildClient(); + + BlobContainerClient containerClient1 = blobServiceClient1.createBlobContainer("test-container"); + BlobClient blobClient1 = containerClient1.getBlobClient("test-blob.txt"); + blobClient1.upload(BinaryData.fromString("content")); + boolean existsWithAccount1 = blobClient1.exists(); + String contentWithAccount1 = blobClient1.downloadContent().toString(); + + BlobServiceClient blobServiceClient2 = new BlobServiceClientBuilder() + .connectionString(connectionString2) + .buildClient(); + BlobContainerClient containerClient2 = blobServiceClient2.getBlobContainerClient("test-container"); + BlobClient blobClient2 = containerClient2.getBlobClient("test-blob.txt"); + boolean existsWithAccount2 = blobClient2.exists(); + String contentWithAccount2 = blobClient2.downloadContent().toString(); + + assertThat(existsWithAccount1).isTrue(); + assertThat(contentWithAccount1).isEqualTo("content"); + assertThat(existsWithAccount2).isTrue(); + assertThat(contentWithAccount2).isEqualTo("content"); + } + } + + @Test + public void testMultipleAccountsWithBlobServiceClient() { + try ( + // withMoreAccounts { + AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") + .withEnv("AZURITE_ACCOUNTS", "account1:key1;account2:key2") + // } + ) { + emulator.start(); + + // useNonDefaultCredentials { + String connectionString1 = emulator.getConnectionString("account1", "key1"); + // the second account will not have access to the same container + String connectionString2 = emulator.getConnectionString("account2", "key2"); + // } + BlobServiceClient blobServiceClient1 = new BlobServiceClientBuilder() + .connectionString(connectionString1) + .buildClient(); + + BlobContainerClient containerClient1 = blobServiceClient1.createBlobContainer("test-container"); + BlobClient blobClient1 = containerClient1.getBlobClient("test-blob.txt"); + blobClient1.upload(BinaryData.fromString("content")); + boolean existsWithAccount1 = blobClient1.exists(); + String contentWithAccount1 = blobClient1.downloadContent().toString(); + + BlobServiceClient blobServiceClient2 = new BlobServiceClientBuilder() + .connectionString(connectionString2) + .buildClient(); + BlobContainerClient containerClient2 = blobServiceClient2.createBlobContainer("test-container"); + BlobClient blobClient2 = containerClient2.getBlobClient("test-blob.txt"); + boolean existsWithAccount2 = blobClient2.exists(); + + assertThat(existsWithAccount1).isTrue(); + assertThat(contentWithAccount1).isEqualTo("content"); + assertThat(existsWithAccount2).isFalse(); + } + } + + private void testBlob(AzuriteContainer container) { + // createBlobClient { + BlobServiceClient blobServiceClient = new BlobServiceClientBuilder() + .connectionString(container.getConnectionString()) + .buildClient(); + // } + BlobContainerClient containerClient = blobServiceClient.createBlobContainer("test-container"); + + assertThat(containerClient.exists()).isTrue(); + } + + private void testQueue(AzuriteContainer container) { + // createQueueClient { + QueueServiceClient queueServiceClient = new QueueServiceClientBuilder() + .connectionString(container.getConnectionString()) + .buildClient(); + // } + QueueClient queueClient = queueServiceClient.createQueue("test-queue"); + + assertThat(queueClient.getQueueUrl()).isNotNull(); + } + + private void testTable(AzuriteContainer container) { + // createTableClient { + TableServiceClient tableServiceClient = new TableServiceClientBuilder() + .connectionString(container.getConnectionString()) + .buildClient(); + // } + TableClient tableClient = tableServiceClient.createTable("testtable"); + + assertThat(tableClient.getTableEndpoint()).isNotNull(); + } +} diff --git a/modules/azure/src/test/resources/certificate.pem b/modules/azure/src/test/resources/certificate.pem new file mode 100644 index 00000000000..30bedc29f45 --- /dev/null +++ b/modules/azure/src/test/resources/certificate.pem @@ -0,0 +1,23 @@ +Bag Attributes + friendlyName: localhost + localKeyID: 54 69 6D 65 20 31 37 33 34 37 32 32 33 32 31 33 31 39 +subject=CN = localhost +issuer=CN = localhost +-----BEGIN CERTIFICATE----- +MIIC5zCCAc+gAwIBAgIILe7i2bhRE5cwDQYJKoZIhvcNAQEMBQAwFDESMBAGA1UE +AxMJbG9jYWxob3N0MB4XDTI0MTIyMDE5MTg0MVoXDTQ0MTIxNTE5MTg0MVowFDES +MBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAoqYNmLl8IiIoYrXdcoWiMQaM0lOHcV9v3A/THMremHxsR+JPm3FIOAuilFcy +my16kuXIWHfisPxUWr9Vbf8wP/WwZutoOofJrqmruZoorQcNLCs8mQweguRmL1ju +/lDh/9bP626vP9OjwStC4UO4f8Jga8ENoH1U+j1RsIAswYnkk3YIN6YrYv66UvtH +IfR0ERgid2LMBIM+2KD2zw4QRyqXH7Qvo7sCsxdYYHGa6GXfza4vgvce9kJwGqn5 +wiF0Uw9XQbr/LarnR2GCy020OB81KweQJNIh27FZSRLtT+XpsjDRcC2aLBd8CRHd +hwO2zAPI04dLbLM5XAHlEdfT7wIDAQABoz0wOzAdBgNVHQ4EFgQUPqY5isb6Q11Q +t6dbXYHEupxADdMwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3 +DQEBDAUAA4IBAQA2katMXrTJBukiNh9yceLO/MewsxvU3KOO/O89ngfjhKXm9T8E +RtENCmp7hLbj1Aj4PRZx3AbmUt9+tRu8fmrRXJQWgUDSHJWjDwSTBOaHcC5LDWSU +Ex4co5Mnxvrimg7tqQg82Hw/yLH9j6gyTyh6v45QETP7IUkTZe4fg75/kPjng7Xg +wp/QXFUx/f0dbvGRl2Fdgg0SnYFqHS3MFIjjFjv8SQlV7rZe+CD1Lxqy/Z6Fd/Fa +33TzTuJeSAG43vdkGAvsNK/KdnxAW03T4l3pVHpNPcvsIvJUMeKOwYOjwHF/eowk +tGrKbpUYFxUr9iKHTfu14t1oExhAsnda2Fcs +-----END CERTIFICATE----- diff --git a/modules/azure/src/test/resources/eventhubs_config.json b/modules/azure/src/test/resources/eventhubs_config.json new file mode 100644 index 00000000000..554be9d7cbf --- /dev/null +++ b/modules/azure/src/test/resources/eventhubs_config.json @@ -0,0 +1,24 @@ +{ + "UserConfig": { + "NamespaceConfig": [ + { + "Type": "EventHub", + "Name": "emulatorNs1", + "Entities": [ + { + "Name": "eh1", + "PartitionCount": "1", + "ConsumerGroups": [ + { + "Name": "cg1" + } + ] + } + ] + } + ], + "LoggingConfig": { + "Type": "File" + } + } +} diff --git a/modules/azure/src/test/resources/key.pem b/modules/azure/src/test/resources/key.pem new file mode 100644 index 00000000000..7c635f5a278 --- /dev/null +++ b/modules/azure/src/test/resources/key.pem @@ -0,0 +1,32 @@ +Bag Attributes + friendlyName: localhost + localKeyID: 54 69 6D 65 20 31 37 33 34 37 32 32 33 32 31 33 31 39 +Key Attributes: +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCipg2YuXwiIihi +td1yhaIxBozSU4dxX2/cD9Mcyt6YfGxH4k+bcUg4C6KUVzKbLXqS5chYd+Kw/FRa +v1Vt/zA/9bBm62g6h8muqau5miitBw0sKzyZDB6C5GYvWO7+UOH/1s/rbq8/06PB +K0LhQ7h/wmBrwQ2gfVT6PVGwgCzBieSTdgg3piti/rpS+0ch9HQRGCJ3YswEgz7Y +oPbPDhBHKpcftC+juwKzF1hgcZroZd/Nri+C9x72QnAaqfnCIXRTD1dBuv8tqudH +YYLLTbQ4HzUrB5Ak0iHbsVlJEu1P5emyMNFwLZosF3wJEd2HA7bMA8jTh0tsszlc +AeUR19PvAgMBAAECggEAFT8dzZKFTawqnGJncBtWyZKyeJMiwUOXSCblDADQPRkb +x/QfNA4DQhb7AOe3G6BAP8o2dqAKg9YiasxNq5XHRsOgbIFZ1zN/vAo7/X3OzHN8 +XAW138Q+hBiz5IF4js4gB5yXAokt6WeLH6O4E9cV1dKdZ9YLIqjcnee+sRC9R/a3 +CexqLfC6b77JFbtePfq+5cn2RiK540tO/4k+F+kfJtTg78Wf2RB3A0pBAunhPSd5 +eyjiSvOZtTcvl4GdYw9nKf24I1/WUvt9FH/r1XG0CM5iwuGodbBz1iSUDaQGi5Lf +hFWofXt7eebgsPEKciG4xTyk51p9fy9y8asY+jCbkQKBgQDG2UqJToR8G4Fk6uaO +/XJa15TibIwQDEota0OdlXg2ZR4864fkIBv+UTbymZEM/EBuSdMM1CUBDvYHcFQX +Aj8p1LUyKP2QYwxV/OoPfJ5fBqxqONNR1fLFg7xCxnf9kSvsni2WFneQUrTDl8+7 +qnHm4IKPkAxZ4Orxl5qIBmlpGQKBgQDRZUL3cHIVLLg/aZACpo6SYDYg2bztXmz4 +lRk9j17q1uS83Umzd2lPFmSt/Nr85EKraxXZ/lYPKrP/r1pf1/35eXOWqmYBWgo/ +Hh7OzL12bhvv9UWEY/TvW+wNJNtXlJSjEFRN4tjoG2amYumyhwMO1lIulplUWvtw +ymm8hDjeRwKBgCq7n60KVqZlMtWBNbMc/GpRUgmm0iLQwVApcQp4iLEH4gutgjKg +Q+PPiENyhR2JSD9rVhO3s4warvzCQw/+x5wxvg7diEBzSL9h7tsNKOu6/2qEc8Vu +eRHBUb/37ulrPUlIZPuQMHmvjHFMOrRV2MyJCwXXKxBVqafpsKfy2MxhAoGBAIHH +Cswk6u/ouYDDwjeCVxatfp65lHhhb5RZhD09IIzYBwhu9gC+34veyyNydZ8LMa7g +PbjQAzJ/OvQbEB4a1hPKjDMzBOmNjpAz8NAm4L4H3FTKZP16nhHDnPdAgpkzQzQV +KMrk755bbTFuWH0HZIPLnT+2ou0/PltXeFUYdc59AoGBAIGfWgSOiw7aXbSQZFrO +4S0v3VTwTaiGDVS4pkNRLlhEJUhy8+gbLv/zYDmFmGtqVhXTb/nd6DOdylp+W/HS +8xNWBMWdlX/hVdSK7M0TdJvAaCaMidlquf5qZ2tGNNDeTUN1qbRH26pm8vdNZ3gr +Y/WWJGo0iEmwyB8RcFhvNmuJ +-----END PRIVATE KEY----- diff --git a/modules/azure/src/test/resources/keystore.pfx b/modules/azure/src/test/resources/keystore.pfx new file mode 100644 index 00000000000..3fd2975d3e8 Binary files /dev/null and b/modules/azure/src/test/resources/keystore.pfx differ diff --git a/modules/cockroachdb/src/test/java/org/testcontainers/CockroachDBTestImages.java b/modules/cockroachdb/src/test/java/org/testcontainers/CockroachDBTestImages.java index 6e18781f2c4..a410f77e8c8 100644 --- a/modules/cockroachdb/src/test/java/org/testcontainers/CockroachDBTestImages.java +++ b/modules/cockroachdb/src/test/java/org/testcontainers/CockroachDBTestImages.java @@ -4,6 +4,7 @@ public interface CockroachDBTestImages { DockerImageName COCKROACHDB_IMAGE = DockerImageName.parse("cockroachdb/cockroach:v22.2.3"); + DockerImageName FIRST_COCKROACHDB_IMAGE_WITH_ENV_VARS_SUPPORT = DockerImageName.parse( "cockroachdb/cockroach:v22.1.0" ); diff --git a/modules/gcloud/src/test/java/org/testcontainers/containers/BigtableEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/containers/BigtableEmulatorContainerTest.java index e68ecce405b..8a100855b3c 100644 --- a/modules/gcloud/src/test/java/org/testcontainers/containers/BigtableEmulatorContainerTest.java +++ b/modules/gcloud/src/test/java/org/testcontainers/containers/BigtableEmulatorContainerTest.java @@ -15,6 +15,7 @@ import com.google.cloud.bigtable.data.v2.models.Row; import com.google.cloud.bigtable.data.v2.models.RowCell; import com.google.cloud.bigtable.data.v2.models.RowMutation; +import com.google.cloud.bigtable.data.v2.models.TableId; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import org.junit.Rule; @@ -23,7 +24,6 @@ import java.io.IOException; import java.util.List; -import java.util.concurrent.ExecutionException; import static org.assertj.core.api.Assertions.assertThat; @@ -43,28 +43,26 @@ public class BigtableEmulatorContainerTest { @Test // testWithEmulatorContainer { - public void testSimple() throws IOException, InterruptedException, ExecutionException { + public void testSimple() throws IOException { ManagedChannel channel = ManagedChannelBuilder.forTarget(emulator.getEmulatorEndpoint()).usePlaintext().build(); TransportChannelProvider channelProvider = FixedTransportChannelProvider.create( GrpcTransportChannel.create(channel) ); NoCredentialsProvider credentialsProvider = NoCredentialsProvider.create(); - - try { - createTable(channelProvider, credentialsProvider, "test-table"); - + createTable(channelProvider, credentialsProvider, "test-table"); + try ( BigtableDataClient client = BigtableDataClient.create( BigtableDataSettings .newBuilderForEmulator(emulator.getHost(), emulator.getEmulatorPort()) .setProjectId(PROJECT_ID) .setInstanceId(INSTANCE_ID) .build() - ); - - client.mutateRow(RowMutation.create("test-table", "1").setCell("name", "firstName", "Ray")); + ) + ) { + client.mutateRow(RowMutation.create(TableId.of("test-table"), "1").setCell("name", "firstName", "Ray")); - Row row = client.readRow("test-table", "1"); + Row row = client.readRow(TableId.of("test-table"), "1"); List cells = row.getCells("name", "firstName"); assertThat(cells).isNotNull().hasSize(1); diff --git a/modules/grafana/build.gradle b/modules/grafana/build.gradle index 550779c4d55..72082e4cfec 100644 --- a/modules/grafana/build.gradle +++ b/modules/grafana/build.gradle @@ -8,16 +8,3 @@ dependencies { testImplementation 'io.micrometer:micrometer-registry-otlp:1.13.4' testImplementation 'uk.org.webcompere:system-stubs-junit4:2.1.6' } - -test { - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(11) - } -} - -compileTestJava { - javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(11) - } - options.release.set(11) -} diff --git a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/JUnitJupiterTestImages.java b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/JUnitJupiterTestImages.java index 9097c4d3457..4343b5dffc8 100644 --- a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/JUnitJupiterTestImages.java +++ b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/JUnitJupiterTestImages.java @@ -4,6 +4,8 @@ public interface JUnitJupiterTestImages { DockerImageName POSTGRES_IMAGE = DockerImageName.parse("postgres:9.6.12"); + DockerImageName HTTPD_IMAGE = DockerImageName.parse("httpd:2.4-alpine"); + DockerImageName MYSQL_IMAGE = DockerImageName.parse("mysql:8.0.32"); } diff --git a/modules/k3s/build.gradle b/modules/k3s/build.gradle index 3f795d99743..0210ed1e6a8 100644 --- a/modules/k3s/build.gradle +++ b/modules/k3s/build.gradle @@ -6,7 +6,7 @@ dependencies { // https://youtu.be/otCpCn0l4Wo // The core module depends on jackson-databind 2.8.x for backward compatibility. // Any >2.8 version here is not compatible with jackson-databind 2.8.x. - shaded 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.8.8' + shaded 'com.fasterxml.jackson.dataformat:jackson-dataformats-text:2.18.2' testImplementation 'io.fabric8:kubernetes-client:6.13.1' testImplementation 'io.kubernetes:client-java:21.0.1-legacy' diff --git a/modules/kafka/src/test/java/org/testcontainers/AbstractKafka.java b/modules/kafka/src/test/java/org/testcontainers/AbstractKafka.java index 7fc5bb3851e..90977e61c18 100644 --- a/modules/kafka/src/test/java/org/testcontainers/AbstractKafka.java +++ b/modules/kafka/src/test/java/org/testcontainers/AbstractKafka.java @@ -19,6 +19,7 @@ import java.time.Duration; import java.util.Collection; import java.util.Collections; +import java.util.Map; import java.util.Properties; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -28,7 +29,7 @@ public class AbstractKafka { - private final ImmutableMap properties = ImmutableMap.of( + private static final ImmutableMap PLAIN_PROPERTIES = ImmutableMap.of( AdminClientConfig.SECURITY_PROTOCOL_CONFIG, "SASL_PLAINTEXT", SaslConfigs.SASL_MECHANISM, @@ -37,16 +38,39 @@ public class AbstractKafka { "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"admin\" password=\"admin\";" ); + private static final ImmutableMap SCRAM_PROPERTIES = ImmutableMap.of( + AdminClientConfig.SECURITY_PROTOCOL_CONFIG, + "SASL_PLAINTEXT", + SaslConfigs.SASL_MECHANISM, + "SCRAM-SHA-256", + SaslConfigs.SASL_JAAS_CONFIG, + "org.apache.kafka.common.security.scram.ScramLoginModule required username=\"admin\" password=\"admin\";" + ); + protected void testKafkaFunctionality(String bootstrapServers) throws Exception { testKafkaFunctionality(bootstrapServers, false, 1, 1); } - protected void testSecureKafkaFunctionality(String bootstrapServers) throws Exception { - testKafkaFunctionality(bootstrapServers, true, 1, 1); + protected void testSecurePlainKafkaFunctionality(String bootstrapServers) throws Exception { + testKafkaFunctionality(bootstrapServers, true, PLAIN_PROPERTIES, 1, 1); + } + + protected void testSecureScramKafkaFunctionality(String bootstrapServers) throws Exception { + testKafkaFunctionality(bootstrapServers, true, SCRAM_PROPERTIES, 1, 1); } protected void testKafkaFunctionality(String bootstrapServers, boolean authenticated, int partitions, int rf) throws Exception { + testKafkaFunctionality(bootstrapServers, authenticated, Collections.emptyMap(), partitions, rf); + } + + protected void testKafkaFunctionality( + String bootstrapServers, + boolean authenticated, + Map authProperties, + int partitions, + int rf + ) throws Exception { ImmutableMap adminClientDefaultProperties = ImmutableMap.of( AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers @@ -75,9 +99,9 @@ protected void testKafkaFunctionality(String bootstrapServers, boolean authentic producerProperties.putAll(producerDefaultProperties); if (authenticated) { - adminClientProperties.putAll(this.properties); - consumerProperties.putAll(this.properties); - producerProperties.putAll(this.properties); + adminClientProperties.putAll(authProperties); + consumerProperties.putAll(authProperties); + producerProperties.putAll(authProperties); } try ( AdminClient adminClient = AdminClient.create(adminClientProperties); @@ -116,4 +140,14 @@ protected void testKafkaFunctionality(String bootstrapServers, boolean authentic consumer.unsubscribe(); } } + + protected static String getJaasConfig() { + String jaasConfig = + "org.apache.kafka.common.security.plain.PlainLoginModule required " + + "username=\"admin\" " + + "password=\"admin\" " + + "user_admin=\"admin\" " + + "user_test=\"secret\";"; + return jaasConfig; + } } diff --git a/modules/kafka/src/test/java/org/testcontainers/containers/KafkaContainerTest.java b/modules/kafka/src/test/java/org/testcontainers/containers/KafkaContainerTest.java index 5284209a76d..6d3578f7833 100644 --- a/modules/kafka/src/test/java/org/testcontainers/containers/KafkaContainerTest.java +++ b/modules/kafka/src/test/java/org/testcontainers/containers/KafkaContainerTest.java @@ -14,6 +14,7 @@ import org.testcontainers.Testcontainers; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; import java.util.Collection; import java.util.Collections; @@ -229,7 +230,38 @@ public void shouldConfigureAuthenticationWithSaslUsingJaas() { ) { kafka.start(); - testSecureKafkaFunctionality(kafka.getBootstrapServers()); + testSecurePlainKafkaFunctionality(kafka.getBootstrapServers()); + } + } + + @SneakyThrows + @Test + public void shouldConfigureAuthenticationWithSaslScramUsingJaas() { + try ( + KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.7.0")) { + protected String commandKraft() { + String command = "sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure\n"; + command += + "echo 'kafka-storage format --ignore-formatted -t \"" + + "$CLUSTER_ID" + + "\" --add-scram SCRAM-SHA-256=[name=admin,password=admin] -c /etc/kafka/kafka.properties' >> /etc/confluent/docker/configure\n"; + return command; + } + } + .withKraft() + .withEnv("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", "PLAINTEXT:SASL_PLAINTEXT,BROKER:SASL_PLAINTEXT") + .withEnv("KAFKA_LISTENER_NAME_PLAINTEXT_SASL_ENABLED_MECHANISMS", "SCRAM-SHA-256") + .withEnv("KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL", "SCRAM-SHA-256") + .withEnv("KAFKA_SASL_ENABLED_MECHANISMS", "SCRAM-SHA-256") + .withEnv("KAFKA_OPTS", "-Djava.security.auth.login.config=/etc/kafka/secrets/kafka_server_jaas.conf") + .withCopyFileToContainer( + MountableFile.forClasspathResource("kafka_server_jaas.conf"), + "/etc/kafka/secrets/kafka_server_jaas.conf" + ) + ) { + kafka.start(); + + testSecureScramKafkaFunctionality(kafka.getBootstrapServers()); } } @@ -313,14 +345,4 @@ public void enableSaslAndWithAuthenticationError() { }); } } - - private static String getJaasConfig() { - String jaasConfig = - "org.apache.kafka.common.security.plain.PlainLoginModule required " + - "username=\"admin\" " + - "password=\"admin\" " + - "user_admin=\"admin\" " + - "user_test=\"secret\";"; - return jaasConfig; - } } diff --git a/modules/kafka/src/test/java/org/testcontainers/kafka/ConfluentKafkaContainerTest.java b/modules/kafka/src/test/java/org/testcontainers/kafka/ConfluentKafkaContainerTest.java index 45bc846c021..725dc2d2a62 100644 --- a/modules/kafka/src/test/java/org/testcontainers/kafka/ConfluentKafkaContainerTest.java +++ b/modules/kafka/src/test/java/org/testcontainers/kafka/ConfluentKafkaContainerTest.java @@ -1,10 +1,13 @@ package org.testcontainers.kafka; +import com.github.dockerjava.api.command.InspectContainerResponse; +import lombok.SneakyThrows; import org.junit.Test; import org.testcontainers.AbstractKafka; import org.testcontainers.KCatContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.SocatContainer; +import org.testcontainers.utility.MountableFile; import static org.assertj.core.api.Assertions.assertThat; @@ -62,4 +65,60 @@ public void testUsageWithListenerFromProxy() throws Exception { testKafkaFunctionality(bootstrapServers); } } + + @SneakyThrows + @Test + public void shouldConfigureAuthenticationWithSaslUsingJaas() { + try ( + ConfluentKafkaContainer kafka = new ConfluentKafkaContainer("confluentinc/cp-kafka:7.7.0") + .withEnv( + "KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", + "PLAINTEXT:SASL_PLAINTEXT,BROKER:SASL_PLAINTEXT,CONTROLLER:PLAINTEXT" + ) + .withEnv("KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL", "PLAIN") + .withEnv("KAFKA_LISTENER_NAME_PLAINTEXT_SASL_ENABLED_MECHANISMS", "PLAIN") + .withEnv("KAFKA_LISTENER_NAME_BROKER_SASL_ENABLED_MECHANISMS", "PLAIN") + .withEnv("KAFKA_LISTENER_NAME_BROKER_PLAIN_SASL_JAAS_CONFIG", getJaasConfig()) + .withEnv("KAFKA_LISTENER_NAME_PLAINTEXT_PLAIN_SASL_JAAS_CONFIG", getJaasConfig()) + ) { + kafka.start(); + + testSecurePlainKafkaFunctionality(kafka.getBootstrapServers()); + } + } + + @SneakyThrows + @Test + public void shouldConfigureAuthenticationWithSaslScramUsingJaas() { + try ( + ConfluentKafkaContainer kafka = new ConfluentKafkaContainer("confluentinc/cp-kafka:7.7.0") { + @SneakyThrows + @Override + protected void containerIsStarting(InspectContainerResponse containerInfo) { + String command = + "echo 'kafka-storage format --ignore-formatted -t \"" + + "$CLUSTER_ID" + + "\" --add-scram SCRAM-SHA-256=[name=admin,password=admin] -c /etc/kafka/kafka.properties' >> /etc/confluent/docker/configure"; + execInContainer("bash", "-c", command); + super.containerIsStarting(containerInfo); + } + } + .withEnv( + "KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", + "PLAINTEXT:SASL_PLAINTEXT,BROKER:SASL_PLAINTEXT,CONTROLLER:PLAINTEXT" + ) + .withEnv("KAFKA_LISTENER_NAME_PLAINTEXT_SASL_ENABLED_MECHANISMS", "SCRAM-SHA-256") + .withEnv("KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL", "SCRAM-SHA-256") + .withEnv("KAFKA_SASL_ENABLED_MECHANISMS", "SCRAM-SHA-256") + .withEnv("KAFKA_OPTS", "-Djava.security.auth.login.config=/etc/kafka/secrets/kafka_server_jaas.conf") + .withCopyFileToContainer( + MountableFile.forClasspathResource("kafka_server_jaas.conf"), + "/etc/kafka/secrets/kafka_server_jaas.conf" + ) + ) { + kafka.start(); + + testSecureScramKafkaFunctionality(kafka.getBootstrapServers()); + } + } } diff --git a/modules/kafka/src/test/resources/kafka_server_jaas.conf b/modules/kafka/src/test/resources/kafka_server_jaas.conf new file mode 100644 index 00000000000..89c88ed0522 --- /dev/null +++ b/modules/kafka/src/test/resources/kafka_server_jaas.conf @@ -0,0 +1,5 @@ +KafkaServer { + org.apache.kafka.common.security.scram.ScramLoginModule required + username="admin" + password="admin"; +}; diff --git a/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java b/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java index b9a88e96102..fb7d383898a 100644 --- a/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java +++ b/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java @@ -203,7 +203,10 @@ protected void configure() { @Override protected void containerIsStarting(InspectContainerResponse containerInfo) { String command = "#!/bin/bash\n"; - command += "export LAMBDA_DOCKER_FLAGS=" + configureLambdaContainerLabels() + "\n"; + command += "export LAMBDA_DOCKER_FLAGS=" + configureServiceContainerLabels("LAMBDA_DOCKER_FLAGS") + "\n"; + command += "export ECS_DOCKER_FLAGS=" + configureServiceContainerLabels("ECS_DOCKER_FLAGS") + "\n"; + command += "export EC2_DOCKER_FLAGS=" + configureServiceContainerLabels("EC2_DOCKER_FLAGS") + "\n"; + command += "export BATCH_DOCKER_FLAGS=" + configureServiceContainerLabels("BATCH_DOCKER_FLAGS") + "\n"; command += "/usr/local/bin/docker-entrypoint.sh\n"; copyFileToContainer(Transferable.of(command, 0777), STARTER_SCRIPT); } @@ -214,13 +217,13 @@ protected void containerIsStarting(InspectContainerResponse containerInfo) { * chance. * @return the lambda container labels as a string */ - private String configureLambdaContainerLabels() { - String lambdaDockerFlags = internalMarkerLabels(); - String existingLambdaDockerFlags = getEnvMap().get("LAMBDA_DOCKER_FLAGS"); - if (existingLambdaDockerFlags != null) { - lambdaDockerFlags = existingLambdaDockerFlags + " " + lambdaDockerFlags; + private String configureServiceContainerLabels(String existingEnvFlagKey) { + String internalMarkerFlags = internalMarkerLabels(); + String existingFlags = getEnvMap().get(existingEnvFlagKey); + if (existingFlags != null) { + internalMarkerFlags = existingFlags + " " + internalMarkerFlags; } - return "\"" + lambdaDockerFlags + "\""; + return "\"" + internalMarkerFlags + "\""; } /** diff --git a/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackTestImages.java b/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackTestImages.java index d0f984f3388..08e50cc1d2a 100644 --- a/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackTestImages.java +++ b/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackTestImages.java @@ -4,11 +4,16 @@ public interface LocalstackTestImages { DockerImageName LOCALSTACK_IMAGE = DockerImageName.parse("localstack/localstack:0.12.8"); + DockerImageName LOCALSTACK_0_10_IMAGE = LOCALSTACK_IMAGE.withTag("0.10.7"); + DockerImageName LOCALSTACK_0_11_IMAGE = LOCALSTACK_IMAGE.withTag("0.11.3"); + DockerImageName LOCALSTACK_0_12_IMAGE = LOCALSTACK_IMAGE.withTag("0.12.8"); + DockerImageName LOCALSTACK_0_13_IMAGE = LOCALSTACK_IMAGE.withTag("0.13.0"); DockerImageName LOCALSTACK_2_3_IMAGE = LOCALSTACK_IMAGE.withTag("2.3"); + DockerImageName AWS_CLI_IMAGE = DockerImageName.parse("amazon/aws-cli:2.7.27"); } diff --git a/modules/ollama/src/main/java/org/testcontainers/ollama/OllamaContainer.java b/modules/ollama/src/main/java/org/testcontainers/ollama/OllamaContainer.java index 1c739b95a6b..db611a34a39 100644 --- a/modules/ollama/src/main/java/org/testcontainers/ollama/OllamaContainer.java +++ b/modules/ollama/src/main/java/org/testcontainers/ollama/OllamaContainer.java @@ -24,6 +24,8 @@ public class OllamaContainer extends GenericContainer { private static final DockerImageName DOCKER_IMAGE_NAME = DockerImageName.parse("ollama/ollama"); + private static final int OLLAMA_PORT = 11434; + public OllamaContainer(String image) { this(DockerImageName.parse(image)); } @@ -49,7 +51,7 @@ public OllamaContainer(DockerImageName dockerImageName) { }); } } - withExposedPorts(11434); + withExposedPorts(OLLAMA_PORT); } /** @@ -74,7 +76,11 @@ public void commitToImage(String imageName) { } } + public int getPort() { + return getMappedPort(OLLAMA_PORT); + } + public String getEndpoint() { - return "http://" + getHost() + ":" + getMappedPort(11434); + return "http://" + getHost() + ":" + getPort(); } } diff --git a/modules/openfga/build.gradle b/modules/openfga/build.gradle index c2e9f67c43d..6cbbaa91e68 100644 --- a/modules/openfga/build.gradle +++ b/modules/openfga/build.gradle @@ -9,13 +9,13 @@ dependencies { test { javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(11) + languageVersion = JavaLanguageVersion.of(17) } } compileTestJava { javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(11) + languageVersion = JavaLanguageVersion.of(17) } - options.release.set(11) + options.release.set(17) } diff --git a/modules/oracle-free/build.gradle b/modules/oracle-free/build.gradle index 9f1cebf0132..ac81dc66f6e 100644 --- a/modules/oracle-free/build.gradle +++ b/modules/oracle-free/build.gradle @@ -14,16 +14,3 @@ dependencies { testImplementation testFixtures(project(':r2dbc')) testRuntimeOnly 'com.oracle.database.r2dbc:oracle-r2dbc:1.2.0' } - -test { - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(11) - } -} - -compileTestJava { - javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(11) - } - options.release.set(11) -} diff --git a/modules/oracle-xe/build.gradle b/modules/oracle-xe/build.gradle index 43506725f82..3de6202e869 100644 --- a/modules/oracle-xe/build.gradle +++ b/modules/oracle-xe/build.gradle @@ -14,16 +14,3 @@ dependencies { testImplementation testFixtures(project(':r2dbc')) testRuntimeOnly 'com.oracle.database.r2dbc:oracle-r2dbc:1.2.0' } - -test { - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(11) - } -} - -compileTestJava { - javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(11) - } - options.release.set(11) -} diff --git a/modules/presto/src/test/java/org/testcontainers/PrestoTestImages.java b/modules/presto/src/test/java/org/testcontainers/PrestoTestImages.java index 174cb7b4d4a..498e09f0be4 100644 --- a/modules/presto/src/test/java/org/testcontainers/PrestoTestImages.java +++ b/modules/presto/src/test/java/org/testcontainers/PrestoTestImages.java @@ -4,5 +4,6 @@ public interface PrestoTestImages { DockerImageName PRESTO_TEST_IMAGE = DockerImageName.parse("ghcr.io/trinodb/presto:344"); + DockerImageName PRESTO_PREVIOUS_VERSION_TEST_IMAGE = DockerImageName.parse("ghcr.io/trinodb/presto:343"); } diff --git a/modules/pulsar/src/test/java/org/testcontainers/containers/AbstractPulsar.java b/modules/pulsar/src/test/java/org/testcontainers/containers/AbstractPulsar.java index 5246fc6b8ad..c704ce15d41 100644 --- a/modules/pulsar/src/test/java/org/testcontainers/containers/AbstractPulsar.java +++ b/modules/pulsar/src/test/java/org/testcontainers/containers/AbstractPulsar.java @@ -24,12 +24,16 @@ public class AbstractPulsar { protected void testPulsarFunctionality(String pulsarBrokerUrl) throws Exception { try ( PulsarClient client = PulsarClient.builder().serviceUrl(pulsarBrokerUrl).build(); - Consumer consumer = client.newConsumer().topic(TEST_TOPIC).subscriptionName("test-subs").subscribe(); + Consumer consumer = client + .newConsumer() + .topic(TEST_TOPIC) + .subscriptionName("test-subs") + .subscribe(); Producer producer = client.newProducer().topic(TEST_TOPIC).create() ) { producer.send("test containers".getBytes()); - CompletableFuture future = consumer.receiveAsync(); - Message message = future.get(5, TimeUnit.SECONDS); + CompletableFuture> future = consumer.receiveAsync(); + Message message = future.get(5, TimeUnit.SECONDS); assertThat(new String(message.getData())).isEqualTo("test containers"); } diff --git a/modules/questdb/build.gradle b/modules/questdb/build.gradle index 98adb9c40a3..01bc5473247 100644 --- a/modules/questdb/build.gradle +++ b/modules/questdb/build.gradle @@ -11,16 +11,3 @@ dependencies { testImplementation 'org.awaitility:awaitility:4.2.0' testImplementation 'org.apache.httpcomponents:httpclient:4.5.14' } - -test { - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(11) - } -} - -compileTestJava { - javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(11) - } - options.release.set(11) -} diff --git a/modules/rabbitmq/src/main/java/org/testcontainers/containers/RabbitMQContainer.java b/modules/rabbitmq/src/main/java/org/testcontainers/containers/RabbitMQContainer.java index bac998b85ac..9eff3fa83f2 100644 --- a/modules/rabbitmq/src/main/java/org/testcontainers/containers/RabbitMQContainer.java +++ b/modules/rabbitmq/src/main/java/org/testcontainers/containers/RabbitMQContainer.java @@ -9,7 +9,6 @@ import org.testcontainers.utility.MountableFile; import java.io.IOException; -import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -76,14 +75,16 @@ public RabbitMQContainer(final DockerImageName dockerImageName) { addExposedPorts(DEFAULT_AMQP_PORT, DEFAULT_AMQPS_PORT, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT); - this.waitStrategy = - Wait.forLogMessage(".*Server startup complete.*", 1).withStartupTimeout(Duration.ofSeconds(60)); + waitingFor(Wait.forLogMessage(".*Server startup complete.*", 1)); } @Override protected void configure() { - if (adminPassword != null) { - addEnv("RABBITMQ_DEFAULT_PASS", adminPassword); + if (this.adminUsername != null) { + addEnv("RABBITMQ_DEFAULT_USER", this.adminUsername); + } + if (this.adminPassword != null) { + addEnv("RABBITMQ_DEFAULT_PASS", this.adminPassword); } } @@ -105,11 +106,14 @@ protected void containerIsStarted(InspectContainerResponse containerInfo) { * @return The admin password for the admin account */ public String getAdminPassword() { - return adminPassword; + return this.adminPassword; } + /** + * @return The admin user for the admin account + */ public String getAdminUsername() { - return adminUsername; + return this.adminUsername; } public Integer getAmqpPort() { @@ -156,6 +160,17 @@ public String getHttpsUrl() { return "https://" + getHost() + ":" + getHttpsPort(); } + /** + * Sets the user for the admin (default is
guest
) + * + * @param adminUsername The admin user. + * @return This container. + */ + public RabbitMQContainer withAdminUser(final String adminUsername) { + this.adminUsername = adminUsername; + return this; + } + /** * Sets the password for the admin (default is
guest
) * diff --git a/modules/rabbitmq/src/test/java/org/testcontainers/containers/RabbitMQContainerTest.java b/modules/rabbitmq/src/test/java/org/testcontainers/containers/RabbitMQContainerTest.java index d26b4d6e924..728b2622b17 100644 --- a/modules/rabbitmq/src/test/java/org/testcontainers/containers/RabbitMQContainerTest.java +++ b/modules/rabbitmq/src/test/java/org/testcontainers/containers/RabbitMQContainerTest.java @@ -5,13 +5,15 @@ import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.DeliverCallback; import org.junit.Test; import org.testcontainers.containers.RabbitMQContainer.SslVerification; import org.testcontainers.utility.MountableFile; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; @@ -19,6 +21,7 @@ import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.Collections; +import java.util.concurrent.TimeoutException; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; @@ -40,32 +43,19 @@ public class RabbitMQContainerTest { @Test public void shouldCreateRabbitMQContainer() { try (RabbitMQContainer container = new RabbitMQContainer(RabbitMQTestImages.RABBITMQ_IMAGE)) { + container.start(); + assertThat(container.getAdminPassword()).isEqualTo("guest"); assertThat(container.getAdminUsername()).isEqualTo("guest"); - container.start(); - assertThat(container.getAmqpsUrl()) - .isEqualTo( - String.format("amqps://%s:%d", container.getHost(), container.getMappedPort(DEFAULT_AMQPS_PORT)) - ); + .isEqualTo(String.format("amqps://%s:%d", container.getHost(), container.getAmqpsPort())); assertThat(container.getAmqpUrl()) - .isEqualTo( - String.format("amqp://%s:%d", container.getHost(), container.getMappedPort(DEFAULT_AMQP_PORT)) - ); + .isEqualTo(String.format("amqp://%s:%d", container.getHost(), container.getAmqpPort())); assertThat(container.getHttpsUrl()) - .isEqualTo( - String.format("https://%s:%d", container.getHost(), container.getMappedPort(DEFAULT_HTTPS_PORT)) - ); + .isEqualTo(String.format("https://%s:%d", container.getHost(), container.getHttpsPort())); assertThat(container.getHttpUrl()) - .isEqualTo( - String.format("http://%s:%d", container.getHost(), container.getMappedPort(DEFAULT_HTTP_PORT)) - ); - - assertThat(container.getHttpsPort()).isEqualTo(container.getMappedPort(DEFAULT_HTTPS_PORT)); - assertThat(container.getHttpPort()).isEqualTo(container.getMappedPort(DEFAULT_HTTP_PORT)); - assertThat(container.getAmqpsPort()).isEqualTo(container.getMappedPort(DEFAULT_AMQPS_PORT)); - assertThat(container.getAmqpPort()).isEqualTo(container.getMappedPort(DEFAULT_AMQP_PORT)); + .isEqualTo(String.format("http://%s:%d", container.getHost(), container.getHttpPort())); assertThat(container.getLivenessCheckPortNumbers()) .containsExactlyInAnyOrder( @@ -74,6 +64,24 @@ public void shouldCreateRabbitMQContainer() { container.getMappedPort(DEFAULT_HTTP_PORT), container.getMappedPort(DEFAULT_HTTPS_PORT) ); + + assertFunctionality(container); + } + } + + @Test + public void shouldCreateRabbitMQContainerWithCustomCredentials() { + try ( + RabbitMQContainer container = new RabbitMQContainer(RabbitMQTestImages.RABBITMQ_IMAGE) + .withAdminUser("admin") + .withAdminPassword("admin") + ) { + container.start(); + + assertThat(container.getAdminPassword()).isEqualTo("admin"); + assertThat(container.getAdminUsername()).isEqualTo("admin"); + + assertFunctionality(container); } } @@ -283,7 +291,7 @@ private SSLContext createSslContext( KeyStore ks = KeyStore.getInstance("PKCS12"); ks.load( - new FileInputStream(new File(classLoader.getResource(keystoreFile).getFile())), + Files.newInputStream(new File(classLoader.getResource(keystoreFile).getFile()).toPath()), keystorePassword.toCharArray() ); KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); @@ -291,7 +299,7 @@ private SSLContext createSslContext( KeyStore trustStore = KeyStore.getInstance("PKCS12"); trustStore.load( - new FileInputStream(new File(classLoader.getResource(truststoreFile).getFile())), + Files.newInputStream(new File(classLoader.getResource(truststoreFile).getFile()).toPath()), truststorePassword.toCharArray() ); TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); @@ -301,4 +309,27 @@ private SSLContext createSslContext( c.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); return c; } + + private void assertFunctionality(RabbitMQContainer container) { + String queueName = "test-queue"; + String text = "Hello World!"; + + ConnectionFactory factory = new ConnectionFactory(); + factory.setHost(container.getHost()); + factory.setPort(container.getAmqpPort()); + factory.setUsername(container.getAdminUsername()); + factory.setPassword(container.getAdminPassword()); + try (Connection connection = factory.newConnection(); Channel channel = connection.createChannel()) { + channel.queueDeclare(queueName, false, false, false, null); + channel.basicPublish("", queueName, null, text.getBytes()); + + DeliverCallback deliverCallback = (consumerTag, delivery) -> { + String message = new String(delivery.getBody(), StandardCharsets.UTF_8); + assertThat(message).isEqualTo(text); + }; + channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {}); + } catch (IOException | TimeoutException e) { + throw new RuntimeException(e); + } + } } diff --git a/modules/redpanda/src/main/java/org/testcontainers/redpanda/RedpandaContainer.java b/modules/redpanda/src/main/java/org/testcontainers/redpanda/RedpandaContainer.java index a7cfc245d95..e8655beec45 100644 --- a/modules/redpanda/src/main/java/org/testcontainers/redpanda/RedpandaContainer.java +++ b/modules/redpanda/src/main/java/org/testcontainers/redpanda/RedpandaContainer.java @@ -45,16 +45,10 @@ public class RedpandaContainer extends GenericContainer { private static final String IMAGE_NAME = "redpandadata/redpanda"; - @Deprecated - private static final String REDPANDA_OLD_FULL_IMAGE_NAME = "docker.redpanda.com/vectorized/redpanda"; - private static final DockerImageName REDPANDA_IMAGE = DockerImageName.parse(REDPANDA_FULL_IMAGE_NAME); private static final DockerImageName IMAGE = DockerImageName.parse(IMAGE_NAME); - @Deprecated - private static final DockerImageName REDPANDA_OLD_IMAGE = DockerImageName.parse(REDPANDA_OLD_FULL_IMAGE_NAME); - private static final int REDPANDA_PORT = 9092; private static final int REDPANDA_ADMIN_PORT = 9644; @@ -82,13 +76,12 @@ public RedpandaContainer(String image) { public RedpandaContainer(DockerImageName imageName) { super(imageName); - imageName.assertCompatibleWith(REDPANDA_OLD_IMAGE, REDPANDA_IMAGE, IMAGE); + imageName.assertCompatibleWith(REDPANDA_IMAGE, IMAGE); boolean isLessThanBaseVersion = new ComparableVersion(imageName.getVersionPart()).isLessThan("v22.2.1"); boolean isPublicCompatibleImage = REDPANDA_FULL_IMAGE_NAME.equals(imageName.getUnversionedPart()) || - IMAGE_NAME.equals(imageName.getUnversionedPart()) || - REDPANDA_OLD_FULL_IMAGE_NAME.equals(imageName.getUnversionedPart()); + IMAGE_NAME.equals(imageName.getUnversionedPart()); if (isPublicCompatibleImage && isLessThanBaseVersion) { throw new IllegalArgumentException("Redpanda version must be >= v22.2.1"); } diff --git a/modules/redpanda/src/test/java/org/testcontainers/redpanda/CompatibleImageTest.java b/modules/redpanda/src/test/java/org/testcontainers/redpanda/CompatibleImageTest.java index cdb2af6a291..478d6e7efbc 100644 --- a/modules/redpanda/src/test/java/org/testcontainers/redpanda/CompatibleImageTest.java +++ b/modules/redpanda/src/test/java/org/testcontainers/redpanda/CompatibleImageTest.java @@ -15,7 +15,7 @@ public CompatibleImageTest(String image) { @Parameterized.Parameters(name = "{0}") public static String[] image() { - return new String[] { "docker.redpanda.com/vectorized/redpanda:v22.2.1", "redpandadata/redpanda:v22.2.1" }; + return new String[] { "docker.redpanda.com/redpandadata/redpanda:v22.2.1", "redpandadata/redpanda:v22.2.1" }; } @Test diff --git a/modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java b/modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java index dcdaad4da28..605ad76510a 100644 --- a/modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java +++ b/modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java @@ -67,13 +67,6 @@ public void testNotCompatibleVersion() { .hasMessageContaining("Redpanda version must be >= v22.2.1"); } - @Test - public void vectorizedRedpandaImageVersion2221ShouldNotBeCompatible() { - assertThatThrownBy(() -> new RedpandaContainer("docker.redpanda.com/vectorized/redpanda:v21.11.19")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Redpanda version must be >= v22.2.1"); - } - @Test public void redpandadataRedpandaImageVersion2221ShouldNotBeCompatible() { assertThatThrownBy(() -> new RedpandaContainer("redpandadata/redpanda:v21.11.19")) diff --git a/modules/solace/src/main/java/org/testcontainers/solace/Service.java b/modules/solace/src/main/java/org/testcontainers/solace/Service.java index de9366a0e31..6ec7de44d08 100644 --- a/modules/solace/src/main/java/org/testcontainers/solace/Service.java +++ b/modules/solace/src/main/java/org/testcontainers/solace/Service.java @@ -11,8 +11,11 @@ public enum Service { SMF_SSL("smf", 55443, "tcps", true); private final String name; + private final Integer port; + private final String protocol; + private final boolean supportSSL; Service(String name, Integer port, String protocol, boolean supportSSL) { diff --git a/modules/solr/src/main/java/org/testcontainers/containers/SolrContainer.java b/modules/solr/src/main/java/org/testcontainers/containers/SolrContainer.java index 691950f61dc..18aca269235 100644 --- a/modules/solr/src/main/java/org/testcontainers/containers/SolrContainer.java +++ b/modules/solr/src/main/java/org/testcontainers/containers/SolrContainer.java @@ -104,7 +104,7 @@ public int getZookeeperPort() { @SneakyThrows protected void configure() { if (configuration.getSolrSchema() != null && configuration.getSolrConfiguration() == null) { - throw new IllegalStateException("Solr needs to have a configuration is you want to use a schema"); + throw new IllegalStateException("Solr needs to have a configuration if you want to use a schema"); } // Generate Command Builder String command = "solr -f"; diff --git a/modules/spock/src/test/groovy/org/testcontainers/spock/PostgresContainerIT.groovy b/modules/spock/src/test/groovy/org/testcontainers/spock/PostgresContainerIT.groovy index 19edca4b459..4b28dbef5c2 100644 --- a/modules/spock/src/test/groovy/org/testcontainers/spock/PostgresContainerIT.groovy +++ b/modules/spock/src/test/groovy/org/testcontainers/spock/PostgresContainerIT.groovy @@ -41,6 +41,5 @@ class PostgresContainerIT extends Specification { cleanup: ds.close() } - } // } diff --git a/modules/trino/src/test/java/org/testcontainers/TrinoTestImages.java b/modules/trino/src/test/java/org/testcontainers/TrinoTestImages.java index 0ac56e54129..e6770bd8993 100644 --- a/modules/trino/src/test/java/org/testcontainers/TrinoTestImages.java +++ b/modules/trino/src/test/java/org/testcontainers/TrinoTestImages.java @@ -4,5 +4,6 @@ public interface TrinoTestImages { DockerImageName TRINO_TEST_IMAGE = DockerImageName.parse("trinodb/trino:352"); + DockerImageName TRINO_PREVIOUS_VERSION_TEST_IMAGE = DockerImageName.parse("trinodb/trino:351"); } diff --git a/settings.gradle b/settings.gradle index 5d8ea6e2d76..401cb2be959 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,7 +5,7 @@ buildscript { } } dependencies { - classpath "com.gradle.enterprise:com.gradle.enterprise.gradle.plugin:3.18.1" + classpath "com.gradle.enterprise:com.gradle.enterprise.gradle.plugin:3.18.2" classpath "com.gradle:common-custom-user-data-gradle-plugin:2.0.2" classpath "org.gradle.toolchains:foojay-resolver:0.8.0" } diff --git a/smoke-test/build.gradle b/smoke-test/build.gradle index d0998b5f9f2..197452e43c5 100644 --- a/smoke-test/build.gradle +++ b/smoke-test/build.gradle @@ -1,6 +1,6 @@ // empty build.gradle for dependabot plugins { - id 'com.diffplug.spotless' version '6.13.0' apply false + id 'com.diffplug.spotless' version '6.22.0' apply false } apply from: "$rootDir/../gradle/ci-support.gradle" @@ -25,7 +25,7 @@ subprojects { } checkstyle { - toolVersion = "9.3" + toolVersion = "10.12.4" configFile = rootProject.file('../config/checkstyle/checkstyle.xml') } } diff --git a/smoke-test/gradle/wrapper/gradle-wrapper.jar b/smoke-test/gradle/wrapper/gradle-wrapper.jar index d64cd491770..2c3521197d7 100644 Binary files a/smoke-test/gradle/wrapper/gradle-wrapper.jar and b/smoke-test/gradle/wrapper/gradle-wrapper.jar differ diff --git a/smoke-test/gradle/wrapper/gradle-wrapper.properties b/smoke-test/gradle/wrapper/gradle-wrapper.properties index db8c3baafe3..68e8816d71c 100644 --- a/smoke-test/gradle/wrapper/gradle-wrapper.properties +++ b/smoke-test/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=9d926787066a081739e8200858338b4a69e837c3a821a33aca9db09dd4a41026 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/smoke-test/gradlew b/smoke-test/gradlew index 1aa94a42690..f5feea6d6b1 100755 --- a/smoke-test/gradlew +++ b/smoke-test/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/smoke-test/gradlew.bat b/smoke-test/gradlew.bat index 6689b85beec..9b42019c791 100644 --- a/smoke-test/gradlew.bat +++ b/smoke-test/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail