diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 4251a2db853..fed1e5d8766 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -64,6 +64,7 @@ body: - Timeplus - ToxiProxy - Trino + - Typesense - Vault - Weaviate - YugabyteDB diff --git a/.github/ISSUE_TEMPLATE/enhancement.yaml b/.github/ISSUE_TEMPLATE/enhancement.yaml index 24d6b33ffdd..c89fc29208c 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.yaml +++ b/.github/ISSUE_TEMPLATE/enhancement.yaml @@ -64,6 +64,7 @@ body: - Timeplus - ToxiProxy - Trino + - Typesense - Vault - Weaviate - YugabyteDB diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml index d36532b7d9d..aa9bf4e7777 100644 --- a/.github/ISSUE_TEMPLATE/feature.yaml +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -64,6 +64,7 @@ body: - Timeplus - ToxiProxy - Trino + - Typesense - Vault - Weaviate - YugabyteDB 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/dependabot.yml b/.github/dependabot.yml index 22977397e03..17dd0e2aa05 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -361,6 +361,11 @@ updates: schedule: interval: "weekly" open-pull-requests-limit: 10 + - package-ecosystem: "gradle" + directory: "/modules/typesense" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/vault" schedule: diff --git a/.github/labeler.yml b/.github/labeler.yml index e88811fd50a..537f40a944b 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -232,6 +232,10 @@ - changed-files: - any-glob-to-any-file: - modules/trino/**/* +"modules/typesense": + - changed-files: + - any-glob-to-any-file: + - modules/typesense/**/* "modules/vault": - changed-files: - any-glob-to-any-file: diff --git a/.github/settings.yml b/.github/settings.yml index 7571d82d55f..b10394a894a 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -262,6 +262,9 @@ labels: - name: modules/trino color: '#006b75' + - name: modules/typesense + color: '#006b75' + - name: modules/vault color: '#006b75' diff --git a/.github/workflows/ci-rootless.yml b/.github/workflows/ci-rootless.yml index 21ff4f793c6..ba3b26a9e46 100644 --- a/.github/workflows/ci-rootless.yml +++ b/.github/workflows/ci-rootless.yml @@ -52,7 +52,7 @@ jobs: - name: Remove Docket root socket run: sudo rm -rf /var/run/docker.sock - 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 f8b89b4ab0c..45b47f364ad 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -42,6 +42,9 @@ concurrency: permissions: contents: read +env: + DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + jobs: main: if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fcdebaee5cc..87bc31863e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,7 +86,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 +121,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 +158,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 b31294b98d1..3ebfce168c6 100644 --- a/.github/workflows/moby-latest.yml +++ b/.github/workflows/moby-latest.yml @@ -6,6 +6,9 @@ on: # nightly build, at 23:59 CEST - cron: '59 23 * * *' +env: + DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + jobs: test_docker: strategy: @@ -50,7 +53,7 @@ jobs: { "tc_project": "testcontainers-java", "tc_docker_install_type": "${{ matrix.install-docker-type }}", - "tc_github_action_url": "${{ GITHUB_SERVER_URL }}/${{ GITHUB_REPOSITORY }}/actions/runs/${{ GITHUB_RUN_ID }}/job/${{ GITHUB_RUN_NUMBER }}", + "tc_github_action_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}", "tc_github_action_status": "FAILED", "tc_slack_channel_id": "${{ secrets.SLACK_DOCKER_LATEST_CHANNEL_ID }}" } 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/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..6cc5c2b8fcf 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ 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 } diff --git a/core/build.gradle b/core/build.gradle index 10368b59f1f..a4e57d9473b 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" diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index 52dede8792e..acd70f69cab 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -550,6 +550,7 @@ private void tryStart() { } else { logger().error("There are no stdout/stderr logs available for the failed container"); } + stop(); } throw new ContainerLaunchException("Could not create/start container", e); @@ -622,7 +623,14 @@ private void connectToPortForwardingNetwork(String networkMode) { .map(ContainerNetwork::getNetworkID) .ifPresent(networkId -> { if (!Arrays.asList(networkId, "none", "host").contains(networkMode)) { - dockerClient.connectToNetworkCmd().withContainerId(containerId).withNetworkId(networkId).exec(); + com.github.dockerjava.api.model.Network network = + this.dockerClient.inspectNetworkCmd().withNetworkId(networkId).exec(); + if (!network.getContainers().containsKey(this.containerId)) { + this.dockerClient.connectToNetworkCmd() + .withContainerId(this.containerId) + .withNetworkId(networkId) + .exec(); + } } }); } @@ -826,7 +834,7 @@ private void applyConfiguration(CreateContainerCmd createCommand) { withExtraHost(INTERNAL_HOST_HOSTNAME, it.getIpAddress()); }); - String[] extraHostsArray = extraHosts.stream().toArray(String[]::new); + String[] extraHostsArray = extraHosts.stream().distinct().toArray(String[]::new); createCommand.withExtraHosts(extraHostsArray); if (workingDirectory != null) { diff --git a/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java b/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java index b5b11686491..d797a86bfbf 100644 --- a/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java +++ b/core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java @@ -78,16 +78,16 @@ protected Object constructObject(Node node) { private void parseAndValidate() { final Map servicesMap; - if (composeFileContent.containsKey("version")) { - if ("2.0".equals(composeFileContent.get("version"))) { - log.warn( - "Testcontainers may not be able to clean up networks spawned using Docker Compose v2.0 files. " + - "Please see https://github.com/testcontainers/moby-ryuk/issues/2, and specify 'version: \"2.1\"' or " + - "higher in {}", - composeFileName - ); - } + if (composeFileContent.containsKey("version") && "2.0".equals(composeFileContent.get("version"))) { + log.warn( + "Testcontainers may not be able to clean up networks spawned using Docker Compose v2.0 files. " + + "Please see https://github.com/testcontainers/moby-ryuk/issues/2, and specify 'version: \"2.1\"' or " + + "higher in {}", + composeFileName + ); + } + if (composeFileContent.containsKey("services")) { final Object servicesElement = composeFileContent.get("services"); if (servicesElement == null) { log.debug( diff --git a/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java b/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java index 557e8ba6cdf..33f59c78ddb 100644 --- a/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java +++ b/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java @@ -31,7 +31,6 @@ import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; -import java.net.SocketTimeoutException; import java.net.URI; import java.security.KeyManagementException; import java.security.KeyStoreException; @@ -207,17 +206,15 @@ protected boolean test() { } try (Socket socket = socketProvider.call()) { - Duration timeout = Duration.ofMillis(200); Awaitility .await() - .atMost(TestcontainersConfiguration.getInstance().getClientPingTimeout(), TimeUnit.SECONDS) - .pollInterval(timeout) + .atMost(TestcontainersConfiguration.getInstance().getClientPingTimeout(), TimeUnit.SECONDS) // timeout after configured duration + .pollInterval(Duration.ofMillis(200)) // check state every 200ms .pollDelay(Duration.ofSeconds(0)) // start checking immediately - .ignoreExceptionsInstanceOf(SocketTimeoutException.class) - .untilAsserted(() -> socket.connect(socketAddress, (int) timeout.toMillis())); + .untilAsserted(() -> socket.connect(socketAddress)); return true; } catch (Exception e) { - log.warn("DOCKER_HOST {} is not listening", dockerHost); + log.warn("DOCKER_HOST {} is not listening", dockerHost, e); return false; } } 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/main/java/org/testcontainers/utility/RyukContainer.java b/core/src/main/java/org/testcontainers/utility/RyukContainer.java index 8d00ec15189..7175790bb6c 100644 --- a/core/src/main/java/org/testcontainers/utility/RyukContainer.java +++ b/core/src/main/java/org/testcontainers/utility/RyukContainer.java @@ -9,7 +9,7 @@ class RyukContainer extends GenericContainer { RyukContainer() { - super("testcontainers/ryuk:0.10.2"); + super("testcontainers/ryuk:0.11.0"); withExposedPorts(8080); withCreateContainerCmdModifier(cmd -> { cmd.withName("testcontainers-ryuk-" + DockerClientFactory.SESSION_ID); diff --git a/core/src/main/java/org/testcontainers/utility/RyukResourceReaper.java b/core/src/main/java/org/testcontainers/utility/RyukResourceReaper.java index 7e1b7802fb0..999b1cbbffb 100644 --- a/core/src/main/java/org/testcontainers/utility/RyukResourceReaper.java +++ b/core/src/main/java/org/testcontainers/utility/RyukResourceReaper.java @@ -77,19 +77,6 @@ private synchronized void maybeStart() { ryukContainer.start(); - Runtime - .getRuntime() - .addShutdownHook( - new Thread( - DockerClientFactory.TESTCONTAINERS_THREAD_GROUP, - () -> { - this.dockerClient.killContainerCmd(this.ryukContainer.getContainerId()) - .withSignal("SIGTERM") - .exec(); - } - ) - ); - CountDownLatch ryukScheduledLatch = new CountDownLatch(1); String host = ryukContainer.getHost(); 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/ExposedHostTest.java b/core/src/test/java/org/testcontainers/containers/ExposedHostTest.java index 4a3142d9461..4ce316fcf80 100644 --- a/core/src/test/java/org/testcontainers/containers/ExposedHostTest.java +++ b/core/src/test/java/org/testcontainers/containers/ExposedHostTest.java @@ -7,13 +7,21 @@ import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.testcontainers.DockerClientFactory; import org.testcontainers.TestImages; import org.testcontainers.Testcontainers; +import org.testcontainers.utility.TestcontainersConfiguration; +import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; +import java.util.List; +import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; public class ExposedHostTest { @@ -33,7 +41,6 @@ public static void setUpClass() throws Exception { } } ); - server.start(); } @@ -82,6 +89,61 @@ public void testExposedHostPortOnFixedInternalPorts() { assertResponse(new GenericContainer<>(tinyContainerDef()), 81); } + @Test + public void testExposedHostWithReusableContainerAndFixedNetworkName() throws IOException, InterruptedException { + assumeThat(TestcontainersConfiguration.getInstance().environmentSupportsReuse()).isTrue(); + Network network = createReusableNetwork(UUID.randomUUID()); + Testcontainers.exposeHostPorts(server.getAddress().getPort()); + + GenericContainer container = new GenericContainer<>(tinyContainerDef()).withReuse(true).withNetwork(network); + container.start(); + + assertHttpResponseFromHost(container, server.getAddress().getPort()); + + PortForwardingContainer.INSTANCE.reset(); + Testcontainers.exposeHostPorts(server.getAddress().getPort()); + + GenericContainer reusedContainer = new GenericContainer<>(tinyContainerDef()) + .withReuse(true) + .withNetwork(network); + reusedContainer.start(); + + assertThat(reusedContainer.getContainerId()).isEqualTo(container.getContainerId()); + assertHttpResponseFromHost(reusedContainer, server.getAddress().getPort()); + + container.stop(); + reusedContainer.stop(); + DockerClientFactory.lazyClient().removeNetworkCmd(network.getId()).exec(); + } + + @Test + public void testExposedHostOnFixedInternalPortsWithReusableContainerAndFixedNetworkName() + throws IOException, InterruptedException { + assumeThat(TestcontainersConfiguration.getInstance().environmentSupportsReuse()).isTrue(); + Network network = createReusableNetwork(UUID.randomUUID()); + Testcontainers.exposeHostPorts(ImmutableMap.of(server.getAddress().getPort(), 1234)); + + GenericContainer container = new GenericContainer<>(tinyContainerDef()).withReuse(true).withNetwork(network); + container.start(); + + assertHttpResponseFromHost(container, 1234); + + PortForwardingContainer.INSTANCE.reset(); + Testcontainers.exposeHostPorts(ImmutableMap.of(server.getAddress().getPort(), 1234)); + + GenericContainer reusedContainer = new GenericContainer<>(tinyContainerDef()) + .withReuse(true) + .withNetwork(network); + reusedContainer.start(); + + assertThat(reusedContainer.getContainerId()).isEqualTo(container.getContainerId()); + assertHttpResponseFromHost(reusedContainer, 1234); + + container.stop(); + reusedContainer.stop(); + DockerClientFactory.lazyClient().removeNetworkCmd(network.getId()).exec(); + } + @SneakyThrows protected void assertResponse(GenericContainer container, int port) { try { @@ -108,4 +170,40 @@ private static class TinyContainerDef extends ContainerDef { setCommand("top"); } } + + private void assertHttpResponseFromHost(GenericContainer container, int port) + throws IOException, InterruptedException { + String httpResponseFromHost = container + .execInContainer("wget", "-O", "-", "http://host.testcontainers.internal:" + port) + .getStdout(); + assertThat(httpResponseFromHost).isEqualTo("Hello World!"); + } + + private static Network createReusableNetwork(UUID name) { + String networkName = name.toString(); + Network network = new Network() { + @Override + public String getId() { + return networkName; + } + + @Override + public void close() {} + + @Override + public Statement apply(Statement base, Description description) { + return null; + } + }; + + List networks = DockerClientFactory + .lazyClient() + .listNetworksCmd() + .withNameFilter(networkName) + .exec(); + if (networks.isEmpty()) { + Network.builder().createNetworkCmdModifier(cmd -> cmd.withName(networkName)).build().getId(); + } + return network; + } } diff --git a/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java b/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java index 0ba06dd4ca8..acb24cf0d68 100644 --- a/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java +++ b/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java @@ -5,9 +5,12 @@ import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.command.InspectContainerResponse.ContainerState; +import com.github.dockerjava.api.model.Container; import com.github.dockerjava.api.model.ExposedPort; import com.github.dockerjava.api.model.Info; import com.github.dockerjava.api.model.Ports; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.experimental.FieldDefaults; @@ -28,9 +31,12 @@ import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; +import java.time.Duration; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -273,6 +279,63 @@ public void shouldRespectWaitStrategy() { } } + @Test + public void testStartupAttemptsDoesNotLeaveContainersRunningWhenWrongWaitStrategyIsUsed() { + try ( + GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) + .withLabel("waitstrategy", "wrong") + .withStartupAttempts(3) + .waitingFor( + Wait.forLogMessage("this text does not exist in logs", 1).withStartupTimeout(Duration.ofMillis(1)) + ) + .withCommand("tail", "-f", "/dev/null"); + ) { + assertThatThrownBy(container::start).hasStackTraceContaining("Retry limit hit with exception"); + } + assertThat(reportLeakedContainers()).isEmpty(); + } + + private static Optional reportLeakedContainers() { + @SuppressWarnings("resource") // Throws when close is attempted, as this is a global instance. + DockerClient dockerClient = DockerClientFactory.lazyClient(); + + List containers = dockerClient + .listContainersCmd() + .withAncestorFilter(Collections.singletonList("alpine:3.17")) + .withLabelFilter( + Arrays.asList( + DockerClientFactory.TESTCONTAINERS_SESSION_ID_LABEL + "=" + DockerClientFactory.SESSION_ID, + "waitstrategy=wrong" + ) + ) + // ignore status "exited" - for example, failed containers after using `withStartupAttempts()` + .withStatusFilter(Arrays.asList("created", "restarting", "running", "paused")) + .exec() + .stream() + .collect(ImmutableList.toImmutableList()); + + if (containers.isEmpty()) { + return Optional.empty(); + } + + return Optional.of( + String.format( + "Leaked containers: %s", + containers + .stream() + .map(container -> { + return MoreObjects + .toStringHelper("container") + .add("id", container.getId()) + .add("image", container.getImage()) + .add("imageId", container.getImageId()) + .toString(); + }) + .collect(Collectors.joining(", ", "[", "]")) + ) + ); + } + static class NoopStartupCheckStrategy extends StartupCheckStrategy { @Override diff --git a/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java b/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java index 08b2746b386..9067d77d825 100644 --- a/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java +++ b/core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java @@ -111,6 +111,19 @@ public void shouldObtainImageNamesV2() { ); } + @Test + public void shouldObtainImageNamesV2WithNoVersionTag() { + File file = new File("src/test/resources/docker-compose-imagename-parsing-v2-no-version.yml"); + ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file); + assertThat(parsedFile.getServiceNameToImageNames()) + .as("all defined images are found") + .contains( + entry("mysql", Sets.newHashSet("mysql")), + entry("redis", Sets.newHashSet("redis")), + entry("custom", Sets.newHashSet("postgres")) + ); + } + @Test public void shouldObtainImageFromDockerfileBuild() { File file = new File("src/test/resources/docker-compose-imagename-parsing-dockerfile.yml"); 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/core/src/test/resources/docker-compose-imagename-parsing-v2-no-version.yml b/core/src/test/resources/docker-compose-imagename-parsing-v2-no-version.yml new file mode 100644 index 00000000000..0b48ad3eb05 --- /dev/null +++ b/core/src/test/resources/docker-compose-imagename-parsing-v2-no-version.yml @@ -0,0 +1,9 @@ +services: + redis: + image: redis + mysql: + image: mysql + custom: + build: . +networks: + custom_network: {} diff --git a/docs/features/configuration.md b/docs/features/configuration.md index 3350c6018e7..c13c0a659e3 100644 --- a/docs/features/configuration.md +++ b/docs/features/configuration.md @@ -98,7 +98,7 @@ but does not allow starting privileged containers, you can turn off the Ryuk con ## Customizing client ping behaviour -> **client.ping.timeout = 5** +> **client.ping.timeout = 10** > Specifies for how long Testcontainers will try to connect to the Docker client to obtain valid info about the client before giving up and trying next strategy, if applicable (in seconds). ## Customizing Docker host detection 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/docs/modules/typesense.md b/docs/modules/typesense.md new file mode 100644 index 00000000000..b9828e40f33 --- /dev/null +++ b/docs/modules/typesense.md @@ -0,0 +1,30 @@ +# Typesense + +Testcontainers module for [Typesense](https://hub.docker.com/r/typesense/typesense). + +## TypesenseContainer's usage examples + +You can start an Typesense container instance from any Java application by using: + + +[Typesense container](../../modules/typesense/src/test/java/org/testcontainers/typesense/TypesenseContainerTest.java) inside_block:container + + +## Adding this module to your project dependencies + +Add the following dependency to your `pom.xml`/`build.gradle` file: + +=== "Gradle" + ```groovy + testImplementation "org.testcontainers:typesense:{{latest_version}}" + ``` + +=== "Maven" + ```xml + + org.testcontainers + typesense + {{latest_version}} + test + + ``` diff --git a/gradle.properties b/gradle.properties index 01c18a4c987..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.2 +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/mkdocs.yml b/mkdocs.yml index 3823455b0db..c308128969b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -102,6 +102,7 @@ nav: - modules/solace.md - modules/solr.md - modules/toxiproxy.md + - modules/typesense.md - modules/vault.md - modules/weaviate.md - modules/webdriver_containers.md @@ -135,4 +136,4 @@ nav: - bounty.md edit_uri: edit/main/docs/ extra: - latest_version: 1.20.2 + latest_version: 1.20.4 diff --git a/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseContainer.java b/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseContainer.java index f8e4826932c..65cb2db5738 100644 --- a/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseContainer.java +++ b/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseContainer.java @@ -1,7 +1,7 @@ package org.testcontainers.clickhouse; import org.testcontainers.containers.JdbcDatabaseContainer; -import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.time.Duration; @@ -56,11 +56,14 @@ public ClickHouseContainer(final DockerImageName dockerImageName) { dockerImageName.assertCompatibleWith(CLICKHOUSE_IMAGE_NAME); addExposedPorts(HTTP_PORT, NATIVE_PORT); - this.waitStrategy = - new HttpWaitStrategy() + waitingFor( + Wait + .forHttp("/") + .forPort(HTTP_PORT) .forStatusCode(200) .forResponsePredicate("Ok."::equals) - .withStartupTimeout(Duration.ofMinutes(1)); + .withStartupTimeout(Duration.ofMinutes(1)) + ); } @Override @@ -130,4 +133,9 @@ public ClickHouseContainer withDatabaseName(String databaseName) { this.databaseName = databaseName; return this; } + + @Override + protected void waitUntilContainerStarted() { + getWaitStrategy().waitUntilReady(this); + } } diff --git a/modules/cockroachdb/src/main/java/org/testcontainers/containers/CockroachContainer.java b/modules/cockroachdb/src/main/java/org/testcontainers/containers/CockroachContainer.java index 3a4ed8586c9..a7bb1fef7ed 100644 --- a/modules/cockroachdb/src/main/java/org/testcontainers/containers/CockroachContainer.java +++ b/modules/cockroachdb/src/main/java/org/testcontainers/containers/CockroachContainer.java @@ -1,6 +1,7 @@ package org.testcontainers.containers; -import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.containers.wait.strategy.WaitAllStrategy; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; @@ -66,19 +67,31 @@ public CockroachContainer(final String dockerImageName) { public CockroachContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); - isVersionGreaterThanOrEqualTo221 = isVersionGreaterThanOrEqualTo221(dockerImageName); + this.isVersionGreaterThanOrEqualTo221 = isVersionGreaterThanOrEqualTo221(dockerImageName); - withExposedPorts(REST_API_PORT, DB_PORT); - waitingFor( - new HttpWaitStrategy() - .forPath("/health") - .forPort(REST_API_PORT) - .forStatusCode(200) - .withStartupTimeout(Duration.ofMinutes(1)) + WaitAllStrategy waitStrategy = new WaitAllStrategy(); + waitStrategy.withStrategy( + Wait.forHttp("/health").forPort(REST_API_PORT).forStatusCode(200).withStartupTimeout(Duration.ofMinutes(1)) ); + if (this.isVersionGreaterThanOrEqualTo221) { + waitStrategy.withStrategy(Wait.forSuccessfulCommand("[ -f ./init_success ] || { exit 1; }")); + } + + withExposedPorts(REST_API_PORT, DB_PORT); + waitingFor(waitStrategy); withCommand("start-single-node --insecure"); } + @Override + protected void configure() { + withEnv("COCKROACH_USER", this.username); + withEnv("COCKROACH_PASSWORD", this.password); + if (this.password != null && !this.password.isEmpty()) { + withCommand("start-single-node"); + } + withEnv("COCKROACH_DATABASE", this.databaseName); + } + @Override public String getDriverClassName() { return JDBC_DRIVER_CLASS_NAME; @@ -123,21 +136,21 @@ public String getTestQueryString() { public CockroachContainer withUsername(String username) { validateIfVersionSupportsUsernameOrPasswordOrDatabase("username"); this.username = username; - return withEnv("COCKROACH_USER", username); + return this; } @Override public CockroachContainer withPassword(String password) { validateIfVersionSupportsUsernameOrPasswordOrDatabase("password"); this.password = password; - return withEnv("COCKROACH_PASSWORD", password).withCommand("start-single-node"); + return this; } @Override public CockroachContainer withDatabaseName(final String databaseName) { validateIfVersionSupportsUsernameOrPasswordOrDatabase("databaseName"); this.databaseName = databaseName; - return withEnv("COCKROACH_DATABASE", databaseName); + return this; } private boolean isVersionGreaterThanOrEqualTo221(DockerImageName dockerImageName) { @@ -152,4 +165,9 @@ private void validateIfVersionSupportsUsernameOrPasswordOrDatabase(String parame ); } } + + @Override + protected void waitUntilContainerStarted() { + getWaitStrategy().waitUntilReady(this); + } } diff --git a/modules/cockroachdb/src/test/java/org/testcontainers/jdbc/cockroachdb/CockroachDBJDBCDriverTest.java b/modules/cockroachdb/src/test/java/org/testcontainers/jdbc/cockroachdb/CockroachDBJDBCDriverTest.java index 86aaf6b4f63..b5e147ce553 100644 --- a/modules/cockroachdb/src/test/java/org/testcontainers/jdbc/cockroachdb/CockroachDBJDBCDriverTest.java +++ b/modules/cockroachdb/src/test/java/org/testcontainers/jdbc/cockroachdb/CockroachDBJDBCDriverTest.java @@ -14,7 +14,7 @@ public class CockroachDBJDBCDriverTest extends AbstractJDBCDriverTest { public static Iterable data() { return Arrays.asList( new Object[][] { // - { "jdbc:tc:cockroach://hostname/databasename", EnumSet.noneOf(Options.class) }, + { "jdbc:tc:cockroach:v21.2.17://hostname/databasename", EnumSet.noneOf(Options.class) }, } ); } diff --git a/modules/cockroachdb/src/test/java/org/testcontainers/junit/cockroachdb/SimpleCockroachDBTest.java b/modules/cockroachdb/src/test/java/org/testcontainers/junit/cockroachdb/SimpleCockroachDBTest.java index fe9ef5fb81c..9ea8dd6c770 100644 --- a/modules/cockroachdb/src/test/java/org/testcontainers/junit/cockroachdb/SimpleCockroachDBTest.java +++ b/modules/cockroachdb/src/test/java/org/testcontainers/junit/cockroachdb/SimpleCockroachDBTest.java @@ -4,6 +4,7 @@ import org.testcontainers.CockroachDBTestImages; import org.testcontainers.containers.CockroachContainer; import org.testcontainers.db.AbstractContainerDatabaseTest; +import org.testcontainers.images.builder.Transferable; import java.sql.ResultSet; import java.sql.SQLException; @@ -105,4 +106,25 @@ public void testAnExceptionIsThrownWhenImageDoesNotSupportEnvVars() { .isInstanceOf(UnsupportedOperationException.class) .withFailMessage("Setting a databaseName in not supported in the versions below 22.1.0"); } + + @Test + public void testInitializationScript() throws SQLException { + String sql = + "USE postgres; \n" + + "CREATE TABLE bar (foo VARCHAR(255)); \n" + + "INSERT INTO bar (foo) VALUES ('hello world');"; + + try ( + CockroachContainer cockroach = new CockroachContainer(CockroachDBTestImages.COCKROACHDB_IMAGE) + .withCopyToContainer(Transferable.of(sql), "/docker-entrypoint-initdb.d/init.sql") + .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) + ) { // CockroachDB is expected to be compatible with Postgres + cockroach.start(); + + ResultSet resultSet = performQuery(cockroach, "SELECT foo FROM bar"); + + String firstColumnValue = resultSet.getString(1); + assertThat(firstColumnValue).as("Value from init script should equal real value").isEqualTo("hello world"); + } + } } diff --git a/modules/db2/src/main/java/org/testcontainers/containers/Db2Container.java b/modules/db2/src/main/java/org/testcontainers/containers/Db2Container.java index 0cde8f0dee3..d04835cbc02 100644 --- a/modules/db2/src/main/java/org/testcontainers/containers/Db2Container.java +++ b/modules/db2/src/main/java/org/testcontainers/containers/Db2Container.java @@ -1,5 +1,6 @@ package org.testcontainers.containers; +import com.github.dockerjava.api.model.Capability; import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.LicenseAcceptance; @@ -57,7 +58,7 @@ public Db2Container(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_NEW_IMAGE_NAME, DEFAULT_IMAGE_NAME); - withPrivilegedMode(true); + withCreateContainerCmdModifier(cmd -> cmd.withCapAdd(Capability.IPC_LOCK).withCapAdd(Capability.IPC_OWNER)); this.waitStrategy = new LogMessageWaitStrategy() .withRegEx(".*Setup has completed\\..*") 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/mariadb/build.gradle b/modules/mariadb/build.gradle index fa47370764e..88734091907 100644 --- a/modules/mariadb/build.gradle +++ b/modules/mariadb/build.gradle @@ -1,9 +1,6 @@ description = "Testcontainers :: JDBC :: MariaDB" dependencies { - annotationProcessor 'com.google.auto.service:auto-service:1.1.1' - compileOnly 'com.google.auto.service:auto-service:1.1.1' - api project(':jdbc') compileOnly project(':r2dbc') diff --git a/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerProvider.java b/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerProvider.java index 6994ab44dff..34635ac239f 100644 --- a/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerProvider.java +++ b/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerProvider.java @@ -1,6 +1,5 @@ package org.testcontainers.containers; -import com.google.auto.service.AutoService; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; import org.mariadb.r2dbc.MariadbConnectionFactoryProvider; @@ -9,7 +8,6 @@ import javax.annotation.Nullable; -@AutoService(R2DBCDatabaseContainerProvider.class) public class MariaDBR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { static final String DRIVER = MariadbConnectionFactoryProvider.MARIADB_DRIVER; diff --git a/modules/mariadb/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider b/modules/mariadb/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider new file mode 100644 index 00000000000..0c312685de9 --- /dev/null +++ b/modules/mariadb/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.containers.MariaDBR2DBCDatabaseContainerProvider diff --git a/modules/mssqlserver/build.gradle b/modules/mssqlserver/build.gradle index 257d8954709..e7541434291 100644 --- a/modules/mssqlserver/build.gradle +++ b/modules/mssqlserver/build.gradle @@ -1,9 +1,6 @@ description = "Testcontainers :: MS SQL Server" dependencies { - annotationProcessor 'com.google.auto.service:auto-service:1.1.1' - compileOnly 'com.google.auto.service:auto-service:1.1.1' - api project(':jdbc') compileOnly project(':r2dbc') diff --git a/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerProvider.java b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerProvider.java index fe7ada669bd..f829ddb8f1f 100644 --- a/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerProvider.java +++ b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerProvider.java @@ -1,6 +1,5 @@ package org.testcontainers.containers; -import com.google.auto.service.AutoService; import io.r2dbc.mssql.MssqlConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; @@ -9,7 +8,6 @@ import javax.annotation.Nullable; -@AutoService(R2DBCDatabaseContainerProvider.class) public class MSSQLR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { static final String DRIVER = MssqlConnectionFactoryProvider.MSSQL_DRIVER; diff --git a/modules/mssqlserver/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider b/modules/mssqlserver/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider new file mode 100644 index 00000000000..0ec6b22ddf6 --- /dev/null +++ b/modules/mssqlserver/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.containers.MSSQLR2DBCDatabaseContainerProvider diff --git a/modules/mysql/build.gradle b/modules/mysql/build.gradle index beef298197d..d31fbcce266 100644 --- a/modules/mysql/build.gradle +++ b/modules/mysql/build.gradle @@ -1,9 +1,6 @@ description = "Testcontainers :: JDBC :: MySQL" dependencies { - annotationProcessor 'com.google.auto.service:auto-service:1.1.1' - compileOnly 'com.google.auto.service:auto-service:1.1.1' - api project(':jdbc') compileOnly project(':r2dbc') diff --git a/modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerProvider.java b/modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerProvider.java index bf74e8ec27a..97f7f4a243d 100644 --- a/modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerProvider.java +++ b/modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerProvider.java @@ -1,6 +1,5 @@ package org.testcontainers.containers; -import com.google.auto.service.AutoService; import io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; @@ -9,7 +8,6 @@ import javax.annotation.Nullable; -@AutoService(R2DBCDatabaseContainerProvider.class) public class MySQLR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { static final String DRIVER = MySqlConnectionFactoryProvider.MYSQL_DRIVER; diff --git a/modules/mysql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider b/modules/mysql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider new file mode 100644 index 00000000000..88cab78dc2e --- /dev/null +++ b/modules/mysql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.containers.MySQLR2DBCDatabaseContainerProvider diff --git a/modules/oracle-free/build.gradle b/modules/oracle-free/build.gradle index 2765c99fd92..9f1cebf0132 100644 --- a/modules/oracle-free/build.gradle +++ b/modules/oracle-free/build.gradle @@ -1,9 +1,6 @@ description = "Testcontainers :: JDBC :: Oracle Database Free" dependencies { - annotationProcessor 'com.google.auto.service:auto-service:1.1.1' - compileOnly 'com.google.auto.service:auto-service:1.1.1' - api project(':jdbc') compileOnly project(':r2dbc') diff --git a/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainerProvider.java b/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainerProvider.java index 6fe809aa214..d8fe1ee7084 100644 --- a/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainerProvider.java +++ b/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainerProvider.java @@ -1,13 +1,11 @@ package org.testcontainers.oracle; -import com.google.auto.service.AutoService; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; import org.jetbrains.annotations.Nullable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; import org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider; -@AutoService(R2DBCDatabaseContainerProvider.class) public class OracleR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { static final String DRIVER = "oracle"; diff --git a/modules/oracle-free/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider b/modules/oracle-free/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider new file mode 100644 index 00000000000..20465a3e57b --- /dev/null +++ b/modules/oracle-free/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.oracle.OracleR2DBCDatabaseContainerProvider diff --git a/modules/oracle-xe/build.gradle b/modules/oracle-xe/build.gradle index d3caec05e86..43506725f82 100644 --- a/modules/oracle-xe/build.gradle +++ b/modules/oracle-xe/build.gradle @@ -1,9 +1,6 @@ description = "Testcontainers :: JDBC :: Oracle XE" dependencies { - annotationProcessor 'com.google.auto.service:auto-service:1.1.1' - compileOnly 'com.google.auto.service:auto-service:1.1.1' - api project(':jdbc') compileOnly project(':r2dbc') diff --git a/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleR2DBCDatabaseContainerProvider.java b/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleR2DBCDatabaseContainerProvider.java index 4d6a03530c2..35a98fda77d 100644 --- a/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleR2DBCDatabaseContainerProvider.java +++ b/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleR2DBCDatabaseContainerProvider.java @@ -1,13 +1,11 @@ package org.testcontainers.containers; -import com.google.auto.service.AutoService; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; import org.jetbrains.annotations.Nullable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; import org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider; -@AutoService(R2DBCDatabaseContainerProvider.class) public class OracleR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { static final String DRIVER = "oracle"; diff --git a/modules/oracle-xe/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider b/modules/oracle-xe/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider new file mode 100644 index 00000000000..cd1df0b4488 --- /dev/null +++ b/modules/oracle-xe/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.containers.OracleR2DBCDatabaseContainerProvider diff --git a/modules/postgresql/build.gradle b/modules/postgresql/build.gradle index 834656a3dac..23ad800186b 100644 --- a/modules/postgresql/build.gradle +++ b/modules/postgresql/build.gradle @@ -1,9 +1,6 @@ description = "Testcontainers :: JDBC :: PostgreSQL" dependencies { - annotationProcessor 'com.google.auto.service:auto-service:1.1.1' - compileOnly 'com.google.auto.service:auto-service:1.1.1' - api project(':jdbc') compileOnly project(':r2dbc') diff --git a/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java b/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java index 6b4a81c5e40..f1bdf67ee5c 100644 --- a/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java +++ b/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java @@ -1,6 +1,5 @@ package org.testcontainers.containers; -import com.google.auto.service.AutoService; import io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; @@ -9,7 +8,6 @@ import javax.annotation.Nullable; -@AutoService(R2DBCDatabaseContainerProvider.class) public final class PostgreSQLR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { static final String DRIVER = PostgresqlConnectionFactoryProvider.POSTGRESQL_DRIVER; diff --git a/modules/postgresql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider b/modules/postgresql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider new file mode 100644 index 00000000000..6224c0e6093 --- /dev/null +++ b/modules/postgresql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.containers.PostgreSQLR2DBCDatabaseContainerProvider diff --git a/modules/pulsar/src/main/java/org/testcontainers/containers/PulsarContainer.java b/modules/pulsar/src/main/java/org/testcontainers/containers/PulsarContainer.java index 0181d7144d9..36485ad76a0 100644 --- a/modules/pulsar/src/main/java/org/testcontainers/containers/PulsarContainer.java +++ b/modules/pulsar/src/main/java/org/testcontainers/containers/PulsarContainer.java @@ -7,7 +7,7 @@ /** * Testcontainers implementation for Apache Pulsar. *

- * Supported image: {@code apachepulsar/pulsar} + * Supported images: {@code apachepulsar/pulsar}, {@code apachepulsar/pulsar-all} *

* Exposed ports: *

    @@ -64,7 +64,7 @@ public PulsarContainer(String pulsarVersion) { public PulsarContainer(final DockerImageName dockerImageName) { super(dockerImageName); - dockerImageName.assertCompatibleWith(DockerImageName.parse("apachepulsar/pulsar")); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, DockerImageName.parse("apachepulsar/pulsar-all")); withExposedPorts(BROKER_PORT, BROKER_HTTP_PORT); setWaitStrategy(waitAllStrategy); } diff --git a/modules/pulsar/src/test/java/org/testcontainers/containers/AbstractPulsar.java b/modules/pulsar/src/test/java/org/testcontainers/containers/AbstractPulsar.java new file mode 100644 index 00000000000..5246fc6b8ad --- /dev/null +++ b/modules/pulsar/src/test/java/org/testcontainers/containers/AbstractPulsar.java @@ -0,0 +1,67 @@ +package org.testcontainers.containers; + +import org.apache.pulsar.client.admin.ListTopicsOptions; +import org.apache.pulsar.client.admin.PulsarAdmin; +import org.apache.pulsar.client.admin.PulsarAdminException; +import org.apache.pulsar.client.api.Consumer; +import org.apache.pulsar.client.api.Message; +import org.apache.pulsar.client.api.Producer; +import org.apache.pulsar.client.api.PulsarClient; +import org.apache.pulsar.client.api.Schema; +import org.apache.pulsar.client.api.SubscriptionInitialPosition; +import org.apache.pulsar.client.api.transaction.Transaction; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AbstractPulsar { + + public static final String TEST_TOPIC = "test_topic"; + + 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(); + Producer producer = client.newProducer().topic(TEST_TOPIC).create() + ) { + producer.send("test containers".getBytes()); + CompletableFuture future = consumer.receiveAsync(); + Message message = future.get(5, TimeUnit.SECONDS); + + assertThat(new String(message.getData())).isEqualTo("test containers"); + } + } + + protected void testTransactionFunctionality(String pulsarBrokerUrl) throws Exception { + try ( + PulsarClient client = PulsarClient.builder().serviceUrl(pulsarBrokerUrl).enableTransaction(true).build(); + Consumer consumer = client + .newConsumer(Schema.STRING) + .topic("transaction-topic") + .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) + .subscriptionName("test-transaction-sub") + .subscribe(); + Producer producer = client + .newProducer(Schema.STRING) + .sendTimeout(0, TimeUnit.SECONDS) + .topic("transaction-topic") + .create() + ) { + final Transaction transaction = client.newTransaction().build().get(); + producer.newMessage(transaction).value("first").send(); + transaction.commit(); + Message message = consumer.receive(); + assertThat(message.getValue()).isEqualTo("first"); + } + } + + protected void assertTransactionsTopicCreated(PulsarAdmin pulsarAdmin) throws PulsarAdminException { + final List topics = pulsarAdmin + .topics() + .getPartitionedTopicList("pulsar/system", ListTopicsOptions.builder().includeSystemTopic(true).build()); + assertThat(topics).contains("persistent://pulsar/system/transaction_coordinator_assign"); + } +} diff --git a/modules/pulsar/src/test/java/org/testcontainers/containers/CompatibleApachePulsarImageTest.java b/modules/pulsar/src/test/java/org/testcontainers/containers/CompatibleApachePulsarImageTest.java new file mode 100644 index 00000000000..b7a9cd3ff4c --- /dev/null +++ b/modules/pulsar/src/test/java/org/testcontainers/containers/CompatibleApachePulsarImageTest.java @@ -0,0 +1,41 @@ +package org.testcontainers.containers; + +import org.apache.pulsar.client.admin.PulsarAdmin; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.testcontainers.utility.DockerImageName; + +@RunWith(Parameterized.class) +public class CompatibleApachePulsarImageTest extends AbstractPulsar { + + @Parameterized.Parameters(name = "{0}") + public static String[] params() { + return new String[] { "apachepulsar/pulsar:3.0.0", "apachepulsar/pulsar-all:3.0.0" }; + } + + @Parameterized.Parameter + public String imageName; + + @Test + public void testUsage() throws Exception { + try (PulsarContainer pulsar = new PulsarContainer(DockerImageName.parse(this.imageName));) { + pulsar.start(); + final String pulsarBrokerUrl = pulsar.getPulsarBrokerUrl(); + + testPulsarFunctionality(pulsarBrokerUrl); + } + } + + @Test + public void testTransactions() throws Exception { + try (PulsarContainer pulsar = new PulsarContainer(DockerImageName.parse(this.imageName)).withTransactions();) { + pulsar.start(); + + try (PulsarAdmin pulsarAdmin = PulsarAdmin.builder().serviceHttpUrl(pulsar.getHttpServiceUrl()).build()) { + assertTransactionsTopicCreated(pulsarAdmin); + } + testTransactionFunctionality(pulsar.getPulsarBrokerUrl()); + } + } +} diff --git a/modules/pulsar/src/test/java/org/testcontainers/containers/PulsarContainerTest.java b/modules/pulsar/src/test/java/org/testcontainers/containers/PulsarContainerTest.java index 29321afca82..779a3a6114a 100644 --- a/modules/pulsar/src/test/java/org/testcontainers/containers/PulsarContainerTest.java +++ b/modules/pulsar/src/test/java/org/testcontainers/containers/PulsarContainerTest.java @@ -1,29 +1,16 @@ package org.testcontainers.containers; -import org.apache.pulsar.client.admin.ListTopicsOptions; import org.apache.pulsar.client.admin.PulsarAdmin; import org.apache.pulsar.client.admin.PulsarAdminException; -import org.apache.pulsar.client.api.Consumer; -import org.apache.pulsar.client.api.Message; -import org.apache.pulsar.client.api.Producer; -import org.apache.pulsar.client.api.PulsarClient; -import org.apache.pulsar.client.api.Schema; -import org.apache.pulsar.client.api.SubscriptionInitialPosition; -import org.apache.pulsar.client.api.transaction.Transaction; import org.junit.Test; import org.testcontainers.utility.DockerImageName; import java.time.Duration; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -public class PulsarContainerTest { - - public static final String TEST_TOPIC = "test_topic"; +public class PulsarContainerTest extends AbstractPulsar { private static final DockerImageName PULSAR_IMAGE = DockerImageName.parse("apachepulsar/pulsar:3.0.0"); @@ -84,7 +71,8 @@ public void shouldNotEnableFunctionsWorkerByDefault() throws Exception { public void shouldWaitForFunctionsWorkerStarted() throws Exception { try ( // constructorWithFunctionsWorker { - PulsarContainer pulsar = new PulsarContainer(PULSAR_IMAGE).withFunctionsWorker(); + PulsarContainer pulsar = new PulsarContainer(DockerImageName.parse("apachepulsar/pulsar:3.0.0")) + .withFunctionsWorker(); // } ) { pulsar.start(); @@ -111,13 +99,6 @@ public void testTransactions() throws Exception { } } - private void assertTransactionsTopicCreated(PulsarAdmin pulsarAdmin) throws PulsarAdminException { - final List topics = pulsarAdmin - .topics() - .getPartitionedTopicList("pulsar/system", ListTopicsOptions.builder().includeSystemTopic(true).build()); - assertThat(topics).contains("persistent://pulsar/system/transaction_coordinator_assign"); - } - @Test public void testTransactionsAndFunctionsWorker() throws Exception { try (PulsarContainer pulsar = new PulsarContainer(PULSAR_IMAGE).withTransactions().withFunctionsWorker()) { @@ -149,41 +130,4 @@ public void testStartupTimeoutIsHonored() { .hasRootCauseMessage("Precondition failed: timeout must be greater than zero"); } } - - 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(); - Producer producer = client.newProducer().topic(TEST_TOPIC).create() - ) { - producer.send("test containers".getBytes()); - CompletableFuture future = consumer.receiveAsync(); - Message message = future.get(5, TimeUnit.SECONDS); - - assertThat(new String(message.getData())).isEqualTo("test containers"); - } - } - - protected void testTransactionFunctionality(String pulsarBrokerUrl) throws Exception { - try ( - PulsarClient client = PulsarClient.builder().serviceUrl(pulsarBrokerUrl).enableTransaction(true).build(); - Consumer consumer = client - .newConsumer(Schema.STRING) - .topic("transaction-topic") - .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) - .subscriptionName("test-transaction-sub") - .subscribe(); - Producer producer = client - .newProducer(Schema.STRING) - .sendTimeout(0, TimeUnit.SECONDS) - .topic("transaction-topic") - .create() - ) { - final Transaction transaction = client.newTransaction().build().get(); - producer.newMessage(transaction).value("first").send(); - transaction.commit(); - Message message = consumer.receive(); - assertThat(message.getValue()).isEqualTo("first"); - } - } } diff --git a/modules/r2dbc/build.gradle b/modules/r2dbc/build.gradle index a440af1dc97..1415df161fa 100644 --- a/modules/r2dbc/build.gradle +++ b/modules/r2dbc/build.gradle @@ -5,9 +5,6 @@ plugins { description = "Testcontainers :: R2DBC" dependencies { - annotationProcessor 'com.google.auto.service:auto-service:1.1.1' - compileOnly 'com.google.auto.service:auto-service:1.1.1' - api project(':testcontainers') api 'io.r2dbc:r2dbc-spi:0.9.0.RELEASE' diff --git a/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/Hidden.java b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/Hidden.java index 5d9de2025a0..327ad6ab9ca 100644 --- a/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/Hidden.java +++ b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/Hidden.java @@ -1,6 +1,5 @@ package org.testcontainers.r2dbc; -import com.google.auto.service.AutoService; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.ConnectionFactoryProvider; @@ -10,7 +9,6 @@ */ class Hidden { - @AutoService(ConnectionFactoryProvider.class) public static final class TestcontainersR2DBCConnectionFactoryProvider implements ConnectionFactoryProvider { public static final String DRIVER = "tc"; diff --git a/modules/r2dbc/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider b/modules/r2dbc/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider new file mode 100644 index 00000000000..23f1702c57a --- /dev/null +++ b/modules/r2dbc/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider @@ -0,0 +1 @@ +org.testcontainers.r2dbc.Hidden$TestcontainersR2DBCConnectionFactoryProvider 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/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/typesense/build.gradle b/modules/typesense/build.gradle new file mode 100644 index 00000000000..1a639ba2677 --- /dev/null +++ b/modules/typesense/build.gradle @@ -0,0 +1,8 @@ +description = "Testcontainers :: Typesense" + +dependencies { + api project(':testcontainers') + + testImplementation 'org.assertj:assertj-core:3.26.3' + testImplementation 'org.typesense:typesense-java:0.9.0' +} diff --git a/modules/typesense/src/main/java/org/testcontainers/typesense/TypesenseContainer.java b/modules/typesense/src/main/java/org/testcontainers/typesense/TypesenseContainer.java new file mode 100644 index 00000000000..f5ba92aee02 --- /dev/null +++ b/modules/typesense/src/main/java/org/testcontainers/typesense/TypesenseContainer.java @@ -0,0 +1,58 @@ +package org.testcontainers.typesense; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +/** + * Testcontainers implementation for Typesense. + *

    + * Supported image: {@code typesense/typesense} + *

    + * Exposed ports: 8108 + */ +public class TypesenseContainer extends GenericContainer { + + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("typesense/typesense"); + + private static final int PORT = 8108; + + private static final String DEFAULT_API_KEY = "testcontainers"; + + private String apiKey = DEFAULT_API_KEY; + + public TypesenseContainer(String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + public TypesenseContainer(DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + withExposedPorts(PORT); + withEnv("TYPESENSE_DATA_DIR", "/tmp"); + waitingFor( + Wait + .forHttp("/health") + .forStatusCode(200) + .forResponsePredicate(response -> response.contains("\"ok\":true")) + ); + } + + @Override + protected void configure() { + withEnv("TYPESENSE_API_KEY", this.apiKey); + } + + public TypesenseContainer withApiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + public String getHttpPort() { + return String.valueOf(getMappedPort(PORT)); + } + + public String getApiKey() { + return this.apiKey; + } +} diff --git a/modules/typesense/src/test/java/org/testcontainers/typesense/TypesenseContainerTest.java b/modules/typesense/src/test/java/org/testcontainers/typesense/TypesenseContainerTest.java new file mode 100644 index 00000000000..8f39a05dd33 --- /dev/null +++ b/modules/typesense/src/test/java/org/testcontainers/typesense/TypesenseContainerTest.java @@ -0,0 +1,49 @@ +package org.testcontainers.typesense; + +import org.junit.Test; +import org.typesense.api.Client; +import org.typesense.api.Configuration; +import org.typesense.resources.Node; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TypesenseContainerTest { + + @Test + public void query() throws Exception { + try ( // container { + TypesenseContainer typesense = new TypesenseContainer("typesense/typesense:27.1") + // } + ) { + typesense.start(); + List nodes = Collections.singletonList( + new Node("http", typesense.getHost(), typesense.getHttpPort()) + ); + + assertThat(typesense.getApiKey()).isEqualTo("testcontainers"); + Configuration configuration = new Configuration(nodes, Duration.ofSeconds(5), typesense.getApiKey()); + Client client = new Client(configuration); + System.out.println(client.health.retrieve()); + assertThat(client.health.retrieve()).containsEntry("ok", true); + } + } + + @Test + public void withCustomApiKey() throws Exception { + try (TypesenseContainer typesense = new TypesenseContainer("typesense/typesense:27.1").withApiKey("s3cr3t")) { + typesense.start(); + List nodes = Collections.singletonList( + new Node("http", typesense.getHost(), typesense.getHttpPort()) + ); + + assertThat(typesense.getApiKey()).isEqualTo("s3cr3t"); + Configuration configuration = new Configuration(nodes, Duration.ofSeconds(5), typesense.getApiKey()); + Client client = new Client(configuration); + assertThat(client.health.retrieve()).containsEntry("ok", true); + } + } +} diff --git a/modules/typesense/src/test/resources/logback-test.xml b/modules/typesense/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/typesense/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + 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" }